/**
* Copyright 2013 Apigee Corporation.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.apigee.trireme.core.internal;
import io.apigee.trireme.core.ArgUtils;
import io.apigee.trireme.core.NodeEnvironment;
import io.apigee.trireme.core.NodeException;
import io.apigee.trireme.core.NodeModule;
import io.apigee.trireme.core.NodeRuntime;
import io.apigee.trireme.core.NodeScript;
import io.apigee.trireme.core.Sandbox;
import io.apigee.trireme.core.ScriptFuture;
import io.apigee.trireme.core.ScriptStatus;
import io.apigee.trireme.core.ScriptTask;
import io.apigee.trireme.core.Utils;
import io.apigee.trireme.core.modules.AbstractFilesystem;
import io.apigee.trireme.core.modules.Buffer;
import io.apigee.trireme.core.modules.NativeModule;
import io.apigee.trireme.core.modules.Process;
import io.apigee.trireme.core.modules.ProcessWrap;
import io.apigee.trireme.net.SelectorHandler;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextAction;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This class actually runs the script.
*/
public class ScriptRunner
implements NodeRuntime, Callable<ScriptStatus>
{
public static final String RUNNER = "runner";
private static final Logger log = LoggerFactory.getLogger(ScriptRunner.class);
private static final long DEFAULT_DELAY = Integer.MAX_VALUE;
public static final String TIMEOUT_TIMESTAMP_KEY = "_tickTimeout";
private final NodeEnvironment env;
private long now;
private ModuleRegistry registry;
private File scriptFile;
private String script;
private final NodeScript scriptObject;
private final String[] args;
private final HashMap<String, NativeModule.ModuleImpl> moduleCache = new HashMap<String, NativeModule.ModuleImpl>();
private final HashMap<String, Object> internalModuleCache = new HashMap<String, Object>();
private ScriptFuture future;
private final CountDownLatch initialized = new CountDownLatch(1);
private final Sandbox sandbox;
private final PathTranslator pathTranslator;
private final ExecutorService asyncPool;
private final IdentityHashMap<Closeable, Closeable> openHandles =
new IdentityHashMap<Closeable, Closeable>();
private final ConcurrentLinkedQueue<Activity> tickFunctions = new ConcurrentLinkedQueue<Activity>();
private final PriorityQueue<Activity> timerQueue = new PriorityQueue<Activity>();
private final Selector selector;
private int timerSequence;
private final AtomicInteger pinCount = new AtomicInteger(0);
// Globals that are set up for the process
private NativeModule.NativeImpl nativeModule;
protected Process.ProcessImpl process;
private Buffer.BufferModuleImpl buffer;
private String workingDirectory;
private String scriptFileName;
private ProcessWrap.ProcessImpl parentProcess;
private boolean forceRepl;
private ScriptableObject scope;
public ScriptRunner(NodeScript so, NodeEnvironment env, Sandbox sandbox,
File scriptFile, String[] args)
{
this(so, env, sandbox, args);
this.scriptFile = scriptFile;
try {
this.scriptFileName = pathTranslator.reverseTranslate(scriptFile.getPath());
} catch (IOException ioe) {
throw new AssertionError("Error translating file path: " + ioe);
}
}
public ScriptRunner(NodeScript so, NodeEnvironment env, Sandbox sandbox,
String scriptName, String script,
String[] args)
{
this(so, env, sandbox, args);
this.script = script;
this.scriptFileName = scriptName;
}
public ScriptRunner(NodeScript so, NodeEnvironment env, Sandbox sandbox,
String[] args, boolean forceRepl)
{
this(so, env, sandbox, args);
this.forceRepl = forceRepl;
}
private ScriptRunner(NodeScript so, NodeEnvironment env, Sandbox sandbox,
String[] args)
{
this.env = env;
this.scriptObject = so;
this.args = args;
this.sandbox = sandbox;
this.pathTranslator = new PathTranslator();
if ((sandbox != null) && (sandbox.getFilesystemRoot() != null)) {
try {
pathTranslator.setRoot(sandbox.getFilesystemRoot());
} catch (IOException ioe) {
throw new AssertionError("Unexpected I/O error setting filesystem root: " + ioe);
}
}
if ((sandbox != null) && (sandbox.getWorkingDirectory() != null)) {
this.workingDirectory = sandbox.getWorkingDirectory();
} else if ((sandbox != null) && (sandbox.getFilesystemRoot() != null)) {
this.workingDirectory = "/";
} else {
this.workingDirectory = new File(".").getAbsolutePath();
}
pathTranslator.setWorkingDir(workingDirectory);
if ((sandbox != null) && (sandbox.getAsyncThreadPool() != null)) {
this.asyncPool = sandbox.getAsyncThreadPool();
} else {
this.asyncPool = env.getAsyncPool();
}
if ((sandbox != null) && (sandbox.getMounts() != null)) {
for (Map.Entry<String, String> mount : sandbox.getMounts()) {
pathTranslator.mount(mount.getKey(), new File(mount.getValue()));
}
}
try {
this.selector = Selector.open();
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
public void close()
{
try {
selector.close();
} catch (IOException ioe) {
log.debug("Error closing selector", ioe);
}
}
public void setFuture(ScriptFuture future) {
this.future = future;
}
public ScriptFuture getFuture() {
return future;
}
public NodeEnvironment getEnvironment() {
return env;
}
public long getLoopTimestamp() {
return now;
}
public ModuleRegistry getRegistry() {
return registry;
}
public void setRegistry(ModuleRegistry registry) {
this.registry = registry;
}
@Override
public Sandbox getSandbox() {
return sandbox;
}
@Override
public NodeScript getScriptObject() {
return scriptObject;
}
public String getWorkingDirectory() {
return workingDirectory;
}
public void setWorkingDirectory(String wd)
throws IOException
{
File wdf = new File(wd);
if (wdf.isAbsolute()) {
this.workingDirectory = wd;
} else {
File newWdf = new File(this.workingDirectory, wd);
this.workingDirectory = newWdf.getCanonicalPath();
}
pathTranslator.setWorkingDir(this.workingDirectory);
}
public Scriptable getScriptScope() {
return scope;
}
public NativeModule.NativeImpl getNativeModule() {
return nativeModule;
}
public Buffer.BufferModuleImpl getBufferModule() {
return buffer;
}
@Override
public Selector getSelector() {
return selector;
}
@Override
public ExecutorService getAsyncPool() {
return asyncPool;
}
@Override
public ExecutorService getUnboundedPool() {
return env.getScriptPool();
}
public InputStream getStdin() {
return ((sandbox != null) && (sandbox.getStdin() != null)) ? sandbox.getStdin() : System.in;
}
public OutputStream getStdout() {
return ((sandbox != null) && (sandbox.getStdout() != null)) ? sandbox.getStdout() : System.out;
}
public OutputStream getStderr() {
return ((sandbox != null) && (sandbox.getStderr() != null)) ? sandbox.getStderr() : System.err;
}
public ProcessWrap.ProcessImpl getParentProcess() {
return parentProcess;
}
public Process.ProcessImpl getProcess() {
return process;
}
public void setParentProcess(ProcessWrap.ProcessImpl parentProcess)
{
this.parentProcess = parentProcess;
}
/**
* We use this when spawning child scripts to avoid sending them messages before they are ready.
*/
public void awaitInitialization()
{
try {
initialized.await();
} catch (InterruptedException ignore) {
}
}
/**
* Translate a path based on the root.
*/
@Override
public File translatePath(String path)
{
// NIO does not like \\?\ UNC path prefix
if(path.startsWith("\\\\?\\"))
path = path.substring(4);
File pf = new File(path);
/*
if (!pf.isAbsolute()) {
pf = new File(pf, workingDirectory);
}
*/
return pathTranslator.translate(pf.getPath());
}
@Override
public String reverseTranslatePath(String path)
throws IOException
{
return pathTranslator.reverseTranslate(path);
}
public PathTranslator getPathTranslator()
{
return pathTranslator;
}
/**
* This method uses a concurrent queue so it may be called from any thread.
*/
@Override
public void enqueueCallback(Function f, Scriptable scope, Scriptable thisObj, Object[] args)
{
enqueueCallback(f, scope, thisObj, null, args);
}
/**
* This method uses a concurrent queue so it may be called from any thread.
*/
@Override
public void enqueueCallback(Function f, Scriptable scope, Scriptable thisObj, Scriptable domain, Object[] args)
{
Callback cb = new Callback(f, scope, thisObj, args);
cb.setDomain(domain);
tickFunctions.offer(cb);
selector.wakeup();
}
/**
* This method uses a concurrent queue so it may be called from any thread.
*/
@Override
public void enqueueTask(ScriptTask task)
{
enqueueTask(task, null);
}
/**
* This method uses a concurrent queue so it may be called from any thread.
*/
@Override
public void enqueueTask(ScriptTask task, Scriptable domain)
{
Task t = new Task(task, scope);
t.setDomain(domain);
tickFunctions.offer(t);
selector.wakeup();
}
/**
* This method is used by the "child_process" module when sending an IPC message between child processes
* in the same JVM.
*
* @param message A JavaScript object, String, or Buffer. We will make a copy to prevent confusion.
* @param child If null, deliver the message to the "process" object. Otherwise, deliver it to the
* specified child.
*/
public void enqueueIpc(Context cx, Object message, final ProcessWrap.ProcessImpl child)
{
Object toDeliver;
String event = "message";
if (message == ProcessWrap.IPC_DISCONNECT) {
event = "disconnect";
toDeliver = Undefined.instance;
} else if (message instanceof Buffer.BufferImpl) {
// Copy the bytes, because a buffer might be modified between apps
ByteBuffer bb = ((Buffer.BufferImpl)message).getBuffer();
toDeliver = Buffer.BufferImpl.newBuffer(cx, scope, bb, true);
} else if (message instanceof Scriptable) {
// Copy the object because we can't rely on safely sharing them between apps.
Scriptable s = (Scriptable)message;
toDeliver = copy(cx, s);
if (s.has("cmd", s)) {
String cmd = Context.toString(s.get("cmd", s));
if (cmd.startsWith("NODE_")) {
event = "internalMessage";
}
}
} else if (message instanceof String) {
// Strings are immutable in Java!
toDeliver = message;
} else {
throw new AssertionError("Unsupported object type for IPC");
}
final Object reallyDeliver = toDeliver;
final String fevent = event;
if (child == null) {
// We are called on child's script runtime, so enqueue a task here
enqueueTask(new ScriptTask() {
@Override
public void execute(Context cx, Scriptable scope)
{
if ("disconnect".equals(fevent)) {
// Special handling for a disconnect from the parent
if (process.isConnected()) {
process.setConnected(false);
process.getEmit().call(cx, scope, process, new Object[] { fevent });
}
} else {
process.getEmit().call(cx, scope, process,
new Object[] { fevent, reallyDeliver });
}
}
});
} else {
// We are the child's script runtime. Enqueue task that sends to the parent
// "child" here actually refers to the "child_process" object inside the parent!
assert(child.getRuntime() != this);
child.getRuntime().enqueueTask(new ScriptTask()
{
@Override
public void execute(Context cx, Scriptable scope)
{
// Now we should be running inside the script thread of the other script
child.getOnMessage().call(cx, scope, null, new Object[] { fevent, reallyDeliver });
}
});
}
}
/**
* Copy one JavaScript object to another, taking nested objects into account. Don't copy primitive fields
* because we assume that they are immutable (string, boolean, and number).
*/
private Scriptable copy(Context cx, Scriptable s)
{
if (s instanceof Function) {
return null;
}
Scriptable t = cx.newObject(scope);
for (Object id : s.getIds()) {
if (id instanceof String) {
String n = (String)id;
Object val = s.get(n, s);
if (val instanceof Scriptable) {
val = copy(cx, (Scriptable)val);
}
t.put(n, t, val);
} else if (id instanceof Number) {
int i = ((Number)id).intValue();
Object val = s.get(i, s);
if (val instanceof Scriptable) {
val = copy(cx, (Scriptable)val);
}
t.put(i, t, val);
} else {
throw new AssertionError();
}
}
return t;
}
public Scriptable getDomain()
{
return ArgUtils.ensureValid(process.getDomain());
}
/**
* This method puts the task directly on the timer queue, which is unsynchronized. If it is ever used
* outside the context of the "TimerWrap" module then we need to check for synchronization, add an
* assertion check, or synchronize the timer queue.
*/
public Activity createTimer(long delay, boolean repeating, long repeatInterval, ScriptTask task,
Scriptable scope)
{
Task t = new Task(task, scope);
long timeout = System.currentTimeMillis() + delay;
int seq = timerSequence++;
if (log.isDebugEnabled()) {
log.debug("Going to fire timeout {} at {}", seq, timeout);
}
t.setId(seq);
t.setTimeout(timeout);
if (repeating) {
t.setInterval(repeatInterval);
t.setRepeating(true);
}
timerQueue.add(t);
selector.wakeup();
return t;
}
@Override
public void pin()
{
int currentPinCount = pinCount.incrementAndGet();
log.debug("Pin count is now {}", currentPinCount);
}
@Override
public void unPin()
{
int currentPinCount = pinCount.decrementAndGet();
log.debug("Pin count is now {}", currentPinCount);
if (currentPinCount < 0) {
log.warn("Negative pin count: {}", currentPinCount);
}
if (currentPinCount == 0) {
selector.wakeup();
}
}
public void setErrno(String err)
{
scope.put("errno", scope, err);
}
public void clearErrno()
{
scope.put("errno", scope, 0);
}
public Object getErrno()
{
if (scope.has("errno", scope)) {
Object errno = scope.get("errno", scope);
if (errno == null) {
return Context.getUndefinedValue();
}
return scope.get("errno", scope);
}
return Context.getUndefinedValue();
}
@Override
public void registerCloseable(Closeable c)
{
openHandles.put(c, c);
}
@Override
public void unregisterCloseable(Closeable c)
{
openHandles.remove(c);
}
/**
* Clean up all the leaked handles and file descriptors.
*/
private void closeCloseables(Context cx)
{
AbstractFilesystem fs = (AbstractFilesystem)requireInternal("fs", cx);
if (fs == null) {
// We might still be initializing
return;
}
fs.cleanup();
for (Closeable c: openHandles.values()) {
if (log.isDebugEnabled()) {
log.debug("Closing leaked handle {}", c);
}
try {
c.close();
} catch (IOException ioe) {
if (log.isDebugEnabled()) {
log.debug("Error closing leaked handle: {}", ioe);
}
}
}
}
/**
* Execute the script.
*/
@Override
public ScriptStatus call()
throws NodeException
{
Object ret = env.getContextFactory().call(new ContextAction()
{
@Override
public Object run(Context cx)
{
return runScript(cx);
}
});
return (ScriptStatus)ret;
}
protected ScriptStatus runScript(Context cx)
{
ScriptStatus status;
if (scriptObject.getDisplayName() != null) {
try {
Thread.currentThread().setName("Trireme: " + scriptObject.getDisplayName());
} catch (SecurityException ignore) {
}
}
cx.putThreadLocal(RUNNER, this);
now = System.currentTimeMillis();
try {
// All scripts get their own global scope. This is a lot safer than sharing them in case a script wants
// to add to the prototype of String or Date or whatever (as they often do)
// This uses a bit more memory and in theory slows down script startup but in practice it is
// a drop in the bucket.
scope = cx.initStandardObjects();
// Lazy first-time init of the node version.
registry.load(cx);
try {
initGlobals(cx);
} catch (NodeException ne) {
return new ScriptStatus(ne);
} finally {
initialized.countDown();
}
if ((scriptFile == null) && (script == null)) {
// Just have trireme.js process "process.argv"
process.setForceRepl(forceRepl);
setArgv(null);
} else if (scriptFile == null) {
// If the script was passed as a string, pretend that "-e" was used to "eval" it.
// We also get here if we were called by "executeModule".
process.setEval(script);
process.setPrintEval(scriptObject.isPrintEval());
setArgv(scriptFileName);
} else {
// Otherwise, assume that the script was the second argument to "argv".
setArgv(scriptFileName);
}
// Run "trireme.js," which is our equivalent of "node.js". It returns a function that takes
// "process". When done, we may have ticks to execute.
Script mainScript = registry.getMainScript();
Function main = (Function)mainScript.exec(cx, scope);
boolean timing = startTiming(cx);
try {
main.call(cx, scope, scope, new Object[] { process });
} catch (RhinoException re) {
boolean handled = handleScriptException(cx, re);
if (!handled) {
throw re;
}
} finally {
if (timing) {
endTiming(cx);
}
}
status = mainLoop(cx);
} catch (NodeExitException ne) {
// This exception is thrown by process.exit()
status = ne.getStatus();
} catch (IOException ioe) {
log.debug("I/O exception processing script: {}", ioe);
status = new ScriptStatus(ioe);
} catch (Throwable t) {
log.debug("Unexpected script error: {}", t);
status = new ScriptStatus(t);
}
log.debug("Script exiting with exit code {}", status.getExitCode());
if (!status.hasCause() && !process.isExiting()) {
// Fire the exit callback, but only if we aren't exiting due to an unhandled exception, and "exit"
// wasn't already fired because we called "exit"
try {
process.setExiting(true);
process.fireExit(cx, status.getExitCode());
} catch (NodeExitException ee) {
// Exit called exit -- allow it to replace the exit code
log.debug("Script replacing exit code with {}", ee.getCode());
status = ee.getStatus();
} catch (RhinoException re) {
// Many of the unit tests fire exceptions inside exit.
status = new ScriptStatus(re);
}
}
closeCloseables(cx);
try {
OutputStream stdout = getStdout();
if (stdout != System.out) {
stdout.close();
}
OutputStream stderr = getStderr();
if (stderr != System.err) {
stderr.close();
}
} catch (IOException ignore) {
}
return status;
}
private void setArgv(String scriptName)
{
String[] argv;
if (scriptName == null) {
argv = new String[args == null ? 1 : args.length + 1];
} else {
argv = new String[args == null ? 2 : args.length + 2];
}
int p = 0;
argv[p++] = Process.EXECUTABLE_NAME;
if (scriptName != null) {
argv[p++] = scriptName;
}
if (args != null) {
System.arraycopy(args, 0, argv, p, args.length);
}
if (log.isDebugEnabled()) {
for (int i = 0; i < argv.length; i++) {
log.debug("argv[{}] = {}", i, argv[i]);
}
}
process.initializeArgv(argv);
}
private ScriptStatus mainLoop(Context cx)
throws IOException
{
// Exit if there's no work do to but only if we're not pinned by a module.
// We might exit if there are events on the timer queue if they are not also pinned.
while (!tickFunctions.isEmpty() || (pinCount.get() > 0) || process.isCallbacksRequired()) {
try {
if ((future != null) && future.isCancelled()) {
return ScriptStatus.CANCELLED;
}
// Call tick functions scheduled by process.nextTick. Node.js docs for
// process.nextTick say that these things run before anything else in the event loop
executeNextTicks(cx);
// Call tick functions scheduled by Java code.
executeTicks(cx);
// If necessary, call into the timer module to fire all the tasks set up with "setImmediate."
// Again, like regular Node, the docs say that these run before all I/O activity and all timers.
executeImmediateCallbacks(cx);
// Calculate how long we will wait in the call to select, taking into consideration
// what is on the timer queue and if there are pending ticks or immediate tasks.
now = System.currentTimeMillis();
long pollTimeout;
if (!tickFunctions.isEmpty() || process.isCallbacksRequired() || (pinCount.get() == 0)) {
// Immediate work -- need to keep spinning
// Also keep spinning if we have no reason to keep the loop open
pollTimeout = 0L;
} else if (timerQueue.isEmpty()) {
pollTimeout = DEFAULT_DELAY;
} else {
Activity nextActivity = timerQueue.peek();
pollTimeout = (nextActivity.timeout - now);
}
if (log.isTraceEnabled()) {
Scriptable ib = (Scriptable)process.getTickInfoBox();
log.trace("PollDelay = {}. tickFunctions = {} needImmediate = {} needTick = {} timerQueue = {} pinCount = {} tick = {}, {}, {}",
pollTimeout, tickFunctions.size(), process.isNeedImmediateCallback(),
process.isNeedTickCallback(), timerQueue.size(), pinCount.get(),
ib.get(0, ib), ib.get(1, ib), ib.get(2, ib));
}
// Check for network I/O and also sleep if necessary.
// Any new timer or tick will wake up the selector immediately
if (pollTimeout > 0L) {
if (log.isDebugEnabled()) {
log.debug("mainLoop: sleeping for {} pinCount = {}", pollTimeout, pinCount.get());
}
selector.select(pollTimeout);
} else {
selector.selectNow();
}
// Fire any selected I/O functions
executeNetworkCallbacks(cx);
// Check the timer queue for all expired timers
executeTimerTasks(cx, now);
} catch (NodeExitException ne) {
// This exception is thrown by process.exit()
return ne.getStatus();
} catch (RhinoException re) {
// All domain and process-wide error handling happened before we got here, so
// if we get a RhinoException here, then we know that it is fatal.
return new ScriptStatus(re);
}
}
return ScriptStatus.OK;
}
private Scriptable makeError(Context cx, RhinoException re)
{
if ((re instanceof JavaScriptException) &&
(((JavaScriptException)re).getValue() instanceof Scriptable)) {
return (Scriptable)((JavaScriptException)re).getValue();
} else if (re instanceof EcmaError) {
return Utils.makeErrorObject(cx, scope, ((EcmaError) re).getErrorMessage(), re);
} else {
return Utils.makeErrorObject(cx, scope, re.getMessage(), re);
}
}
private boolean handleScriptException(Context cx, RhinoException re)
{
if (re instanceof NodeExitException) {
return false;
}
// Stop script timing before we run this, so that we don't end up timing out the script twice!
endTiming(cx);
Function handleFatal = process.getFatalException();
if (handleFatal == null) {
return false;
}
if (log.isDebugEnabled()) {
log.debug("Handling fatal exception {} domain = {}\n{}",
re, System.identityHashCode(process.getDomain()), re.getScriptStackTrace());
log.debug("Fatal Java exception: {}", re);
}
Scriptable error = makeError(cx, re);
boolean handled =
Context.toBoolean(handleFatal.call(cx, scope, scope, new Object[] { error }));
if (log.isDebugEnabled()) {
log.debug("Handled = {}", handled);
}
return handled;
}
/**
* Execute ticks as defined by process.nextTick() and anything put on the queue from Java code.
* Each one is timed separately, and error handling is done in here
* so that we fire other things in the loop (such as timers) in the event of an error.
*/
public void executeTicks(Context cx)
throws RhinoException
{
Activity nextCall;
do {
nextCall = tickFunctions.poll();
if (nextCall != null) {
boolean timing = startTiming(cx);
try {
nextCall.execute(cx);
} catch (RhinoException re) {
boolean handled = handleScriptException(cx, re);
if (!handled) {
throw re;
} else {
// We can't keep looping here, because all these errors could cause starvation.
// Let timers and network I/O run instead.
return;
}
} finally {
if (timing) {
endTiming(cx);
}
}
}
} while (nextCall != null);
}
/**
* Execute everything set up by nextTick()
*/
private void executeNextTicks(Context cx)
throws RhinoException
{
if (process.isNeedTickCallback()) {
if (log.isTraceEnabled()) {
log.trace("Executing ticks");
}
boolean timed = startTiming(cx);
try {
process.callTickFromSpinner(cx);
} catch (RhinoException re) {
boolean handled = handleScriptException(cx, re);
if (!handled) {
throw re;
}
} finally {
if (timed) {
endTiming(cx);
}
}
}
}
/**
* Execute everything set up by setImmediate().
*/
private void executeImmediateCallbacks(Context cx)
throws RhinoException
{
if (process.isNeedImmediateCallback()) {
if (log.isTraceEnabled()) {
log.trace("Executing immediate tasks");
}
boolean timed = startTiming(cx);
try {
process.callImmediateTasks(cx);
} catch (RhinoException re) {
boolean handled = handleScriptException(cx, re);
if (!handled) {
throw re;
}
} finally {
if (timed) {
endTiming(cx);
}
}
}
}
/**
* Execute everything that the selector has told is is ready.
*/
private void executeNetworkCallbacks(Context cx)
throws RhinoException
{
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey selKey = keys.next();
keys.remove();
boolean timed = startTiming(cx);
try {
((SelectorHandler)selKey.attachment()).selected(selKey);
} catch (RhinoException re) {
boolean handled = handleScriptException(cx, re);
if (!handled) {
throw re;
}
} finally {
if (timed) {
endTiming(cx);
}
}
}
}
private void executeTimerTasks(Context cx, long now)
throws RhinoException
{
Activity timed = timerQueue.peek();
while ((timed != null) && (timed.timeout <= now)) {
timerQueue.poll();
if (!timed.cancelled) {
boolean timing = startTiming(cx);
try {
if (log.isDebugEnabled()) {
log.debug("Executing timer {}", timed.id);
}
timed.execute(cx);
} catch (RhinoException re) {
boolean handled = handleScriptException(cx, re);
if (!handled) {
throw re;
}
} finally {
if (timing) {
endTiming(cx);
}
}
if (timed.repeating && !timed.cancelled) {
timed.timeout = now + timed.interval;
if (log.isDebugEnabled()) {
log.debug("Re-registering {} to fire at {}", timed.id, timed.timeout);
}
timerQueue.add(timed);
}
}
timed = timerQueue.peek();
}
}
/**
* One-time initialization of the built-in modules and objects.
*/
private void initGlobals(Context cx)
throws NodeException
{
try {
// Need to bootstrap the "native module" before we can do anything
NativeModule.NativeImpl nativeMod =
(NativeModule.NativeImpl)initializeModule(NativeModule.MODULE_NAME, ModuleRegistry.ModuleType.PUBLIC, cx, scope);
this.nativeModule = nativeMod;
NativeModule.ModuleImpl nativeModMod = NativeModule.ModuleImpl.newModule(cx, scope,
NativeModule.MODULE_NAME, NativeModule.MODULE_NAME);
nativeModMod.setLoaded(true);
nativeModMod.setExports(nativeMod);
cacheModule(NativeModule.MODULE_NAME, nativeModMod);
// Next we need "process" which takes a bit more care
process = (Process.ProcessImpl)require(Process.MODULE_NAME, cx);
// Check if we are connected to a parent via API
process.setConnected(parentProcess != null);
// The buffer module needs special handling because of the "charsWritten" variable
buffer = (Buffer.BufferModuleImpl)require("buffer", cx);
// Set up metrics -- defining these lets us run internal Node projects.
// Presumably in "real" node these are set up by some sort of preprocessor...
Scriptable metrics = nativeMod.internalRequire("trireme_metrics", cx);
copyProp(metrics, scope, "DTRACE_NET_SERVER_CONNECTION");
copyProp(metrics, scope, "DTRACE_NET_STREAM_END");
copyProp(metrics, scope, "COUNTER_NET_SERVER_CONNECTION");
copyProp(metrics, scope, "COUNTER_NET_SERVER_CONNECTION_CLOSE");
copyProp(metrics, scope, "DTRACE_HTTP_CLIENT_REQUEST");
copyProp(metrics, scope, "DTRACE_HTTP_CLIENT_RESPONSE");
copyProp(metrics, scope, "DTRACE_HTTP_SERVER_REQUEST");
copyProp(metrics, scope, "DTRACE_HTTP_SERVER_RESPONSE");
copyProp(metrics, scope, "COUNTER_HTTP_CLIENT_REQUEST");
copyProp(metrics, scope, "COUNTER_HTTP_CLIENT_RESPONSE");
copyProp(metrics, scope, "COUNTER_HTTP_SERVER_REQUEST");
copyProp(metrics, scope, "COUNTER_HTTP_SERVER_RESPONSE");
} catch (InvocationTargetException e) {
throw new NodeException(e);
} catch (IllegalAccessException e) {
throw new NodeException(e);
} catch (InstantiationException e) {
throw new NodeException(e);
}
}
private static void copyProp(Scriptable src, Scriptable dest, String name)
{
dest.put(name, dest, src.get(name, src));
}
/**
* Initialize a native module implemented in Java code.
*/
public Object initializeModule(String modName, ModuleRegistry.ModuleType type,
Context cx, Scriptable scope)
throws InvocationTargetException, InstantiationException, IllegalAccessException
{
NodeModule mod;
switch (type) {
case PUBLIC:
mod = registry.get(modName);
break;
case INTERNAL:
mod = registry.getInternal(modName);
break;
case NATIVE:
mod = registry.getNative(modName);
break;
default:
throw new AssertionError();
}
if (mod == null) {
return null;
}
Object exp = mod.registerExports(cx, scope, this);
if (exp == null) {
throw new AssertionError("Module " + modName + " returned a null export");
}
return exp;
}
/**
* This is used internally when one native module depends on another.
*/
@Override
public Object require(String modName, Context cx)
{
try {
return nativeModule.internalRequire(modName, cx);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
throw new EvaluatorException("Error initializing module: " +
((targetException != null) ?
e.toString() + ": " + targetException.toString() :
e.toString()));
} catch (InstantiationException e) {
throw new EvaluatorException("Error initializing module: " + e.toString());
} catch (IllegalAccessException e) {
throw new EvaluatorException("Error initializing modugle: " + e.toString());
}
}
public Object requireInternal(String modName, Context cx)
{
if (process == null) {
// This might be called after a fatal initialization error.
return null;
}
return process.getInternalModule(modName, cx);
}
/**
* This is where we load native modules.
*/
public boolean isNativeModule(String name)
{
return (registry.get(name) != null) ||
(registry.getCompiledModule(name) != null);
}
/**
* Return a module that was created implicitly or by the "native module"
*/
public NativeModule.ModuleImpl getCachedModule(String name)
{
return moduleCache.get(name);
}
public void cacheModule(String name, NativeModule.ModuleImpl module)
{
moduleCache.put(name, module);
}
/**
* Return a module that is used internally and exposed by "process.binding".
*/
public Object getCachedInternalModule(String name)
{
return internalModuleCache.get(name);
}
public void cacheInternalModule(String name, Object module)
{
internalModuleCache.put(name, module);
}
private boolean startTiming(Context cx)
{
if (env != null) {
long tl = env.getScriptTimeLimit();
if (tl > 0L) {
cx.putThreadLocal(TIMEOUT_TIMESTAMP_KEY, System.currentTimeMillis() + tl);
return true;
}
}
return false;
}
private void endTiming(Context cx)
{
cx.removeThreadLocal(TIMEOUT_TIMESTAMP_KEY);
}
public abstract class Activity
implements Comparable<Activity>
{
protected int id;
protected long timeout;
protected long interval;
protected boolean repeating;
protected boolean cancelled;
protected Scriptable domain;
abstract void execute(Context cx);
int getId() {
return id;
}
void setId(int id) {
this.id = id;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public long getInterval() {
return interval;
}
public void setInterval(long interval) {
this.interval = interval;
}
public boolean isRepeating() {
return repeating;
}
public void setRepeating(boolean repeating) {
this.repeating = repeating;
}
public boolean isCancelled() {
return cancelled;
}
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public Scriptable getDomain() {
return domain;
}
public void setDomain(Scriptable domain) {
this.domain = domain;
}
@Override
public int compareTo(Activity a)
{
if (timeout < a.timeout) {
return -1;
}
if (timeout > a.timeout) {
return 1;
}
return 0;
}
}
private final class Callback
extends Activity
{
Function function;
Scriptable scope;
Scriptable thisObj;
Object[] args;
Callback(Function f, Scriptable s, Scriptable thisObj, Object[] args)
{
this.function = f;
this.scope = s;
this.thisObj = thisObj;
this.args = args;
}
/**
* Submit the tick, with support for domains handled in JavaScript.
* This is also necessary because not everything that we do is a "top level function" in JS
* and we cannot invoke those functions directly from Java code.
*/
@Override
void execute(Context cx)
{
Function submitTick = process.getSubmitTick();
Object[] callArgs =
new Object[(args == null ? 0 : args.length) + 3];
callArgs[0] = function;
callArgs[1] = thisObj;
callArgs[2] = domain;
if (args != null) {
System.arraycopy(args, 0, callArgs, 3, args.length);
}
// Submit in the scope of "function"
// pass "this" and the args to "submitTick," which will honor them
submitTick.call(cx, function, process, callArgs);
}
}
private final class Task
extends Activity
{
private ScriptTask task;
private Scriptable scope;
Task(ScriptTask task, Scriptable scope)
{
this.task = task;
this.scope = scope;
}
@Override
void execute(Context cx)
{
if (domain != null) {
if (ScriptableObject.hasProperty(domain, "_disposed")) {
domain = null;
}
}
if (domain != null) {
if (log.isDebugEnabled()) {
log.debug("Entering domain {}", System.identityHashCode(domain));
}
Function enter = (Function)ScriptableObject.getProperty(domain, "enter");
enter.call(cx, enter, domain, ScriptRuntime.emptyArgs);
}
task.execute(cx, scope);
// Do NOT do this next bit in a try..finally block. Why not? Because the exception handling
// logic in runMain depends on "process.domain" still being set, and it will clean up
// on failure there.
if (domain != null) {
if (log.isDebugEnabled()) {
log.debug("Exiting domain {}", System.identityHashCode(domain));
}
Function exit = (Function)ScriptableObject.getProperty(domain, "exit");
exit.call(cx, exit, domain, ScriptRuntime.emptyArgs);
}
}
}
}