/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.session;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import divconq.bus.Message;
import divconq.bus.MessageUtil;
import divconq.bus.ServiceResult;
import divconq.ctp.CtpAdapter;
import divconq.hub.Hub;
import divconq.lang.op.FuncCallback;
import divconq.lang.op.IOperationObserver;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationContextBuilder;
import divconq.lang.op.OperationObserver;
import divconq.lang.op.OperationResult;
import divconq.lang.op.UserContext;
import divconq.log.DebugLevel;
import divconq.log.HubLog;
import divconq.log.Logger;
import divconq.struct.FieldStruct;
import divconq.struct.ListStruct;
import divconq.struct.RecordStruct;
import divconq.struct.Struct;
import divconq.util.StringUtil;
import divconq.util.TimeUtil;
import divconq.work.ISynchronousWork;
import divconq.work.TaskRun;
import divconq.work.Task;
import divconq.xml.XElement;
// TODO needs a plan system for what to do when session ends/times out/etc
public class Session {
static protected SecureRandom random = new SecureRandom();
static protected AtomicLong taskid = new AtomicLong();
static public String nextSessionId() {
return new BigInteger(130, Session.random).toString(32);
}
static public String nextUUId() {
return UUID.randomUUID().toString().replace("-", "");
}
protected String id = null;
protected String key = null;
protected long lastAccess = 0;
protected long lastTetherAccess = System.currentTimeMillis();
protected long lastReauthAccess = System.currentTimeMillis();
protected UserContext user = null;
protected DebugLevel level = null;
protected String originalOrigin = null;
protected HashMap<String, Struct> cache = new HashMap<>();
protected HashMap<String, IComponent> components = new HashMap<>();
protected ReentrantLock tasklock = new ReentrantLock();
protected HashMap<String, TaskRun> tasks = new HashMap<>();
protected ReentrantLock channellock = new ReentrantLock();
protected HashMap<String, DataStreamChannel> channels = new HashMap<>();
protected ISessionAdapter adapter = null;
protected HashMap<String, SendWaitInfo> sendwaits = new HashMap<>();
protected boolean keep = false;
/* add interactive debugging - for root only (config), gateway prohibited - base on code from
*
* https://github.com/sheehan/grails-console/blob/d6c12eea6abc0e7bfa1f78b51939b05944e0ec0b/grails3/plugin/grails-app/services/org/grails/plugins/console/ConsoleService.groovy
*
* especially this:
*
Evaluation eval(String code, boolean autoImportDomains, request) {
log.trace "eval() code: $code"
ByteArrayOutputStream baos = new ByteArrayOutputStream()
PrintStream out = new PrintStream(baos)
SystemOutputInterceptor systemOutInterceptor = createInterceptor(out)
systemOutInterceptor.start()
Evaluation evaluation = new Evaluation()
long startTime = System.currentTimeMillis()
try {
Binding binding = createBinding(request, out)
CompilerConfiguration configuration = createConfiguration(autoImportDomains)
GroovyShell groovyShell = new GroovyShell(grailsApplication.classLoader, binding, configuration)
evaluation.result = groovyShell.evaluate code
} catch (Throwable t) {
evaluation.exception = t
}
evaluation.totalTime = System.currentTimeMillis() - startTime
systemOutInterceptor.stop()
evaluation.output = baos.toString('UTF8')
evaluation
}
private static SystemOutputInterceptor createInterceptor(PrintStream out) {
new SystemOutputInterceptor({ String s ->
out.println s
return false
})
}
private Binding createBinding(request, PrintStream out) {
new Binding([
session : request.session,
request : request,
ctx : grailsApplication.mainContext,
grailsApplication: grailsApplication,
config : grailsApplication.config,
log : log,
out : out
])
}
private CompilerConfiguration createConfiguration(boolean autoImportDomains) {
CompilerConfiguration configuration = new CompilerConfiguration()
if (autoImportDomains) {
ImportCustomizer importCustomizer = new ImportCustomizer()
importCustomizer.addImports(*grailsApplication.domainClasses*.fullName)
configuration.addCompilationCustomizers importCustomizer
}
configuration
}
*/
/*
Context: {
Domain: "divconq.com",
Origin: "http:[ipaddress]",
Chronology: "/America/Chicago",
Locale: "en-US",
UserId: "119",
Username: "awhite",
FullName: "Andy White",
Email: "andy.white@divconq.com",
AuthToken: "010A0D0502",
Credentials: {
Username: "nnnn",
Password: "mmmm"
}
}
Context: {
Domain: "divconq.com",
Origin: "http:[ipaddress]",
Chronology: "/America/Chicago",
Locale: "en-US"
}
*/
public String getId() {
return this.id;
}
public HashMap<String, Struct> getCache() {
return this.cache;
}
public String getKey() {
return this.key;
}
/**
* @return logging level to use with this session (and all sub tasks)
*/
public DebugLevel getLevel() {
return this.level;
}
/**
* @param v logging level to use with this session (and all sub tasks)
*/
public void setLevel(DebugLevel v) {
this.level = v;
}
public boolean getKeep() {
return this.keep;
}
public void setKeep(boolean v) {
this.keep = v;
}
public UserContext getUser() {
return this.user;
}
public void setAdatper(ISessionAdapter v) {
this.adapter = v;
}
public ISessionAdapter getAdapter() {
return this.adapter;
}
public Session(OperationContextBuilder usrctx) {
this.id = OperationContext.getHubId() + "_" + Session.nextSessionId();
this.key = StringUtil.buildSecurityCode();
this.level = HubLog.getGlobalLevel();
this.user = UserContext.allocate(usrctx);
this.originalOrigin = "hub:";
this.touch();
}
public Session(String origin, String domainid) {
this(new OperationContextBuilder().withGuestUserTemplate().withDomainId(domainid));
this.originalOrigin = origin;
}
public Session(OperationContext ctx) {
this(ctx.getUserContext().toBuilder());
this.level = ctx.getLevel();
this.originalOrigin = ctx.getOrigin();
}
public void touch() {
this.lastAccess = System.currentTimeMillis();
//System.out.println("Session touched: " + this.id);
// keep any tethered sessions alive by pinging them at least once every minute
if ((this.lastAccess - this.lastTetherAccess > 59000) // if tether was last updated a minute or more ago
&& this.user.isAuthenticated() // if this is an authenticated user
) {
XElement config = Hub.instance.getConfig();
boolean useTether = true;
if (config != null) {
XElement sessions = config.find("Sessions");
if (sessions != null)
useTether = Struct.objectToBooleanOrFalse(sessions.getAttribute("Tether", "True"));
}
if (useTether && Hub.instance.getResources().isGateway()) { // if we are a gateway
// at least for now, gateways are only ever tethered to 1 hub, so if we find that hub we have the dest
String tid = Hub.instance.getBus().getTetherId();
if (StringUtil.isNotEmpty(tid)) {
OperationContext curr = OperationContext.get();
try {
// be sure to send the message with the correct context
this.useContext();
// send and forget the keep alive request
Message msg = new Message("Session", "Manager", "Touch",
new RecordStruct(new FieldStruct("Id", this.id)));
//System.out.println("Session pinging tethered: " + this.id);
msg.setField("ToHub", tid);
Hub.instance.getBus().sendMessage(msg);
}
finally {
OperationContext.set(curr);
}
}
}
// keep this up to date whether we are gateway or not, this way fewer checks
this.lastTetherAccess = this.lastAccess;
}
// keep auth session alive by pinging them at least once every 25 minutes
if ((this.lastAccess - this.lastReauthAccess > (25 * 60000)) && this.user.isAuthenticated()) {
if (!Hub.instance.getResources().isGateway()) { // if we are NOT a gateway
OperationContext curr = OperationContext.get();
try {
// be sure to send the message with the correct context
this.useContext();
Message vmsg = new Message("dcAuth", "Authentication", "Verify");
Hub.instance.getBus().sendMessage(vmsg, r -> {
Session.this.user = r.hasErrors() ? UserContext.allocateGuest() : r.getContext().getUserContext();
// TODO communicate to session initiator that our context has changed
});
}
finally {
OperationContext.set(curr);
}
}
// keep this up to date whether we are gateway or not, this way fewer checks
this.lastReauthAccess = this.lastAccess;
}
}
public void end() {
//System.out.println("collab session ended: " + this.collabId);
if (!this.user.looksLikeGuest() && !this.user.looksLikeRoot()) {
OperationContext curr = OperationContext.get();
try {
// be sure to send the message with the correct context
this.useContext();
Message msg = new Message("dcAuth", "Authentication", "SignOut");
Hub.instance.getBus().sendMessage(msg);
this.clearToGuest();
}
finally {
OperationContext.set(curr);
}
}
Logger.info("Ending session: " + this.id);
// TODO consider clearing adapter and reply handler too
}
public OperationContextBuilder allocateContextBuilder() {
return new OperationContextBuilder()
.withOrigin(this.originalOrigin)
.withDebugLevel(this.level)
.withSessionId(this.id);
}
public OperationContext allocateContext(OperationContextBuilder tcb) {
return OperationContext.allocate(this.user, tcb);
}
public OperationContext useContext() {
return this.useContext(this.allocateContextBuilder());
}
public OperationContext useContext(OperationContextBuilder tcb) {
OperationContext oc = OperationContext.allocate(this.user, tcb);
OperationContext.set(oc);
return oc;
}
/*
public Task allocateTaskBuilder() {
return new Task()
.withContext(this.allocateContext(this.originalOrigin));
}
public Task allocateTaskBuilder(String origin) {
return new Task()
.withContext(this.allocateContext(origin));
}
*/
public TaskRun submitTask(Task task, IOperationObserver... observers) {
TaskRun run = new TaskRun(task);
if (task == null) {
run.errorTr(213, "info");
return run;
}
// ensure we have an id
run.prep();
final String id = task.getId();
// the submitted task will now report as owned by this session - if it isn't already
String sid = task.getContext().getSessionId();
if (!this.id.equals(sid))
task.withContext(task.getContext().toBuilder().withSessionId(this.id).toOperationContext());
this.tasks.put(id, run);
for (IOperationObserver observer: observers)
task.withObserver(observer);
task.withObserver(new OperationObserver() {
@Override
public void completed(OperationContext or) {
// TODO review that this is working correctly and does not consume memory
// otherwise TaskRun complete can lookup session and remove via there - might be better
Session.this.tasks.remove(id);
}
});
Hub.instance.getWorkPool().submit(run);
return run;
}
// collect all tasks, filter by tags if any
public void collectTasks(List<TaskRun> bucket, String... tags) {
for (TaskRun task : this.tasks.values())
if ((tags.length == 0) || task.getTask().isTagged(tags))
bucket.add(task);
}
public void countTags(Map<String, Long> tagcount) {
for (TaskRun task : this.tasks.values()) {
ListStruct tags = task.getTask().getTags();
if ((tags == null) || (tags.getSize() == 0)) {
long cnt = tagcount.containsKey("[none]") ? tagcount.get("[none]") : 0;
cnt++;
tagcount.put("[none]", cnt);
}
else {
for (Struct stag : tags.getItems()) {
String tag = stag.toString();
long cnt = tagcount.containsKey(tag) ? tagcount.get(tag) : 0;
cnt++;
tagcount.put(tag, cnt);
}
}
}
}
// count all tasks, filter by tags if any
public int countTasks(String... tags) {
int num = 0;
for (TaskRun task : this.tasks.values())
if ((tags.length == 0) || task.getTask().isTagged(tags))
num++;
return num;
}
// count all tasks, filter by tags if any
public int countIncompleteTasks(String... tags) {
int num = 0;
for (TaskRun task : this.tasks.values())
if (!task.isComplete() && ((tags.length == 0) || task.getTask().isTagged(tags)))
num++;
return num;
}
public RecordStruct toStatusReport() {
RecordStruct rec = new RecordStruct();
rec.setField("Id", this.id);
rec.setField("Key", this.key);
if (this.lastAccess != 0)
rec.setField("LastAccess", TimeUtil.stampFmt.print(this.lastAccess));
if (this.user != null)
rec.setField("UserContext", this.user.freezeToRecord());
if (this.level != null)
rec.setField("DebugLevel", this.level.toString());
if (StringUtil.isNotEmpty(this.originalOrigin))
rec.setField("Origin", this.originalOrigin);
rec.setField("Keep", this.keep);
ListStruct tasks = new ListStruct();
for (TaskRun t : this.tasks.values())
tasks.addItem(t.toStatusReport());
rec.setField("Tasks", tasks);
return rec;
}
public void deliver(Message msg) {
String serv = msg.getFieldAsString("Service");
String feat = msg.getFieldAsString("Feature");
String op = msg.getFieldAsString("Op");
if ("Replies".equals(serv)) {
if ("Reply".equals(feat)) {
if ("Deliver".equals(op)) {
String tag = msg.getFieldAsString("Tag");
SendWaitInfo info = this.sendwaits.remove(tag);
// if null fall through to adapter
if (info == null) {
//if (!"SendForget".equals(tag))
// OperationContext.get().error("Missing reply handler for tag: " + tag);
}
else {
// restore original respond tag
msg.setField("RespondTag", info.respondtag);
info.callback.setReply(msg);
info.callback.complete();
return;
}
}
}
}
if (this.adapter != null)
this.adapter.deliver(msg);
}
// only allowed to be called on local session replies, not for external use because of threading
protected void reply(Message rmsg, Message msg) {
// put the reply on a new thread because of how LocalSession will build up a large call stack
// if threads don't change
rmsg.setField("Service", "Replies"); // msg.getFieldAsString("RespondTo"));
rmsg.setField("Feature", "Reply");
rmsg.setField("Op", "Deliver");
String tag = msg.getFieldAsString("RespondTag");
// should always have a tag if got here
if (StringUtil.isNotEmpty(tag)) {
// pull session id out of the tag
int pos = tag.indexOf('_', 30);
if (pos != -1)
tag = tag.substring(pos + 1);
// strip out session id, restore original tag
rmsg.setField("Tag", tag);
Hub.instance.getWorkPool().submit(new ISynchronousWork() {
@Override
public void run(TaskRun run) {
Session.this.deliver(rmsg);
}
});
}
}
/*
* Typically called by Hyper RPC
*
* we don't need a time out, it is up to the client to timeout
*
* @param msg
* @param serviceResult
*/
public void sendMessageWait(Message msg, ServiceResult serviceResult) {
SendWaitInfo swi = new SendWaitInfo();
swi.original = msg;
swi.callback = serviceResult;
swi.respondtag = msg.getFieldAsString("RespondTag");
msg.setField("RespondTag", swi.id);
this.sendwaits.put(swi.id, swi);
this.sendMessage(msg);
}
/*
* Typically called by Web and Common RPC
*
* @param msg
*/
public void sendMessage(Message msg) {
// be sure we are using a proper context
if (!OperationContext.hasContext())
this.useContext();
// note that session has been used
this.touch();
// update the credentials if present in message
if (msg.hasField("Credentials")) {
// we don't want the creds in the message root on the bus - because they should
// travel as part of the context with the message
RecordStruct newcreds = msg.getFieldAsRecord("Credentials").deepCopyExclude();
msg.removeField("Credentials");
String ckey = this.adapter.getClientKey();
if (StringUtil.isNotEmpty(ckey))
newcreds.setField("ClientKeyPrint", ckey);
else
newcreds.removeField("ClientKeyPrint");
// if the sent credentials are different from those already in context then change
// (set checks if different)
OperationContextBuilder umod = UserContext.checkCredentials(this.user, newcreds);
// credentials have changed
if (umod != null) {
this.user = umod.toUserContext();
OperationContext.set(OperationContext.allocate(this.user, OperationContext.get().freezeToRecord()));
}
}
// not valid outside of RPC calls
// msg.removeField("Session"); NOT valid at all?
String service = msg.getFieldAsString("Service");
String feature = msg.getFieldAsString("Feature");
String op = msg.getFieldAsString("Op");
// user requests a new session directly
if ("Session".equals(service)) {
// user requests end to session
if ("Control".equals(feature)) {
if ("Start".equals(op)) {
this.verifySession(new FuncCallback<Message>() {
@Override
public void callback() {
Message rmsg = this.getResult();
// TODO review how this is used/give less info to caller by default
RecordStruct body = new RecordStruct();
rmsg.setField("Body", body);
Session.this.user.freezeRpc(body);
body.setField("SessionId", Session.this.id);
//body.setField("SessionKey", Session.this.key); // TODO remove this, use only the HTTPONLY cookie for key - resolve for Java level clients
Session.this.reply(rmsg, msg);
}
});
return;
}
else if ("Stop".equals(op)) {
Session.this.reply(MessageUtil.success(), msg);
Hub.instance.getSessions().terminate(this.id);
return;
}
else if ("Touch".equals(op)) {
Session.this.reply(MessageUtil.success(), msg);
return;
}
else if ("LoadUser".equals(op)) {
Message rmsg = new Message();
// TODO review how this is used/give less info to caller by default
RecordStruct body = new RecordStruct();
rmsg.setField("Body", body);
Session.this.user.freezeRpc(body);
body.setField("SessionId", Session.this.id);
//body.setField("SessionKey", Session.this.key); // TODO remove this, use only the HTTPONLY cookie for key - resolve for Java level clients
Session.this.reply(rmsg, msg);
return;
}
else if ("ReloadUser".equals(op)) {
Message vmsg = new Message("dcAuth", "Authentication", "Verify");
Hub.instance.getBus().sendMessage(vmsg, r -> {
UserContext uc = r.hasErrors() ? UserContext.allocateGuest() : r.getContext().getUserContext();
Session.this.user = uc;
if (r.hasErrors()) {
Session.this.reply(r.getResult(), msg);
return;
}
Message rmsg = new Message();
// TODO review how this is used/give less info to caller by default
RecordStruct body = new RecordStruct();
rmsg.setField("Body", body);
Session.this.user.freezeRpc(body);
body.setField("SessionId", Session.this.id);
//body.setField("SessionKey", Session.this.key); // TODO remove this, use only the HTTPONLY cookie for key - resolve for Java level clients
Session.this.reply(rmsg, msg);
});
return;
}
}
}
// if the caller skips Session Start that is fine - but if they pass creds we verify anyway before processing the message
if (!this.user.isVerified()) {
this.verifySession(new FuncCallback<Message>() {
@Override
public void callback() {
Message rmsg = this.getResult();
if (rmsg.hasErrors())
Session.this.reply(rmsg, msg);
else
Session.this.sendMessageThru(msg);
}
});
}
else {
Session.this.sendMessageThru(msg);
}
}
// if session has an unverified user, verify it
public void verifySession(FuncCallback<Message> cb) {
boolean waslikeguest = this.user.looksLikeGuest();
OperationContext tc = OperationContext.get();
tc.verify(new FuncCallback<UserContext>() {
@Override
public void callback() {
UserContext uc = this.getResult();
if (uc != null) {
// it would not be ok to store TaskContext here because of elevation
// but user context can be stored
Session.this.user = uc;
// although we typically do not change the context of a callback, in this case we should
// so the user verify will travel along with the request message
OperationContext.switchUser(this.getContext(), uc);
boolean nowlikeguest = uc.looksLikeGuest();
if (nowlikeguest && !waslikeguest)
cb.error(1, "User not authenticated!");
}
if (cb != null) {
cb.setResult(this.toLogMessage());
cb.complete();
}
}
});
}
private void sendMessageThru(final Message msg) {
// TODO make sure the message has been validated by now
String service = msg.getFieldAsString("Service");
String feature = msg.getFieldAsString("Feature");
String op = msg.getFieldAsString("Op");
if ("Session".equals(service)) {
if ("Control".equals(feature)) {
if ("CheckInBox".equals(op)) {
Message reply = MessageUtil.success();
if (this.adapter != null)
reply.setField("Body", this.adapter.popMessages());
Session.this.reply(reply, msg);
return;
}
if ("CheckJob".equals(op)) {
RecordStruct rec = msg.getFieldAsRecord("Body");
Long jid = rec.getFieldAsInteger("JobId");
TaskRun info = this.tasks.get(jid);
if (info != null) {
Struct res = info.getResult();
Message reply = info.toLogMessage();
reply.setField("Body",
new RecordStruct(
new FieldStruct("AmountCompleted", info.getContext().getAmountCompleted()),
new FieldStruct("Steps", info.getContext().getSteps()),
new FieldStruct("CurrentStep", info.getContext().getCurrentStep()),
new FieldStruct("CurrentStepName", info.getContext().getCurrentStepName()),
new FieldStruct("ProgressMessage", info.getContext().getProgressMessage()),
new FieldStruct("Result", res)
)
);
Session.this.reply(reply, msg);
}
else {
Message reply = MessageUtil.error(1, "Job Not Found"); // TODO
Session.this.reply(reply, msg);
}
return;
}
if ("ClearJob".equals(op)) {
RecordStruct rec = msg.getFieldAsRecord("Body");
Long jid = rec.getFieldAsInteger("JobId");
this.tasks.remove(jid);
Session.this.reply(MessageUtil.success(), msg);
return;
}
if ("KillJob".equals(op)) {
RecordStruct rec = msg.getFieldAsRecord("Body");
Long jid = rec.getFieldAsInteger("JobId");
// get not remove, because kill should do the remove and we let it do it in the natural way
TaskRun info = this.tasks.get(jid);
if (info != null)
info.kill();
Session.this.reply(MessageUtil.success(), msg);
return;
}
/* TODO someday support an interactive groovy shell via any session, assuming SysAdmin access and system wide setting
Binding b = new Binding();
b.setVariable("x", 1);
b.setVariable("y", 2);
b.setVariable("z", 3);
GroovyShell sh = new GroovyShell(b);
sh.evaluate("print z");
sh.evaluate("d = 1");
sh.evaluate("print d");
sh.evaluate("println divconq.util.HashUtil.getMd5('abcxyz')");
sh.evaluate("import divconq.util.HashUtil");
sh.evaluate("println HashUtil.getMd5('abcxyz')");
*
*
* consider
* http://mrhaki.blogspot.co.uk/2011/06/groovy-goodness-add-imports.html
*
*
*/
}
else if ("DataChannel".equals(feature)) {
this.dataChannel(msg);
return;
}
}
this.sendMessageIn(msg);
}
private void sendMessageIn(final Message msg) {
// so that responses come to the Sessions service
// the id will be stripped off before delivery to client
String resptag = msg.getFieldAsString("RespondTag");
if (StringUtil.isNotEmpty(resptag)) {
msg.setField("RespondTag", this.id + "_" + resptag);
msg.setField("RespondTo", "Session");
}
/*
System.out.println("------------");
System.out.println("elevated: " + tc.isElevated());
System.out.println("user: " + tc.getUserContext());
System.out.println("message: " + msg);
System.out.println("------------");
*/
OperationResult smor = Hub.instance.getBus().sendMessage(msg);
if (smor.hasErrors())
Session.this.reply(smor.toLogMessage(), msg);
}
public void addChannel(DataStreamChannel v) {
this.channellock.lock();
try {
this.channels.put(v.getId(), v);
}
finally {
this.channellock.unlock();
}
}
public DataStreamChannel getChannel(String id) {
return this.channels.get(id);
}
public void removeChannel(String id) {
this.channellock.lock();
try {
this.channels.remove(id);
}
finally {
this.channellock.unlock();
}
}
public void dataChannel(final Message msg) {
String op = msg.getFieldAsString("Op");
if ("Establish".equals(op)) {
DataStreamChannel chan = new DataStreamChannel(this.getId(), msg.getFieldAsRecord("Body").getFieldAsString("Title"));
RecordStruct sr = msg.getFieldAsRecord("Body").getFieldAsRecord("StreamRequest");
RecordStruct srb = sr.getFieldAsRecord("Body");
if (srb == null) {
Session.this.reply(MessageUtil.error(0, "Missing StreamRequest Body"), msg);
return;
}
// add to the existing fields - which might typically be "FilePath" or "Token"
srb.setField("Channel", chan.getId());
Message srmsg = MessageUtil.fromRecord(sr);
Hub.instance.getBus().sendMessage(srmsg, res -> {
if (res.hasErrors()) {
res.error(1, "Start Upload error: " + res.getMessage());
Session.this.reply(res.toLogMessage(), msg);
return;
}
RecordStruct srrec = res.getBodyAsRec();
if (srrec == null) {
Session.this.reply(MessageUtil.error(1, "Start Upload error: Missing StreamRequest response"), msg);
return;
}
Session.this.addChannel(chan);
chan.setBinding((RecordStruct) srrec.deepCopy());
// protect from client view
srrec.removeField("Hub");
srrec.removeField("Session");
srrec.removeField("Channel");
// include the client end of the channel
srrec.setField("ChannelId", chan.getId());
Session.this.reply(MessageUtil.success(srrec), msg);
});
return;
}
if ("Free".equals(op)) {
String chid = msg.getFieldAsRecord("Body").getFieldAsString("ChannelId");
this.removeChannel(chid);
Session.this.reply(MessageUtil.success(), msg);
return;
}
/*
if ("Allocate".equals(op)) {
DataStreamChannel chan = new DataStreamChannel(this.getId(), msg.getFieldAsRecord("Body").getFieldAsString("Title"));
this.addChannel(chan);
Session.this.reply(MessageUtil.success("ChannelId", chan.getId()), msg);
return;
}
if ("Bind".equals(op)) {
RecordStruct rec = msg.getFieldAsRecord("Body");
String chid = rec.getFieldAsString("ChannelId");
DataStreamChannel chan = this.getChannel(chid);
if (chan == null) {
Session.this.reply(MessageUtil.error(1, "Missing channel"), msg);
return;
}
chan.setBinding(rec);
// TODO tell the channel it is a dest or src
Session.this.reply(MessageUtil.success(), msg);
return;
}
*/
Session.this.reply(MessageUtil.errorTr(441, "Session", "DataChannel", op), msg);
}
public void clearToGuest() {
this.user = new OperationContextBuilder()
.withGuestUserTemplate()
.withDomainId(this.user.getDomainId())
.toUserContext();
}
public void setToRoot() {
this.user = UserContext.allocateRoot();
}
public boolean reviewPlan(long clearGuest, long clearUser) {
// TODO add plans into mix - check both tasks and channels for completeness (terminate only on complete, vs on timeout, vs never)
// TODO add session plan features
// review get called often - optimize so that as few objects as possible are
// created each time this is called
if (this.channels.size() > 0) {
// cleannup expired channels
List<DataStreamChannel> killlist = null;
this.channellock.lock();
try {
for (DataStreamChannel chan : this.channels.values()) {
if (chan.isHung()) {
if (killlist == null)
killlist = new ArrayList<>();
killlist.add(chan);
}
}
}
finally {
this.channellock.unlock();
}
if (killlist != null) {
for (DataStreamChannel chan : killlist) {
Logger.warn("Session " + this.id + " found hung transfer: " + chan);
chan.abort();
}
}
}
if (this.sendwaits.size() > 0) {
// cleannup expired channels
List<SendWaitInfo> killlist = null;
this.channellock.lock();
try {
for (SendWaitInfo chan : this.sendwaits.values()) {
if (chan.isHung()) {
if (killlist == null)
killlist = new ArrayList<>();
killlist.add(chan);
}
}
}
finally {
this.channellock.unlock();
}
if (killlist != null) {
for (SendWaitInfo chan : killlist) {
Logger.warn("Session " + this.id + " found hung send wait: " + chan);
this.sendwaits.remove(chan.id);
}
}
}
if (this.isLongRunning())
return ((this.lastAccess > clearUser) || this.keep);
return ((this.lastAccess > clearGuest) || this.keep);
}
// user sessions can be idle for a longer time (3 minutes default) than guest sessions (75 seconds default)
// why? because we need to keep tethered sessions going and we only want to send message once every minute
// and it might take user 1 minute to update their session, so to be sure to keep everything in line
// requires > 2 minutes
public boolean isLongRunning() {
return this.user.isTagged("User");
}
public Collection<DataStreamChannel> channels() {
return this.channels.values();
}
public CtpAdapter allocateCtpAdapter() {
return new CtpAdapter(this.allocateContext(this.allocateContextBuilder()));
}
public class SendWaitInfo {
protected String id = StringUtil.buildSecurityCode(); // Session.nextUUId();
protected ServiceResult callback = null;
protected Message original = null;
protected String respondtag = null;
protected long started = System.currentTimeMillis();
// give two minutes
protected boolean isHung() {
return (this.started < (System.currentTimeMillis() - 120000));
}
}
}