/*
* 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.shadow;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
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.runtime.controller.debug.BreakpointType;
import org.jactr.tools.async.common.NetworkedEndpoint;
import org.jactr.tools.async.message.command.breakpoint.BreakpointCommand;
import org.jactr.tools.async.message.command.breakpoint.IBreakpointCommand;
import org.jactr.tools.async.message.command.breakpoint.IProductionCommand;
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.IStateCommand;
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.ModelStateEvent;
import org.jactr.tools.async.message.event.state.RuntimeStateEvent;
import org.jactr.tools.async.shadow.handlers.BreakpointMessageHandler;
import org.jactr.tools.async.shadow.handlers.BulkMessageTransformer;
import org.jactr.tools.async.shadow.handlers.LoginMessageHandler;
import org.jactr.tools.async.shadow.handlers.LogoutMessageHandler;
import org.jactr.tools.async.shadow.handlers.ModelDataMessageHandler;
import org.jactr.tools.async.shadow.handlers.ModelStateHandler;
import org.jactr.tools.async.shadow.handlers.RuntimeStateMessageHandler;
/**
* a mock controller that is to be used to interface with the real one
* controller.NetworkedDebugController
*
* @author developer
*/
public class ShadowController extends NetworkedEndpoint
{
static public final String CONTROLLER_ATTR = "jactr.controller";
/**
* logger definition
*/
static private final Log LOGGER = LogFactory
.getLog(ShadowController.class);
private Set<String> _runningModels;
private Set<String> _terminatedModels;
private Set<String> _suspendedModels;
private Map<String, CommonTree> _modelDescriptors;
/*
* keyed on model descriptor with value being the break point data
*/
private Map<String, CommonTree> _breakpointData;
private ReentrantLock _lock = new ReentrantLock();
private Condition _state = _lock.newCondition();
/**
* keeps track of the number of state changes that have occured
*/
private volatile long _stateCounter;
/**
* what was the state counter when a request (start, resume, stop, suspend)
* was issued, so that we can escape the waits at the right time.
*/
private long _stateAtSuspend;
private long _stateAtResume;
private double _lastSimulationTime;
public ShadowController()
{
_modelDescriptors = new HashMap<String, CommonTree>();
_breakpointData = new HashMap<String, CommonTree>();
_runningModels = new TreeSet<String>();
_terminatedModels = new TreeSet<String>();
_suspendedModels = new TreeSet<String>();
}
@Override
protected void createDefaultHandlers()
{
super.createDefaultHandlers();
_defaultHandlers.put(LoginAcknowledgedMessage.class,
new LoginMessageHandler());
_defaultHandlers.put(LogoutCommand.class, new LogoutMessageHandler());
_defaultHandlers.put(RuntimeStateEvent.class,
new RuntimeStateMessageHandler());
_defaultHandlers.put(ModelStateEvent.class, new ModelStateHandler());
_defaultHandlers.put(ModelDataEvent.class, new ModelDataMessageHandler());
_defaultHandlers.put(BreakpointReachedEvent.class,
new BreakpointMessageHandler());
}
@SuppressWarnings("rawtypes")
@Override
protected void sessionOpened(ISessionInfo session)
{
try
{
/**
* horrible exception handler..
*/
session.addExceptionHandler((s, t) -> {
try
{
if (!(t instanceof IOException)
&& t.getMessage().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;
});
// add the bulk message handler.
session.addTransformer(new BulkMessageTransformer());
session.setAttribute(CONTROLLER_ATTR, this);
/*
* first thing's first, send out our credentials
*/
session.writeAndWait(new LoginCommand(getActualCredentials()));
super.sessionOpened(session);
}
catch (Exception e)
{
LOGGER.error("Failed to send login command ", e);
}
}
@Override
protected void sessionClosed(ISessionInfo session)
{
if (isRunning()) for (String modelName : getRunningModels())
stopped(modelName);
}
public Collection<String> getModelNames()
{
try
{
_lock.lock();
return new ArrayList<String>(_modelDescriptors.keySet());
}
finally
{
_lock.unlock();
}
}
public Collection<String> getRunningModels()
{
try
{
_lock.lock();
return new ArrayList<String>(_runningModels);
}
finally
{
_lock.unlock();
}
}
public Collection<String> getTerminatedModels()
{
try
{
_lock.lock();
return new ArrayList<String>(_terminatedModels);
}
finally
{
_lock.unlock();
}
}
public Collection<String> getSuspendedModels()
{
try
{
_lock.lock();
return new ArrayList<String>(_suspendedModels);
}
finally
{
_lock.unlock();
}
}
public CommonTree getModelDescriptor(String modelName)
{
try
{
_lock.lock();
return _modelDescriptors.get(modelName);
}
finally
{
_lock.unlock();
}
}
public void setModelDescriptor(String modelName, CommonTree modelDescriptor)
{
try
{
_lock.lock();
_modelDescriptors.put(modelName, modelDescriptor);
}
finally
{
_lock.unlock();
}
}
public CommonTree getBreakpointData(String modelName)
{
try
{
_lock.lock();
return _breakpointData.get(modelName);
}
finally
{
_lock.unlock();
}
}
public void setBreakpointData(String modelName, CommonTree breakpointData)
{
try
{
_lock.lock();
_breakpointData.put(modelName, breakpointData);
}
finally
{
_lock.unlock();
}
}
public boolean isConnected()
{
return getSession() != null;
}
protected void checkConnection()
{
if (!isConnected())
throw new IllegalStateException(
"Can only manipulate a remote controller if the connection is alive");
}
/**
* start up the service
*
* @see org.jactr.core.runtime.controller.IController#attach()
*/
public void attach()
{
try
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("Attaching " + this);
connect();
}
catch (Exception e)
{
throw new RuntimeException("Could not start service ", e);
}
}
public void detach(boolean force)
{
try
{
if (isSuspended()) resume();
if (isRunning()) stop();
ISessionInfo<?> session = getActiveSession();
// allow the other side to disconnect first
if (!force && session != null) session.waitForPendingWrites();
disconnect(force);
}
catch (Exception e)
{
throw new RuntimeException("Could not stop service ", e);
}
}
public double getCurrentSimulationTime()
{
return _lastSimulationTime;
}
public void setCurrentSimulationTime(double time)
{
_lastSimulationTime = time;
}
/**
* @see org.jactr.core.runtime.controller.IController#isRunning()
*/
public boolean isRunning()
{
try
{
_lock.lock();
return _runningModels.size() != 0;
}
finally
{
_lock.unlock();
}
}
public boolean isRunning(String modelName)
{
try
{
_lock.lock();
return _runningModels.contains(modelName);
}
finally
{
_lock.unlock();
}
}
/**
* @see org.jactr.core.runtime.controller.IController#isSuspended()
*/
public boolean isSuspended()
{
try
{
_lock.lock();
return _suspendedModels.containsAll(_runningModels) && isRunning();
}
finally
{
_lock.unlock();
}
}
public boolean isSuspended(String modelName)
{
try
{
_lock.lock();
return _suspendedModels.contains(modelName);
}
finally
{
_lock.unlock();
}
}
public void addBreakpoint(BreakpointType type, String modelName,
String details)
{
checkConnection();
try
{
getSession().writeAndWait(
new BreakpointCommand(IBreakpointCommand.Action.ADD, type, modelName,
details));
}
catch (Exception e)
{
LOGGER.error("Failed to send network command ", e);
}
}
public void removeBreakpoint(BreakpointType type, String modelName,
String details)
{
checkConnection();
try
{
getSession().writeAndWait(
new BreakpointCommand(IBreakpointCommand.Action.REMOVE, type,
modelName, details));
}
catch (Exception e)
{
LOGGER.error("Failed to send network command ", e);
}
}
public void clearBreakpoints()
{
checkConnection();
try
{
getSession().writeAndWait(
new BreakpointCommand(IBreakpointCommand.Action.CLEAR,
BreakpointType.ALL));
}
catch (Exception e)
{
LOGGER.error("Failed to send network command ", e);
}
}
public void setProductionEnabled(String modelName, String productionName,
boolean enabled)
{
checkConnection();
try
{
getSession().write(
new ProductionCommand(modelName, productionName,
enabled ? IProductionCommand.Action.ENABLE
: IProductionCommand.Action.DISABLE));
}
catch (Exception e)
{
LOGGER.error("Failed to send network command ", e);
}
}
/**
* @see org.jactr.core.runtime.controller.IController#reset()
*/
public void reset()
{
checkConnection();
if (isRunning())
{
// clear breakpoints
clearBreakpoints();
if (isSuspended()) resume();
try
{
waitForCompletion();
}
catch (InterruptedException ie)
{
LOGGER.error("Could not wait for completion ", ie);
}
}
try
{
_lock.lock();
_stateCounter = 0;
_modelDescriptors.clear();
_breakpointData.clear();
_terminatedModels.clear();
_suspendedModels.clear();
_runningModels.clear();
}
finally
{
_lock.unlock();
}
}
/**
* @see org.jactr.core.runtime.controller.IController#resume()
*/
public void resume()
{
checkConnection();
try
{
_lock.lock();
_stateAtResume = getStateCounter();
if (LOGGER.isDebugEnabled()) LOGGER.debug("Sending resume");
getSession().writeAndWait(
new RuntimeStateCommand(IStateCommand.State.RESUME));
}
catch (Exception e)
{
LOGGER.error("Failed to send resume command ", e);
}
finally
{
_lock.unlock();
}
}
public void resume(String modelName)
{
checkConnection();
try
{
_lock.lock();
_stateAtResume = getStateCounter();
if (LOGGER.isDebugEnabled())
LOGGER.debug("Sending resume to " + modelName);
getSession().write(
new ModelStateCommand(modelName, IStateCommand.State.RESUME));
}
catch (Exception e)
{
LOGGER.error("Failed to send resume command ", e);
}
finally
{
_lock.unlock();
}
}
/**
* @see org.jactr.core.runtime.controller.IController#start()
*/
public void start()
{
start(false);
}
/**
* @see org.jactr.core.runtime.controller.IController#start(boolean)
*/
public void start(boolean suspendImmediately)
{
checkConnection();
try
{
_lock.lock();
_stateAtResume = Long.MAX_VALUE;
_stateAtSuspend = Long.MAX_VALUE;
getSession().writeAndWait(new RuntimeStateCommand(suspendImmediately));
}
catch (Exception e)
{
LOGGER.error("Failed to send start command ", e);
}
finally
{
_lock.unlock();
}
}
/**
* @see org.jactr.core.runtime.controller.IController#stop()
*/
public void stop()
{
checkConnection();
try
{
_lock.lock();
_stateAtResume = _stateAtSuspend = getStateCounter();
getSession().writeAndWait(
new RuntimeStateCommand(IStateCommand.State.STOP));
}
catch (Exception e)
{
LOGGER.error("Failed to send stop command ", e);
}
finally
{
_lock.unlock();
}
}
public void stop(String modelName)
{
checkConnection();
try
{
_lock.lock();
_stateAtResume = _stateAtSuspend = getStateCounter();
getSession().write(
new ModelStateCommand(modelName, IStateCommand.State.STOP));
}
catch (Exception e)
{
LOGGER.error("Failed to send stop command ", e);
}
finally
{
_lock.unlock();
}
}
/**
* @see org.jactr.core.runtime.controller.IController#suspend()
*/
public void suspend()
{
checkConnection();
try
{
_lock.lock();
_stateAtSuspend = getStateCounter();
if (LOGGER.isDebugEnabled()) LOGGER.debug("Sending suspend");
getSession().writeAndWait(
new RuntimeStateCommand(IStateCommand.State.SUSPEND));
}
catch (Exception e)
{
LOGGER.error("Failed to send susension command ", e);
}
finally
{
_lock.unlock();
}
}
public void suspend(String modelName)
{
checkConnection();
try
{
_lock.lock();
_stateAtSuspend = getStateCounter();
if (LOGGER.isDebugEnabled())
LOGGER.debug("Sending suspend to " + modelName);
getSession().writeAndWait(
new ModelStateCommand(modelName, IStateCommand.State.SUSPEND));
}
catch (Exception e)
{
LOGGER.error("Failed to send susension command ", e);
}
finally
{
_lock.unlock();
}
}
public long waitForStateChange() throws InterruptedException
{
return waitForStateChange(0);
}
/**
* wait at most maxWait milliseconds for some change of state in the system.
* This is much safer than waitForResumption() and waitForSuspension() as they
* may miss events if the notifications are coming in faster than
* ShadowController can respond to them.
*
* @param maxWait
* @return the number of state changes that have elapsed since start of wait
* @throws InterruptedException
*/
public long waitForStateChange(long maxWait) throws InterruptedException
{
try
{
_lock.lock();
long startTime = System.currentTimeMillis();
long startState = getStateCounter();
while (startState == getStateCounter())
{
if (maxWait > 0 && System.currentTimeMillis() - startTime > maxWait)
return getStateCounter() - startState;
if (LOGGER.isDebugEnabled()) LOGGER.debug("Waiting for state change");
if (maxWait > 0)
_state.await(maxWait, TimeUnit.MILLISECONDS);
else
_state.await();
}
return getStateCounter() - startState;
}
finally
{
_lock.unlock();
}
}
/**
* should only be called from within the lock blocks
*
* @return
*/
protected long getStateCounter()
{
return _stateCounter;
}
public boolean waitForCompletion() throws InterruptedException
{
return waitForCompletion(0);
}
/**
* @return if the runtime is running
* @see org.jactr.core.runtime.controller.IController#waitForCompletion()
*/
public boolean waitForCompletion(long maxWait) throws InterruptedException
{
checkConnection();
try
{
_lock.lock();
long startTime = System.currentTimeMillis();
if (LOGGER.isDebugEnabled()) LOGGER.debug("Waiting for completion");
while (isRunning())
{
// time has elapsed
if (maxWait > 0 && System.currentTimeMillis() - startTime > maxWait)
return isRunning();
if (maxWait > 0)
_state.await(maxWait, TimeUnit.MILLISECONDS);
else
_state.await();
}
return isRunning();
}
finally
{
_lock.unlock();
}
}
/**
* called by the io handler only when the actual runtime has resumed.
*/
public void resumed(String modelName)
{
if (!isSuspended(modelName))
{
if (LOGGER.isDebugEnabled())
LOGGER.debug(modelName + " is currently resumed, ignoring");
return;
}
try
{
_lock.lock();
_breakpointData.clear();
_suspendedModels.remove(modelName);
_stateCounter++;
if (LOGGER.isDebugEnabled()) LOGGER.debug("Resumed, signalling");
_state.signalAll();
}
finally
{
_lock.unlock();
}
}
/**
* @see #waitForResumption(long)
* @return
* @throws InterruptedException
*/
public boolean waitForResumption() throws InterruptedException
{
return waitForResumption(0);
}
/**
* Will wait until one of three things happens: the state is now resumed, the
* state has changed otherwise, or maxWait milliseconds have elapsed
*
* @param maxWait
* milliseconds to block, 0 is indefinite
* @return if the runtime has resumed, if false, you should explicitly check
* the state
* @see #waitForStateChange(long)
*/
public boolean waitForResumption(long maxWait) throws InterruptedException
{
checkConnection();
try
{
_lock.lock();
/*
* if the state has changed from the time resume() was called
*/
if (getStateCounter() - 1 > _stateAtResume)
{
if (LOGGER.isDebugEnabled())
LOGGER
.debug("State has already changed since resume was called, returning false");
return false;
}
long delta = -1;
while (isSuspended() && delta < 0)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("waiting for resumption");
// delta 0 means time has elapsed w/ no change
delta = waitForStateChange(maxWait);
}
if (delta <= 1) return !isSuspended();
return false;
}
finally
{
_lock.unlock();
}
}
/**
* called by the io handler only to signal that the runtime (where ever it is)
* has been suspended.
*/
public void suspended(String modelName)
{
if (isSuspended(modelName))
{
if (LOGGER.isDebugEnabled())
LOGGER.debug(modelName + " already suspended, ignoring");
return;
}
try
{
_lock.lock();
_suspendedModels.add(modelName);
_stateCounter++;
if (LOGGER.isDebugEnabled()) LOGGER.debug("Suspended, signalling");
_state.signalAll();
}
finally
{
_lock.unlock();
}
}
/**
* @see #waitForSuspension(long)
* @throws InterruptedException
*/
public boolean waitForSuspension() throws InterruptedException
{
return waitForSuspension(0);
}
/**
* Will wait until one of three things happens: the state is now suspended,
* the state has changed otherwise, or maxWait milliseconds have elapsed
*
* @param maxWait
* milliseconds to block, 0 is indefinite
* @return if the runtime has suspended, if false, you should explicitly check
* the state
* @see #waitForStateChange(long)
*/
public boolean waitForSuspension(long maxWait) throws InterruptedException
{
checkConnection();
if (!isRunning()) return false;
try
{
_lock.lock();
if (getStateCounter() - 1 > _stateAtSuspend)
{
if (LOGGER.isDebugEnabled())
LOGGER
.debug("State has already changed since suspend was called, returning false");
return false;
}
long delta = -1;
while (!isSuspended() && delta < 0)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("waiting for suspension");
// delta 0 means time has elapsed w/ no change
delta = waitForStateChange(maxWait);
}
if (delta <= 1) return isSuspended();
return false;
}
finally
{
_lock.unlock();
}
}
/**
* @see org.jactr.core.runtime.controller.IController#waitForStart()
*/
public boolean waitForStart() throws InterruptedException
{
return waitForStart(0);
}
/**
* Will wait until one of three things happens: the state is now running, the
* state has changed otherwise, or maxWait milliseconds have elapsed
*
* @param maxWait
* milliseconds to block, 0 is indefinite
* @return if the runtime has started, if false, you should explicitly check
* the state
* @see #waitForStateChange(long)
*/
public boolean waitForStart(long maxWait) throws InterruptedException
{
checkConnection();
try
{
_lock.lock();
long delta = -1;
while (!isRunning() && delta < 0)
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("Waiting for start");
// delta 0 means that the time elapsed but there was no change
delta = waitForStateChange(maxWait);
}
if (delta <= 1) return isRunning();
return false;
}
finally
{
_lock.unlock();
}
}
/**
* called by the io handler only when the runtime has actually started.
*/
public void started(Collection<String> modelNames)
{
if (isRunning())
{
if (LOGGER.isDebugEnabled()) LOGGER.debug("already started, ignoring");
return;
}
try
{
_lock.lock();
_modelDescriptors.clear();
for (String modelName : modelNames)
setModelDescriptor(modelName, null);
_runningModels.addAll(modelNames);
_suspendedModels.clear();
_terminatedModels.clear();
_breakpointData.clear();
_stateCounter++;
if (LOGGER.isDebugEnabled()) LOGGER.debug("Started, signalling");
_state.signalAll();
}
finally
{
_lock.unlock();
}
}
/**
* called by the io handler only when the runtime has stopped.
*/
public void stopped(String modelName)
{
if (!isRunning(modelName))
{
if (LOGGER.isDebugEnabled())
LOGGER.debug(modelName + " already stopped, ignoring");
return;
}
try
{
_lock.lock();
_terminatedModels.add(modelName);
_runningModels.remove(modelName);
_suspendedModels.remove(modelName);
_stateCounter++;
if (LOGGER.isDebugEnabled())
LOGGER.debug("Stopped, signalling runningModels:" + _runningModels);
_state.signalAll();
}
finally
{
_lock.unlock();
}
}
}