/*
* Created on Feb 20, 2007 Copyright (C) 2001-6, Anthony Harrison anh23@pitt.edu
* (jactr.org) This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the License,
* or (at your option) any later version. This library is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU Lesser General Public License for more details. You should have
* received a copy of the GNU Lesser General Public License along with this
* library; if not, write to the Free Software Foundation, Inc., 59 Temple
* Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.jactr.tools.async.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import org.antlr.runtime.CommonToken;
import org.antlr.runtime.tree.CommonTree;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.commonreality.net.session.ISessionInfo;
import org.jactr.core.concurrent.ExecutorServices;
import org.jactr.core.model.IModel;
import org.jactr.core.model.event.IModelListener;
import org.jactr.core.model.event.ModelEvent;
import org.jactr.core.model.event.ModelListenerAdaptor;
import org.jactr.core.production.IProduction;
import org.jactr.core.runtime.ACTRRuntime;
import org.jactr.core.runtime.controller.IController;
import org.jactr.core.runtime.controller.debug.IDebugController;
import org.jactr.core.runtime.controller.debug.event.BreakpointEvent;
import org.jactr.core.runtime.controller.debug.event.IBreakpointListener;
import org.jactr.core.runtime.event.ACTRRuntimeAdapter;
import org.jactr.core.runtime.event.ACTRRuntimeEvent;
import org.jactr.core.runtime.event.IACTRRuntimeListener;
import org.jactr.core.utils.parameter.IParameterized;
import org.jactr.instrument.IInstrument;
import org.jactr.io.antlr3.builder.JACTRBuilder;
import org.jactr.io.resolver.ASTResolver;
import org.jactr.tools.async.common.NetworkedEndpoint;
import org.jactr.tools.async.controller.handlers.BreakpointHandler;
import org.jactr.tools.async.controller.handlers.LoginHandler;
import org.jactr.tools.async.controller.handlers.LogoutHandler;
import org.jactr.tools.async.controller.handlers.ModelStateHandler;
import org.jactr.tools.async.controller.handlers.ProductionHandler;
import org.jactr.tools.async.controller.handlers.RuntimeStateHandler;
import org.jactr.tools.async.message.command.breakpoint.BreakpointCommand;
import org.jactr.tools.async.message.command.breakpoint.ProductionCommand;
import org.jactr.tools.async.message.command.login.LoginCommand;
import org.jactr.tools.async.message.command.login.LogoutCommand;
import org.jactr.tools.async.message.command.state.ModelStateCommand;
import org.jactr.tools.async.message.command.state.RuntimeStateCommand;
import org.jactr.tools.async.message.event.data.BreakpointReachedEvent;
import org.jactr.tools.async.message.event.data.ModelDataEvent;
import org.jactr.tools.async.message.event.login.LoginAcknowledgedMessage;
import org.jactr.tools.async.message.event.state.IStateEvent;
import org.jactr.tools.async.message.event.state.ModelStateEvent;
import org.jactr.tools.async.message.event.state.RuntimeStateEvent;
/**
* An instrument that permits the remote control of a runtime. The remote
* interface sets up the connection and networking on the runtime side so that a
* ShadowController can issue commands and receive run events
*
* @author developer
*/
public class RemoteInterface extends NetworkedEndpoint implements IInstrument,
IParameterized
{
static public final String CREDENTIALS_KEY = "ri.credentials";
/**
* logger definition
*/
static final Log LOGGER = LogFactory
.getLog(RemoteInterface.class);
static public final String EXECUTOR_PARAM = "Executor";
static public final String SEND_MODEL_ON_SUSPEND_PARAM = "SendModelOnSuspend";
/*
* for start, stop, etc
*/
protected IACTRRuntimeListener _runtimeListener;
protected IBreakpointListener _breakpointListener;
protected IModelListener _modelListener;
private String _executorName = ExecutorServices.BACKGROUND;
private Executor _executor = ExecutorServices
.getExecutor(_executorName);
private boolean _sendOnSuspend;
private boolean _connectedToRuntime;
private Collection<IModel> _installedModels;
static private RemoteInterface _activeInterface;
static public RemoteInterface getActiveRemoteInterface()
{
synchronized (RemoteInterface.class)
{
return _activeInterface;
}
}
static protected void setActiveRemoteInterface(RemoteInterface remoteInterface)
{
synchronized (RemoteInterface.class)
{
if (remoteInterface != null && _activeInterface != null)
throw new RuntimeException("There is already a RemoteInterface active");
_activeInterface = remoteInterface;
}
}
/**
*
*/
public RemoteInterface()
{
ACTRRuntime.getRuntime();
_installedModels = new ArrayList<IModel>();
_modelListener = new ModelListenerAdaptor() {
@Override
public void modelStarted(ModelEvent me)
{
String name = me.getSource().getName();
if (LOGGER.isDebugEnabled()) LOGGER.debug(name + " has started");
try
{
getSession().write(
new ModelStateEvent(name, IStateEvent.State.STARTED, me
.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message ", e);
}
}
@Override
public void modelStopped(ModelEvent me)
{
String name = me.getSource().getName();
if (LOGGER.isDebugEnabled()) LOGGER.debug(name + " has stopped");
try
{
ISessionInfo session = getSession();
if (_sendOnSuspend)
session.write(new ModelDataEvent(me.getSource()));
if (me.getException() != null)
session.write(new ModelStateEvent(name, me.getException(), me
.getSimulationTime()));
else
session.write(new ModelStateEvent(name, IStateEvent.State.STOPPED,
me.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
@Override
public void modelSuspended(ModelEvent me)
{
String name = me.getSource().getName();
if (LOGGER.isDebugEnabled()) LOGGER.debug(name + " has suspended");
try
{
ISessionInfo session = getSession();
if (_sendOnSuspend)
session.write(new ModelDataEvent(me.getSource()));
session.write(new ModelStateEvent(name, IStateEvent.State.SUSPENDED,
me.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
@Override
public void modelResumed(ModelEvent me)
{
try
{
String name = me.getSource().getName();
if (LOGGER.isDebugEnabled()) LOGGER.debug(name + " has resumed");
getSession().write(
new ModelStateEvent(name, IStateEvent.State.RESUMED, me
.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
};
_runtimeListener = new ACTRRuntimeAdapter() {
@Override
public void runtimeResumed(ACTRRuntimeEvent event)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("Runtime resumed");
try
{
getSession().writeAndWait(
new RuntimeStateEvent(IStateEvent.State.RESUMED, event
.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
@Override
public void runtimeStarted(ACTRRuntimeEvent event)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("Runtime started");
try
{
getSession().writeAndWait(
new RuntimeStateEvent(ACTRRuntime.getRuntime().getModels(), event
.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
@Override
public void runtimeStopped(final ACTRRuntimeEvent event)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("runtime stopped");
if (LOGGER.isDebugEnabled()) LOGGER.debug("Sending stop notice");
try
{
ISessionInfo<?> session = getSession();
if (session != null)
if (event.getException() != null)
session.writeAndWait(new RuntimeStateEvent(event.getException(),
event.getSimulationTime()));
else
session.writeAndWait(new RuntimeStateEvent(
IStateEvent.State.STOPPED, event.getSimulationTime()));
// send the logout, shadow controller will echo it back and we'll
// disconnect then.
// we could send now, which could result in immediate termination
// or we can send it on the back ground executor so that everyone
// else has a chance to finish up first.
try
{
ExecutorServices.getExecutor(ExecutorServices.BACKGROUND).execute(
new Runnable() {
public void run()
{
try
{
ISessionInfo<?> session = getSession();
if (session != null)
session.writeAndWait(new LogoutCommand());
}
catch (Exception e)
{
// TODO Auto-generated catch block
LOGGER.error(".run threw Exception : ", e);
}
}
});
}
catch (Exception e)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug(e);
}
}
catch (IllegalStateException ise)
{
/*
* can happen if we've disconnected already
*/
if (LOGGER.isDebugEnabled())
LOGGER.debug("Could not send stop notice, already disconnected");
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
@Override
public void runtimeSuspended(ACTRRuntimeEvent event)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("runtime suspended");
try
{
getSession().writeAndWait(
new RuntimeStateEvent(IStateEvent.State.SUSPENDED, event
.getSimulationTime()));
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
};
_breakpointListener = new IBreakpointListener() {
public void breakpointReached(BreakpointEvent be)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("Breakpoint reached");
Object value = be.getDetails();
CommonTree details = null;
if (value instanceof IProduction)
details = ASTResolver.toAST((IProduction) value);
else
details = new CommonTree(new CommonToken(JACTRBuilder.STRING, ""
+ value));
BreakpointReachedEvent bpre = new BreakpointReachedEvent(be.getSource()
.getName(), be.getType(), be.getSimulationTime(), details);
if (LOGGER.isDebugEnabled())
LOGGER.debug("Sending breakpoint reached " + bpre);
try
{
getSession().writeAndWait(bpre);
}
catch (Exception e)
{
LOGGER.error("Failed to send message", e);
}
}
};
/*
* we can't install just yet because we need the executor
*/
}
@Override
protected void createDefaultHandlers()
{
super.createDefaultHandlers();
/**
* and our default handlers..
*/
_defaultHandlers.put(LoginCommand.class, new LoginHandler());
_defaultHandlers.put(RuntimeStateCommand.class, new RuntimeStateHandler());
_defaultHandlers.put(ModelStateCommand.class, new ModelStateHandler());
_defaultHandlers.put(LogoutCommand.class, new LogoutHandler());
IController controller = ACTRRuntime.getRuntime().getController();
if (controller instanceof IDebugController)
{
_defaultHandlers.put(BreakpointCommand.class, new BreakpointHandler());
_defaultHandlers.put(ProductionCommand.class, new ProductionHandler());
}
/*
* exception handling? set after the session is opened
*/
}
@Override
protected void sessionOpened(ISessionInfo<?> session)
{
/*
* make sure only one client can connect
*/
if (getActiveSession() != null)
{
LOGGER.warn(String.format("Another client has attempted to connect %s",
session));
try
{
session.writeAndWait(new LoginAcknowledgedMessage(false,
"Another client already connected"));
session.close();
}
catch (Exception e)
{
// TODO Auto-generated catch block
LOGGER.error("RemoteInterface.sessionOpened threw Exception : ", e);
}
return;
}
super.sessionOpened(session);
try
{
session.writeAndWait(new LoginAcknowledgedMessage(true,
"You are controller"));
}
catch (Exception e)
{
// TODO Auto-generated catch block
LOGGER.error("RemoteInterface.sessionOpened threw Exception : ", e);
}
/**
* horrible exception handler..
*/
session.addExceptionHandler((s, t) -> {
try
{
String message = t.getMessage();
if (!(t instanceof IOException) && message != null
&& message.indexOf("Connection reset") == -1)
LOGGER.error(String.format("Exception caught from %s, closing ", s),
t);
else
LOGGER.debug("Closing after connection reset");
if (s.isConnected() && !s.isClosing()) s.close();
}
catch (Exception e)
{
LOGGER.error(String.format("Exception from %s, closing. ", s), e);
}
return true;
});
}
public Executor getExecutor()
{
return _executor;
}
public void setExecutor(Executor executor)
{
_executor = executor;
}
/**
* @see org.jactr.instrument.IInstrument#initialize()
*/
public void initialize()
{
}
/**
* @see org.jactr.instrument.IInstrument#install(org.jactr.core.model.IModel)
*/
public void install(IModel model)
{
_installedModels.add(model);
model.addListener(_modelListener, getExecutor());
if (!_connectedToRuntime) try
{
/*
* now let's start this beatch
*/
connect();
}
catch (RuntimeException re)
{
throw re;
}
catch (Exception e)
{
throw new RuntimeException("Could not start RemoteInterface ", e);
}
}
/**
* @see org.jactr.instrument.IInstrument#uninstall(org.jactr.core.model.IModel)
*/
public void uninstall(IModel model)
{
_installedModels.remove(model);
model.removeListener(_modelListener);
}
@Override
protected void disconnect(boolean force) throws Exception
{
try
{
if (LOGGER.isDebugEnabled())
LOGGER.debug("Disconnecting remote interface");
ACTRRuntime runtime = ACTRRuntime.getRuntime();
runtime.removeListener(_runtimeListener);
IController controller = runtime.getController();
if (controller instanceof IDebugController)
((IDebugController) controller).removeListener(_breakpointListener);
super.disconnect(force);
}
finally
{
setActiveRemoteInterface(null);
/*
* now we remove our listeners
*/
_connectedToRuntime = false;
}
}
/**
* safely disconnect asynchronously.. this queues up the disconnect on the
* background executor, allowing other processing that has already been queued
* to finish up
*/
public void disconnectSafe(final boolean force)
{
Runnable disconnect = new Runnable() {
public void run()
{
try
{
disconnect(force);
}
catch (Exception e)
{
LOGGER.error("Could not disconnect", e);
}
}
};
try
{
getExecutor().execute(disconnect);
}
catch (RejectedExecutionException e)
{
if (LOGGER.isDebugEnabled())
LOGGER
.debug(String
.format("Execution of clean disconnect was rejected. Already shutting down."));
}
}
@Override
protected void connect() throws Exception
{
try
{
/*
* now we can install the listeners
*/
ACTRRuntime runtime = ACTRRuntime.getRuntime();
runtime.addListener(_runtimeListener, getExecutor());
IController controller = runtime.getController();
if (controller instanceof IDebugController)
((IDebugController) controller).addListener(_breakpointListener,
getExecutor());
super.connect();
setActiveRemoteInterface(this);
_connectedToRuntime = true;
}
catch (Exception e)
{
setActiveRemoteInterface(null);
throw e;
}
}
@Override
public Collection<String> getPossibleParameters()
{
ArrayList<String> rtn = new ArrayList<String>(super.getPossibleParameters());
rtn.add(EXECUTOR_PARAM);
rtn.add(SEND_MODEL_ON_SUSPEND_PARAM);
return rtn;
}
@Override
public String getParameter(String key)
{
if (EXECUTOR_PARAM.equalsIgnoreCase(key)) return _executorName;
if (SEND_MODEL_ON_SUSPEND_PARAM.equalsIgnoreCase(key))
return "" + _sendOnSuspend;
return super.getParameter(key);
}
@Override
public void setParameter(String key, String value)
{
if (EXECUTOR_PARAM.equalsIgnoreCase(key))
{
Executor ex = ExecutorServices.getExecutor(value);
if (ex == null)
throw new RuntimeException("Could not find executor named " + value);
_executorName = value;
setExecutor(ex);
}
else if (SEND_MODEL_ON_SUSPEND_PARAM.equalsIgnoreCase(key))
_sendOnSuspend = Boolean.parseBoolean(value);
else
super.setParameter(key, value);
}
public boolean willSendOnSuspend()
{
return _sendOnSuspend;
}
public void setSendOnSuspend(boolean send)
{
_sendOnSuspend = send;
}
}