Bug 550051: Engine termination should be improved

  reworked termination process
  on user requested termination we now set an exception as execution
  result
  interface cleanup

Change-Id: I9fed889b754f92420820c4b50383485b22f364ec
diff --git a/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/ObservingContextFactory.java b/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/ObservingContextFactory.java
index 6d71ec3..82d72fd 100644
--- a/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/ObservingContextFactory.java
+++ b/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/ObservingContextFactory.java
@@ -13,18 +13,18 @@
 import java.util.HashSet;
 import java.util.Set;
 
-import org.eclipse.ease.ExitException;
+import org.eclipse.ease.ScriptExecutionException;
 import org.mozilla.javascript.Context;
 import org.mozilla.javascript.ContextFactory;
 
 public class ObservingContextFactory extends ContextFactory {
 
-	private final Set<Context> mTerminationRequests = new HashSet<Context>();
+	private final Set<Context> mTerminationRequests = new HashSet<>();
 
 	@Override
 	protected synchronized void observeInstructionCount(final Context cx, final int instructionCount) {
 		if (mTerminationRequests.remove(cx))
-			throw new ExitException();
+			throw new ScriptExecutionException("Engine got terminated");
 
 		super.observeInstructionCount(cx, instructionCount);
 	}
diff --git a/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/RhinoScriptEngine.java b/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/RhinoScriptEngine.java
index df40762..afe0905 100644
--- a/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/RhinoScriptEngine.java
+++ b/plugins/org.eclipse.ease.lang.javascript.rhino/src/org/eclipse/ease/lang/javascript/rhino/RhinoScriptEngine.java
@@ -289,8 +289,11 @@
 
 	@Override
 	public void terminateCurrent() {
-		// typically requested by a different thread, so do not use getContext() here
-		((ObservingContextFactory) ContextFactory.getGlobal()).terminate(fContext);
+		if (Thread.currentThread().equals(getThread()))
+			throw new ScriptExecutionException("Script got terminated");
+		else
+			// requested by a different thread, so do not use getContext() here
+			((ObservingContextFactory) ContextFactory.getGlobal()).terminate(fContext);
 	}
 
 	@Override
diff --git a/plugins/org.eclipse.ease.ui.scripts/src/org/eclipse/ease/ui/scripts/keywordhandler/ShutdownHandler.java b/plugins/org.eclipse.ease.ui.scripts/src/org/eclipse/ease/ui/scripts/keywordhandler/ShutdownHandler.java
index d04acb2..77f79a1 100644
--- a/plugins/org.eclipse.ease.ui.scripts/src/org/eclipse/ease/ui/scripts/keywordhandler/ShutdownHandler.java
+++ b/plugins/org.eclipse.ease.ui.scripts/src/org/eclipse/ease/ui/scripts/keywordhandler/ShutdownHandler.java
@@ -40,30 +40,27 @@
 		@Override
 		protected IStatus run(final IProgressMonitor monitor) {
 			// wait for engines to be completed
-			for (IScriptEngine engine : fEngines) {
-				long timeout = (fStartTime + fShutdownTimeout) - System.currentTimeMillis();
+			for (final IScriptEngine engine : fEngines) {
+				final long timeout = (fStartTime + fShutdownTimeout) - System.currentTimeMillis();
 				if (timeout > 0) {
 					try {
-						engine.join(timeout);
-					} catch (InterruptedException e) {
+						engine.joinEngine(timeout);
+					} catch (final InterruptedException e) {
 					}
 				} else
 					break;
 			}
 
 			// terminate engines that are not completed
-			for (IScriptEngine engine : fEngines) {
+			for (final IScriptEngine engine : fEngines) {
 				if (!engine.isFinished())
 					engine.terminate();
 			}
 
 			// call final shutdown
-			Display.getDefault().asyncExec(new Runnable() {
-				@Override
-				public void run() {
-					fShutdownScripts.clear();
-					PlatformUI.getWorkbench().close();
-				}
+			Display.getDefault().asyncExec(() -> {
+				fShutdownScripts.clear();
+				PlatformUI.getWorkbench().close();
 			});
 
 			return Status.OK_STATUS;
@@ -71,7 +68,7 @@
 	}
 
 	/** Registered shutdown scripts. */
-	Collection<IScript> fShutdownScripts = new HashSet<IScript>();
+	Collection<IScript> fShutdownScripts = new HashSet<>();
 
 	/** Default script timeout: 10s. */
 	private long fShutdownTimeout = 10 * 1000;
@@ -85,7 +82,7 @@
 	@Override
 	public void handleEvent(final Event event) {
 		final IScript script = (IScript) event.getProperty("script");
-		String value = (String) event.getProperty("value");
+		final String value = (String) event.getProperty("value");
 
 		if (value == null)
 			fShutdownScripts.remove(script);
@@ -95,7 +92,7 @@
 			if (!value.isEmpty()) {
 				try {
 					fShutdownTimeout = Math.max(fShutdownTimeout, Integer.parseInt(value) * 1000);
-				} catch (NumberFormatException e) {
+				} catch (final NumberFormatException e) {
 					Logger.error(Activator.PLUGIN_ID, "Invalid onShutdown timeout for script: " + script.getLocation());
 				}
 			}
@@ -111,8 +108,8 @@
 	public boolean preShutdown(final IWorkbench workbench, final boolean forced) {
 		if ((!forced) && (!fShutdownScripts.isEmpty())) {
 			fStartTime = System.currentTimeMillis();
-			fEngines = new HashSet<IScriptEngine>();
-			for (IScript script : fShutdownScripts)
+			fEngines = new HashSet<>();
+			for (final IScript script : fShutdownScripts)
 				fEngines.add(script.run());
 
 			new ShutdownJob().schedule();
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractReplScriptEngine.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractReplScriptEngine.java
index de78362..5b4bbec 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractReplScriptEngine.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractReplScriptEngine.java
@@ -61,24 +61,12 @@
 		return fTerminateOnIdle;
 	}
 
-	/**
-	 * Get termination status of the interpreter. A terminated interpreter cannot be restarted.
-	 *
-	 * @return true if interpreter is terminated.
-	 */
 	@Override
-	protected boolean isTerminated() {
-		return fTerminateOnIdle && isIdle();
-	}
+	protected boolean shallTerminate() {
+		if (getTerminateOnIdle())
+			return super.shallTerminate();
 
-	/**
-	 * Get idle status of the interpreter. The interpreter is IDLE if there are no pending execution requests and the interpreter is not terminated.
-	 *
-	 * @return true if interpreter is IDLE
-	 */
-	@Override
-	public boolean isIdle() {
-		return super.isTerminated();
+		return getMonitor().isCanceled();
 	}
 
 	@Override
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractScriptEngine.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractScriptEngine.java
index 4c6f1b1..2322217 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractScriptEngine.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/AbstractScriptEngine.java
@@ -24,8 +24,10 @@
 
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IProgressMonitorWithBlocking;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.debug.core.ILaunch;
@@ -36,6 +38,7 @@
 import org.eclipse.ease.security.ScriptUIAccess;
 import org.eclipse.ease.service.EngineDescription;
 import org.eclipse.ease.tools.ResourceTools;
+import org.eclipse.ui.internal.progress.ProgressManager.JobMonitor;
 
 /**
  * Base implementation for a script engine. Handles Job implementation of script engine, adding script code for execution, module loading support and a basic
@@ -44,37 +47,6 @@
 public abstract class AbstractScriptEngine extends Job implements IScriptEngine {
 
 	/**
-	 * Watches over the script execution job and forwards termination requests from the Progress UI.
-	 */
-	private class ScriptTerminator extends Job {
-
-		private final IProgressMonitor fMonitor;
-
-		/**
-		 * @param monitor
-		 *            monitor from the script engine job
-		 */
-		public ScriptTerminator(IProgressMonitor monitor) {
-			super("ScriptEngine termination guard");
-			fMonitor = monitor;
-
-			setSystem(true);
-		}
-
-		@Override
-		protected IStatus run(IProgressMonitor monitor) {
-			if (fMonitor.isCanceled())
-				terminate();
-
-			else
-				// check again in a second
-				schedule(1000);
-
-			return Status.OK_STATUS;
-		}
-	}
-
-	/**
 	 * Get the current script engine. Works only if executed from the script engine thread.
 	 *
 	 * @return script engine or <code>null</code>
@@ -86,6 +58,23 @@
 		return null;
 	}
 
+	/**
+	 * Get the beautified name of a file to be set as part of the job title.
+	 *
+	 * @param file
+	 *            executed file
+	 * @return beautified name or <code>null</code>
+	 */
+	private static String getFilename(Object file) {
+		if (file instanceof IFile) {
+			return ResourceTools.toAbsoluteLocation(file, null);
+		} else if (file instanceof File) {
+			return ResourceTools.toAbsoluteLocation(file, null);
+		} else {
+			return null;
+		}
+	}
+
 	/** List of code junks to be executed. */
 	private final List<Script> fScheduledScripts = Collections.synchronizedList(new ArrayList<Script>());
 
@@ -108,8 +97,6 @@
 
 	private boolean fCloseStreamsOnTerminate;
 
-	private boolean fTerminated = false;
-
 	/** Registered security checks for engine actions. */
 	private final HashMap<ActionType, List<ISecurityCheck>> fSecurityChecks = new HashMap<>();
 
@@ -198,16 +185,6 @@
 		return result.getResult();
 	}
 
-	private static String getFilename(Object file) {
-		if (file instanceof IFile) {
-			return ResourceTools.toAbsoluteLocation(file, null);
-		} else if (file instanceof File) {
-			return ResourceTools.toAbsoluteLocation(file, null);
-		} else {
-			return null;
-		}
-	}
-
 	/**
 	 * Inject script code to the script engine. Injected code is processed synchronous by the current thread unless <i>uiThread</i> is set to <code>true</code>.
 	 * Nevertheless this is a blocking call.
@@ -234,7 +211,7 @@
 				if (securityChecks != null) {
 					for (final ISecurityCheck check : securityChecks) {
 						if (!check.doIt(ActionType.INJECT_CODE, script, uiThread))
-							throw new ExitException();
+							throw new ScriptEngineException("Security check failed: " + check.toString());
 					}
 				}
 
@@ -246,9 +223,6 @@
 
 				script.setResult(execute(script, script.getFile(), fStackTrace.get(0).getName(), uiThread));
 
-			} catch (final ExitException e) {
-				script.setResult(e.getCondition());
-
 			} catch (final BreakException e) {
 				script.setResult(e.getCondition());
 
@@ -274,9 +248,6 @@
 		return script.getResult();
 	}
 
-	/**
-	 * @param filename
-	 */
 	private void updateJobName(String filename) {
 		if (filename != null) {
 			String baseName = getName();
@@ -291,8 +262,40 @@
 	protected IStatus run(final IProgressMonitor monitor) {
 		fMonitor = monitor;
 
+		addStopButtonMonitor();
+
+		IStatus returnStatus = setupRun();
+		if (Status.OK_STATUS.equals(returnStatus)) {
+			// main loop
+			while (!shallTerminate()) {
+
+				// execute code
+				if (!fScheduledScripts.isEmpty()) {
+					final Script piece = fScheduledScripts.remove(0);
+					final ScriptResult scriptResult = inject(piece, true, false);
+					if (scriptResult.hasException())
+						returnStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Script execution failed", scriptResult.getException());
+
+				} else {
+					synchronized (this) {
+						try {
+							Logger.trace(Activator.PLUGIN_ID, TRACE_SCRIPT_ENGINE, "Engine idle: " + getName());
+							wait();
+						} catch (final InterruptedException e) {
+						}
+					}
+				}
+			}
+		}
+
+		if ((Status.OK_STATUS.equals(returnStatus)) && (getMonitor().isCanceled()))
+			returnStatus = Status.CANCEL_STATUS;
+
+		return cleanupRun(returnStatus);
+	}
+
+	private IStatus setupRun() {
 		Logger.trace(Activator.PLUGIN_ID, TRACE_SCRIPT_ENGINE, "Engine started: " + getName());
-		IStatus returnStatus = Status.OK_STATUS;
 
 		addSecurityCheck(ActionType.INJECT_CODE, ScriptUIAccess.getInstance());
 
@@ -300,9 +303,6 @@
 			setupEngine();
 			fSetupDone = true;
 
-			if (!isSystem())
-				new ScriptTerminator(monitor).schedule();
-
 			// engine is initialized, set buffered variables
 			for (final Entry<String, Object> entry : fBufferedVariables.entrySet()) {
 				setVariable(entry.getKey(), entry.getValue());
@@ -315,78 +315,121 @@
 
 			notifyExecutionListeners(null, IExecutionListener.ENGINE_START);
 
-			// main loop
-			while ((!monitor.isCanceled()) && (!isTerminated())) {
-
-				// execute code
-				if (!fScheduledScripts.isEmpty()) {
-					final Script piece = fScheduledScripts.remove(0);
-					inject(piece, true, false);
-
-				} else {
-
-					synchronized (this) {
-						if (!isTerminated()) {
-							try {
-								Logger.trace(Activator.PLUGIN_ID, TRACE_SCRIPT_ENGINE, "Engine idle: " + getName());
-								wait();
-							} catch (final InterruptedException e) {
-							}
-						}
-					}
-				}
-			}
-
-			returnStatus = (!isTerminated()) ? Status.OK_STATUS : Status.CANCEL_STATUS;
-
 		} catch (final ScriptEngineException e) {
-			returnStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Could not setup script engine", e);
-
-		} finally {
-			// discard pending code pieces
-			synchronized (fScheduledScripts) {
-				for (final Script script : fScheduledScripts)
-					script.setException(new ExitException());
-			}
-
-			fScheduledScripts.clear();
-
-			notifyExecutionListeners(null, IExecutionListener.ENGINE_END);
-
-			try {
-				teardownEngine();
-			} catch (final ScriptEngineException e) {
-				if (returnStatus.getSeverity() < IStatus.ERROR) {
-					// We were almost all OK (or just warnings/infos) but then we failed at shutdown
-					// Note we don't override a CANCEL
-					returnStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Could not teardown script engine", e);
-				}
-			} finally {
-				fTerminated = true;
-				synchronized (this) {
-					notifyAll();
-				}
-
-				// discard pending code pieces
-				synchronized (fScheduledScripts) {
-					for (final Script script : fScheduledScripts)
-						script.setException(new ExitException());
-
-					fScheduledScripts.clear();
-				}
-
-				closeStreams();
-
-				Logger.trace(Activator.PLUGIN_ID, TRACE_SCRIPT_ENGINE, "Engine terminated: " + getName());
-			}
+			return new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Could not setup script engine", e);
 		}
 
-		monitor.done();
-		fMonitor = null;
+		return Status.OK_STATUS;
+	}
+
+	private IStatus cleanupRun(IStatus returnStatus) {
+
+		// discard pending code pieces
+		synchronized (fScheduledScripts) {
+			for (final Script script : fScheduledScripts)
+				script.setException(new ScriptExecutionException("Engine got terminated"));
+		}
+
+		fScheduledScripts.clear();
+
+		notifyExecutionListeners(null, IExecutionListener.ENGINE_END);
+
+		try {
+			teardownEngine();
+		} catch (final ScriptEngineException e) {
+			if (returnStatus.getSeverity() < IStatus.ERROR) {
+				// We were almost all OK (or just warnings/infos) but then we failed at shutdown
+				// Note we don't override a CANCEL
+				returnStatus = new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Could not teardown script engine", e);
+			}
+		} finally {
+			synchronized (this) {
+				notifyAll();
+			}
+
+			closeStreams();
+
+			Logger.trace(Activator.PLUGIN_ID, TRACE_SCRIPT_ENGINE, "Engine terminated: " + getName());
+
+			fMonitor.done();
+			fMonitor = null;
+		}
 
 		return returnStatus;
 	}
 
+	/**
+	 * Add monitor to detect clicks on the stop button in the Progress view.
+	 */
+	private void addStopButtonMonitor() {
+		if (fMonitor instanceof JobMonitor)
+			((JobMonitor) fMonitor).addProgressListener(new ScriptEngineMonitor());
+	}
+
+	/**
+	 * Evaluate if the engine shall terminate.
+	 *
+	 * @return <code>true</code> when termination is requested or there is no more work to be done
+	 */
+	protected boolean shallTerminate() {
+		return getMonitor().isCanceled() || fScheduledScripts.isEmpty();
+	}
+
+	@Override
+	public void terminate() {
+
+		final IProgressMonitor monitor = getMonitor();
+		if ((monitor != null) && (!monitor.isCanceled()))
+			monitor.setCanceled(true);
+
+		terminateCurrent();
+
+		synchronized (this) {
+			notify();
+		}
+	}
+
+	/**
+	 * Check engine for cancellation request and terminate if indicated by the monitor.
+	 */
+	public void checkForCancellation() {
+		final IProgressMonitor monitor = getMonitor();
+		if ((monitor != null) && (monitor.isCanceled())) {
+			if (Thread.currentThread().equals(getThread()))
+				throw new ScriptExecutionException("Engine got terminated");
+		}
+	}
+
+	@Override
+	public boolean isFinished() {
+		// setup was done, hence we were started
+		return (Job.NONE == getState()) && fSetupDone;
+	}
+
+	@Override
+	public void joinEngine() throws InterruptedException {
+		if (!Thread.currentThread().equals(getThread())) {
+			// we cannot join our own thread
+
+			synchronized (this) {
+				while (!isFinished())
+					wait(1000);
+			}
+		}
+	}
+
+	@Override
+	public void joinEngine(final long timeout) throws InterruptedException {
+		if (!Thread.currentThread().equals(getThread())) {
+			// we cannot join our own thread
+
+			synchronized (this) {
+				if (!isFinished())
+					wait(timeout);
+			}
+		}
+	}
+
 	@Override
 	public IProgressMonitor getMonitor() {
 		return fMonitor;
@@ -464,15 +507,6 @@
 			fErrorStream = null;
 	}
 
-	/**
-	 * Get termination status of the interpreter. A terminated interpreter cannot be restarted.
-	 *
-	 * @return true if interpreter is terminated.
-	 */
-	protected boolean isTerminated() {
-		return fScheduledScripts.isEmpty();
-	}
-
 	@Override
 	public void addExecutionListener(final IExecutionListener listener) {
 		fExecutionListeners.add(listener);
@@ -488,20 +522,6 @@
 			((IExecutionListener) listener).notify(this, script, status);
 	}
 
-	@Override
-	public void terminate() {
-		fScheduledScripts.clear();
-		terminateCurrent();
-
-		// ask thread to terminate
-		cancel();
-
-		// see bug 512607
-		final Thread thread = getThread();
-		if (thread != null)
-			thread.interrupt();
-	}
-
 	public ScriptStackTrace getStackTrace() {
 		return fStackTrace;
 	}
@@ -584,19 +604,6 @@
 	}
 
 	@Override
-	public boolean isFinished() {
-		return fTerminated;
-	}
-
-	@Override
-	public void join(final long timeout) throws InterruptedException {
-		synchronized (this) {
-			if (!isFinished())
-				wait(timeout);
-		}
-	}
-
-	@Override
 	public void addSecurityCheck(ActionType type, ISecurityCheck check) {
 		if (!fSecurityChecks.containsKey(type))
 			fSecurityChecks.put(type, new ArrayList<ISecurityCheck>());
@@ -672,4 +679,28 @@
 	 *             any exception thrown during script execution
 	 */
 	protected abstract Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable;
+
+	/**
+	 * Simple monitor to forward cancellation requests to the script engine.
+	 */
+	private class ScriptEngineMonitor extends NullProgressMonitor implements IProgressMonitorWithBlocking {
+
+		@Override
+		public void setCanceled(boolean cancelled) {
+			super.setCanceled(cancelled);
+
+			if (isCanceled())
+				terminate();
+		}
+
+		@Override
+		public void setBlocked(IStatus reason) {
+			// nothing to do
+		}
+
+		@Override
+		public void clearBlocked() {
+			// nothing to do
+		}
+	}
 }
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/IReplEngine.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/IReplEngine.java
index f6e16e5..9feb8e8 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/IReplEngine.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/IReplEngine.java
@@ -17,14 +17,6 @@
 public interface IReplEngine extends IScriptEngine {
 
 	/**
-	 * Returns the execution state of the engine. If the engine is processing code or is terminated this will return <code>false</code>. If the engine is
-	 * waiting for further scripts to execute this will return <code>true</code>.
-	 *
-	 * @return execution state.
-	 */
-	boolean isIdle();
-
-	/**
 	 * Set a marker that the interpreter should terminate instead entering IDLE mode. If set, the interpreter will execute all pending requests and terminate
 	 * afterwards.
 	 *
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/IScriptEngine.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/IScriptEngine.java
index 83a6c08..7119c43 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/IScriptEngine.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/IScriptEngine.java
@@ -200,14 +200,22 @@
 	void registerJar(final URL url);
 
 	/**
-	 * Join engine execution thread. Waits for engine execution up to <i>timeout</i>
+	 * Join engine execution thread. Waits for engine execution up to <i>timeout</i> milliseconds.
 	 *
 	 * @param timeout
 	 *            command timeout in milliseconds
 	 * @throws InterruptedException
 	 *             when join command got interrupted
 	 */
-	void join(long timeout) throws InterruptedException;
+	void joinEngine(long timeout) throws InterruptedException;
+
+	/**
+	 * Join engine execution thread. Waits for engine termination.
+	 *
+	 * @throws InterruptedException
+	 *             when join command got interrupted
+	 */
+	void joinEngine() throws InterruptedException;
 
 	/**
 	 * Verify that engine was started and terminated.
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/ScriptExecutionException.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/ScriptExecutionException.java
index 4a7961f..66b925b 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/ScriptExecutionException.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/ScriptExecutionException.java
@@ -44,6 +44,15 @@
 		fErrorName = null;
 	}
 
+	public ScriptExecutionException(String message) {
+		super(message);
+
+		fLineSource = null;
+		fColumnNumber = 0;
+		fScriptStackTrace = null;
+		fErrorName = null;
+	}
+
 	/**
 	 * Instantiate wrapper exception.
 	 *
diff --git a/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/EnvironmentModule.java b/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/EnvironmentModule.java
index da59ae0..a55add1 100644
--- a/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/EnvironmentModule.java
+++ b/plugins/org.eclipse.ease/src/org/eclipse/ease/modules/EnvironmentModule.java
@@ -555,8 +555,21 @@
 		fModuleCallbacks.add(callbackProvider);
 	}
 
-	// needed by dynamic script code
+	//
+	/**
+	 * Check if java callbacks are registered for a module method. This method get called on each module function invokation.
+	 * <p>
+	 * ATTENTION: needed by dynamic script code, do not alter synopsis!
+	 * </p>
+	 *
+	 * @param methodToken
+	 *            unique method token
+	 * @return <code>true</code> when callbacks are registered
+	 */
 	public boolean hasMethodCallback(String methodToken) {
+		if (getScriptEngine() instanceof AbstractScriptEngine)
+			((AbstractScriptEngine) getScriptEngine()).checkForCancellation();
+
 		final Method method = fRegisteredMethods.get(methodToken);
 
 		for (final IModuleCallbackProvider callbackProvider : fModuleCallbacks) {
diff --git a/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractReplScriptEngineTest.java b/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractReplScriptEngineTest.java
index 3ebd914..29e2b0d 100644
--- a/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractReplScriptEngineTest.java
+++ b/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractReplScriptEngineTest.java
@@ -1,9 +1,6 @@
 package org.eclipse.ease;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import java.net.URL;
@@ -16,173 +13,109 @@
 import org.mockito.runners.MockitoJUnitRunner;
 
 @RunWith(MockitoJUnitRunner.class)
-public class AbstractReplScriptEngineTest {
+public class AbstractReplScriptEngineTest extends AbstractScriptEngineTest {
 
 	private static final String SAMPLE_CODE = "Hello world";
 
-	private AbstractReplScriptEngine fTestEngine;
+	private class MockedScriptEngine extends AbstractReplScriptEngine {
 
+		public MockedScriptEngine() {
+			super("Mocked Engine");
+		}
+
+		@Override
+		public void terminateCurrent() {
+		}
+
+		@Override
+		public void registerJar(URL url) {
+		}
+
+		@Override
+		protected Object internalGetVariable(String name) {
+			return null;
+		}
+
+		@Override
+		protected Map<String, Object> internalGetVariables() {
+			return null;
+		}
+
+		@Override
+		protected boolean internalHasVariable(String name) {
+			return false;
+		}
+
+		@Override
+		protected void internalSetVariable(String name, Object content) {
+		}
+
+		@Override
+		protected void setupEngine() throws ScriptEngineException {
+		}
+
+		@Override
+		protected Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable {
+			final String input = script.getCommand().toString();
+			if (input.contains(ERROR_MARKER))
+				throw new RuntimeException(input);
+			else
+				getOutputStream().write(input.getBytes());
+
+			return input;
+		}
+	}
+
+	@Override
 	@Before
 	public void setup() {
-		fTestEngine = new AbstractReplScriptEngine("Test engine") {
-
-			@Override
-			public void terminateCurrent() {
-			}
-
-			@Override
-			public void registerJar(final URL url) {
-			}
-
-			@Override
-			protected void teardownEngine() throws ScriptEngineException {
-			}
-
-			@Override
-			protected void setupEngine() throws ScriptEngineException {
-			}
-
-			@Override
-			protected void internalSetVariable(final String name, final Object content) {
-			}
-
-			@Override
-			protected boolean internalHasVariable(final String name) {
-				return false;
-			}
-
-			@Override
-			protected Map<String, Object> internalGetVariables() {
-				return null;
-			}
-
-			@Override
-			protected Object internalGetVariable(final String name) {
-				return null;
-			}
-
-			@Override
-			protected Object execute(final Script script, final Object reference, final String fileName, final boolean uiThread) throws Exception {
-				return script.getCommand();
-			}
-		};
+		fTestEngine = new MockedScriptEngine();
 	}
 
-	@Test
-	public void isJob() {
-		assertTrue(fTestEngine instanceof Job);
+	private AbstractReplScriptEngine getTestEngine() {
+		return (AbstractReplScriptEngine) fTestEngine;
 	}
 
-	@Test(timeout = 1000)
-	public void executeAsync() {
-
-		final ScriptResult result = fTestEngine.executeAsync(SAMPLE_CODE);
-		assertFalse(result.isReady());
-
-		fTestEngine.schedule();
-
-		synchronized (result) {
-			try {
-				while (!result.isReady())
-					result.wait();
-			} catch (final InterruptedException e) {
-			}
-		}
-		assertTrue(result.isReady());
-	}
-
-	@Test(timeout = 1000)
-	public void executeSync() throws InterruptedException {
-
-		fTestEngine.setTerminateOnIdle(false);
-		fTestEngine.schedule();
-
-		final ScriptResult result = fTestEngine.executeSync(SAMPLE_CODE);
-		assertTrue(result.isReady());
-
-		fTestEngine.terminate();
-	}
-
-	@Test(timeout = 1000)
-	public void inject() throws InterruptedException {
-		assertEquals(SAMPLE_CODE, fTestEngine.inject(SAMPLE_CODE));
-	}
-
-	@Test
-	public void streamsAvailable() {
-		assertNotNull(fTestEngine.getOutputStream());
-		assertNotNull(fTestEngine.getErrorStream());
-		assertNotNull(fTestEngine.getInputStream());
-	}
-
-	@Test(timeout = 1000)
-	public void terminateOnIdle() throws InterruptedException {
-		fTestEngine.setTerminateOnIdle(true);
-		fTestEngine.schedule();
-		fTestEngine.join();
-
-		// test valid if it terminates within the timeout period
-	}
-
-	@Test(timeout = 1000)
+	@Test(timeout = TEST_TIMEOUT)
 	public void terminateOnIdleAfterSchedule() throws InterruptedException {
-		fTestEngine.setTerminateOnIdle(false);
-		fTestEngine.schedule();
+		getTestEngine().setTerminateOnIdle(false);
+		getTestEngine().schedule();
 		while (true) {
 			Thread.sleep(10);
-			if (fTestEngine.getState() != Job.RUNNING) {
+			if (getTestEngine().getState() != Job.RUNNING) {
 				// eclipse job has not started yet
 				continue;
 			}
-			if (!fTestEngine.isIdle()) {
-				// ease still has work to do in the job
-				continue;
-			}
-			if (fTestEngine.getThread().getState() != Thread.State.WAITING) {
+			if (getTestEngine().getThread().getState() != Thread.State.WAITING) {
 				// thread is still running, we want it to be waiting
 				continue;
 			}
 			break;
 		}
-		fTestEngine.setTerminateOnIdle(true);
-		fTestEngine.join();
+		getTestEngine().setTerminateOnIdle(true);
+		getTestEngine().joinEngine();
 
 		// test valid if it terminates within the timeout period
 	}
 
-	@Test
+	@Test(timeout = TEST_TIMEOUT)
 	public void keepRunningOnIdle() throws InterruptedException {
-		fTestEngine.setTerminateOnIdle(false);
-		fTestEngine.executeAsync(SAMPLE_CODE);
-		fTestEngine.schedule();
+		getTestEngine().setTerminateOnIdle(false);
+		getTestEngine().executeAsync(SAMPLE_CODE);
+		getTestEngine().schedule();
 
-		final ScriptResult result = fTestEngine.executeSync(SAMPLE_CODE);
+		final ScriptResult result = getTestEngine().executeSync(SAMPLE_CODE);
 		assertTrue(result.isReady());
 	}
 
-	@Test
+	@Test(timeout = TEST_TIMEOUT)
 	public void terminateEngine() throws InterruptedException {
-		fTestEngine.setTerminateOnIdle(false);
-		fTestEngine.schedule();
+		getTestEngine().setTerminateOnIdle(false);
+		getTestEngine().schedule();
 
-		fTestEngine.terminate();
-		fTestEngine.join(5000);
+		getTestEngine().terminate();
+		getTestEngine().joinEngine();
 
-		assertEquals(Job.NONE, fTestEngine.getState());
-	}
-
-	@Test
-	public void extractEmptyArguments() {
-		assertEquals(0, AbstractScriptEngine.extractArguments(null).length);
-		assertEquals(0, AbstractScriptEngine.extractArguments("").length);
-		assertEquals(0, AbstractScriptEngine.extractArguments("    ").length);
-		assertEquals(0, AbstractScriptEngine.extractArguments("\t\t").length);
-	}
-
-	@Test
-	public void extractArguments() {
-		assertArrayEquals(new String[] { "one" }, AbstractScriptEngine.extractArguments("one"));
-		assertArrayEquals(new String[] { "one with spaces" }, AbstractScriptEngine.extractArguments("one with spaces"));
-		assertArrayEquals(new String[] { "one", "and", "another" }, AbstractScriptEngine.extractArguments("one,and, another"));
+		assertEquals(Job.NONE, getTestEngine().getState());
 	}
 }
diff --git a/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractScriptEngineTest.java b/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractScriptEngineTest.java
index 2e43bb6..40ec096 100644
--- a/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractScriptEngineTest.java
+++ b/tests/org.eclipse.ease.test/src/org/eclipse/ease/AbstractScriptEngineTest.java
@@ -1,57 +1,358 @@
 package org.eclipse.ease;

 

+import static org.junit.Assert.assertArrayEquals;

 import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertFalse;

 import static org.junit.Assert.assertNotNull;

-import static org.mockito.Mockito.mock;

+import static org.junit.Assert.assertNull;

+import static org.junit.Assert.assertTrue;

 

 import java.io.ByteArrayOutputStream;

 import java.io.IOException;

+import java.net.URL;

 import java.util.Arrays;

+import java.util.Map;

 

+import org.eclipse.core.runtime.jobs.Job;

+import org.junit.After;

 import org.junit.Before;

 import org.junit.Test;

-import org.mockito.Mockito;

 

 public class AbstractScriptEngineTest {

+	protected static final int TEST_TIMEOUT = 3000;

 

-	private AbstractScriptEngine fAbstractScriptEngineTest;

+	protected static final String VALID_SAMPLE_CODE = "1";

+

+	protected static final CharSequence ERROR_MARKER = "ERROR";

+

+	private class MockedScriptEngine extends AbstractScriptEngine {

+

+		public MockedScriptEngine() {

+			super("Mocked Engine");

+		}

+

+		@Override

+		public void terminateCurrent() {

+		}

+

+		@Override

+		public void registerJar(URL url) {

+		}

+

+		@Override

+		protected Object internalGetVariable(String name) {

+			return null;

+		}

+

+		@Override

+		protected Map<String, Object> internalGetVariables() {

+			return null;

+		}

+

+		@Override

+		protected boolean internalHasVariable(String name) {

+			return false;

+		}

+

+		@Override

+		protected void internalSetVariable(String name, Object content) {

+		}

+

+		@Override

+		protected void setupEngine() throws ScriptEngineException {

+		}

+

+		@Override

+		protected void teardownEngine() throws ScriptEngineException {

+		}

+

+		@Override

+		protected Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable {

+			final String input = script.getCommand().toString();

+			if (input.contains(ERROR_MARKER))

+				throw new RuntimeException(input);

+			else

+				getOutputStream().write(input.getBytes());

+

+			return input;

+		}

+	}

+

+	protected AbstractScriptEngine fTestEngine;

 

 	@Before

 	public void setup() {

-		fAbstractScriptEngineTest = mock(AbstractScriptEngine.class, Mockito.CALLS_REAL_METHODS);

+		fTestEngine = new MockedScriptEngine();

+	}

+

+	@After

+	public void teardown() throws InterruptedException {

+		if (fTestEngine.getState() != Job.NONE) {

+			fTestEngine.terminate();

+			fTestEngine.joinEngine();

+		}

+	}

+

+	@Test

+	public void streamsAvailable() {

+		assertNotNull(fTestEngine.getOutputStream());

+		assertNotNull(fTestEngine.getErrorStream());

+		assertNotNull(fTestEngine.getInputStream());

 	}

 

 	@Test

 	public void setNotNullOutputStream() throws IOException {

 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

 

-		fAbstractScriptEngineTest.setOutputStream(bos);

+		fTestEngine.setOutputStream(bos);

 

-		assertNotNull(fAbstractScriptEngineTest.getOutputStream());

-		fAbstractScriptEngineTest.getOutputStream().print("test");

+		assertNotNull(fTestEngine.getOutputStream());

+		fTestEngine.getOutputStream().print("test");

 		Arrays.equals("test".getBytes(), bos.toByteArray());

 	}

 

 	@Test

 	public void setNullOutputStream() throws IOException {

-		fAbstractScriptEngineTest.setOutputStream(null);

+		fTestEngine.setOutputStream(null);

 

-		assertEquals(System.out, fAbstractScriptEngineTest.getOutputStream());

+		assertEquals(System.out, fTestEngine.getOutputStream());

 	}

 

 	@Test

 	public void setNotNullErrorStream() throws IOException {

 		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

 

-		assertNotNull(fAbstractScriptEngineTest.getErrorStream());

-		fAbstractScriptEngineTest.getErrorStream().print("test");

+		assertNotNull(fTestEngine.getErrorStream());

+		fTestEngine.getErrorStream().print("test");

 		Arrays.equals("test".getBytes(), bos.toByteArray());

 	}

 

 	@Test

 	public void setNullErrorStream() {

-		fAbstractScriptEngineTest.setErrorStream(null);

+		fTestEngine.setErrorStream(null);

 

-		assertEquals(System.err, fAbstractScriptEngineTest.getErrorStream());

+		assertEquals(System.err, fTestEngine.getErrorStream());

+	}

+

+	@Test

+	public void isJob() {

+		assertTrue(fTestEngine instanceof Job);

+	}

+

+	@Test

+	public void executeValidCodeAndTerminate() throws InterruptedException {

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		fTestEngine.setOutputStream(bos);

+

+		final ScriptResult result1 = fTestEngine.executeAsync(VALID_SAMPLE_CODE);

+		final ScriptResult result2 = fTestEngine.executeAsync("2");

+		fTestEngine.schedule();

+

+		fTestEngine.joinEngine();

+

+		assertTrue(fTestEngine.isFinished());

+		assertEquals("12", bos.toString());

+

+		assertTrue(result1.isReady());

+		assertEquals(VALID_SAMPLE_CODE, result1.getResult());

+		assertFalse(result1.hasException());

+		assertNull(result1.getException());

+

+		assertTrue(result2.isReady());

+		assertEquals("2", result2.getResult());

+		assertFalse(result2.hasException());

+		assertNull(result2.getException());

+	}

+

+	@Test

+	public void executeErrorCodeAndTerminate() throws InterruptedException {

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		fTestEngine.setOutputStream(bos);

+

+		final ScriptResult result1 = fTestEngine.executeAsync(VALID_SAMPLE_CODE);

+		final ScriptResult result2 = fTestEngine.executeAsync("ERROR");

+		fTestEngine.schedule();

+

+		fTestEngine.joinEngine();

+

+		assertTrue(fTestEngine.isFinished());

+		assertEquals(VALID_SAMPLE_CODE, bos.toString());

+

+		assertTrue(result1.isReady());

+		assertEquals(VALID_SAMPLE_CODE, result1.getResult());

+		assertFalse(result1.hasException());

+		assertNull(result1.getException());

+

+		assertTrue(result2.isReady());

+		assertNull(result2.getResult());

+		assertTrue(result2.hasException());

+		assertEquals(RuntimeException.class, result2.getException().getClass());

+	}

+

+	@Test

+	public void executeSync() throws InterruptedException {

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		fTestEngine.setOutputStream(bos);

+

+		final ScriptResult result1 = fTestEngine.executeSync(VALID_SAMPLE_CODE);

+

+		fTestEngine.joinEngine();

+

+		assertTrue(fTestEngine.isFinished());

+		assertEquals(VALID_SAMPLE_CODE, bos.toString());

+

+		assertTrue(result1.isReady());

+		assertEquals(VALID_SAMPLE_CODE, result1.getResult());

+		assertFalse(result1.hasException());

+		assertNull(result1.getException());

+	}

+

+	@Test(timeout = TEST_TIMEOUT)

+	public void inject() throws InterruptedException {

+		assertEquals(VALID_SAMPLE_CODE, fTestEngine.inject(VALID_SAMPLE_CODE));

+	}

+

+	@Test(timeout = TEST_TIMEOUT)

+	public void engineTerminatesWhenIdle() throws InterruptedException {

+		fTestEngine.schedule();

+		fTestEngine.joinEngine();

+	}

+

+	@Test(timeout = TEST_TIMEOUT)

+	public void terminateViaTerminateMethod() throws InterruptedException {

+		final MockedScriptEngine engine = new MockedScriptEngine() {

+			@Override

+			protected Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable {

+				Thread.sleep(100);

+				return super.execute(script, reference, fileName, uiThread);

+			}

+		};

+

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		engine.setOutputStream(bos);

+

+		ScriptResult scriptResult = null;

+		for (int loop = 0; loop <= 100; loop++)

+			scriptResult = engine.executeAsync("Loop " + loop + "\n");

+

+		engine.schedule();

+

+		// wait for engine to produce output

+		while (bos.toString().isEmpty())

+			Thread.yield();

+

+		engine.terminate();

+		engine.joinEngine();

+

+		assertFalse(bos.toString().contains("Loop 100"));

+

+		assertTrue(scriptResult.isReady());

+		assertNull(scriptResult.getResult());

+		assertTrue(scriptResult.hasException());

+		assertEquals(ScriptExecutionException.class, scriptResult.getException().getClass());

+	}

+

+	@Test(timeout = TEST_TIMEOUT)

+	public void terminateViaMonitorCancellation() throws InterruptedException {

+		final MockedScriptEngine engine = new MockedScriptEngine() {

+			@Override

+			protected Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable {

+				Thread.sleep(100);

+				return super.execute(script, reference, fileName, uiThread);

+			}

+		};

+

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		engine.setOutputStream(bos);

+

+		ScriptResult scriptResult = null;

+		for (int loop = 0; loop <= 100; loop++)

+			scriptResult = engine.executeAsync("Loop " + loop + "\n");

+

+		engine.schedule();

+

+		// wait for engine to produce output

+		while (bos.toString().isEmpty())

+			Thread.yield();

+

+		engine.getMonitor().setCanceled(true);

+		engine.joinEngine();

+

+		assertFalse(bos.toString().contains("Loop 100"));

+

+		assertTrue("result " + scriptResult.hashCode() + " is not ready", scriptResult.isReady());

+		assertNull(scriptResult.getResult());

+		assertTrue(scriptResult.hasException());

+		assertEquals(ScriptExecutionException.class, scriptResult.getException().getClass());

+	}

+

+	@Test(timeout = TEST_TIMEOUT)

+	public void terminateViaMethodCallback() throws InterruptedException {

+		final MockedScriptEngine engine = new MockedScriptEngine() {

+			@Override

+			protected Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable {

+				getMonitor().setCanceled(true);

+				checkForCancellation();

+				return super.execute(script, reference, fileName, uiThread);

+			}

+		};

+

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		engine.setOutputStream(bos);

+

+		final ScriptResult scriptResult = engine.executeAsync(VALID_SAMPLE_CODE);

+

+		engine.schedule();

+		engine.joinEngine();

+

+		assertTrue(bos.toString().isEmpty());

+

+		assertTrue(scriptResult.isReady());

+		assertNull(scriptResult.getResult());

+		assertTrue(scriptResult.hasException());

+		assertEquals(ScriptExecutionException.class, scriptResult.getException().getClass());

+	}

+

+	@Test(timeout = TEST_TIMEOUT)

+	public void terminateMultipleTimes() {

+		final MockedScriptEngine engine = new MockedScriptEngine() {

+			@Override

+			protected Object execute(Script script, Object reference, String fileName, boolean uiThread) throws Throwable {

+				Thread.sleep(300);

+				return super.execute(script, reference, fileName, uiThread);

+			}

+		};

+

+		final ByteArrayOutputStream bos = new ByteArrayOutputStream();

+		engine.setOutputStream(bos);

+

+		for (int loop = 0; loop < 10; loop++)

+			engine.executeAsync("Loop " + loop + "\n");

+

+		engine.schedule();

+

+		// wait for engine to produce output

+		while (bos.toString().isEmpty())

+			Thread.yield();

+

+		while (engine.getState() != Job.NONE)

+			engine.terminate();

+

+		// this test is pass when it does not throw an Exception

+	}

+

+	@Test

+	public void extractEmptyArguments() {

+		assertEquals(0, AbstractScriptEngine.extractArguments(null).length);

+		assertEquals(0, AbstractScriptEngine.extractArguments("").length);

+		assertEquals(0, AbstractScriptEngine.extractArguments("    ").length);

+		assertEquals(0, AbstractScriptEngine.extractArguments("\t\t").length);

+	}

+

+	@Test

+	public void extractArguments() {

+		assertArrayEquals(new String[] { "one" }, AbstractScriptEngine.extractArguments("one"));

+		assertArrayEquals(new String[] { "one with spaces" }, AbstractScriptEngine.extractArguments("one with spaces"));

+		assertArrayEquals(new String[] { "one", "and", "another" }, AbstractScriptEngine.extractArguments("one,and, another"));

 	}

 }