", 1, null);
+ mContext.evaluateString(mScriptable, getInitScript().getScript(), SOURCE_NAME_INIT, 1, null);
}
private JavaScriptSource getInitScript() {
@@ -148,12 +154,10 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine {
}
public Context createContext() {
- if (!ContextFactory.hasExplicitGlobal()) {
- ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(mAndroidContext.getCacheDir(), "classes")));
- }
Context context = new RhinoAndroidHelper(mAndroidContext).enterContext();
contextCount++;
setupContext(context);
+ sContextEngineMap.put(context, this);
return context;
}
@@ -164,6 +168,17 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine {
context.setWrapFactory(new WrapFactory());
}
+ public static void initEngine() {
+ if (!ContextFactory.hasExplicitGlobal()) {
+ android.content.Context context = GlobalAppContext.get();
+ ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(context.getCacheDir(), "classes")));
+ }
+ }
+
+ public static RhinoJavaScriptEngine getEngineOfContext(Context context) {
+ return sContextEngineMap.get(context);
+ }
+
private class WrapFactory extends org.mozilla.javascript.WrapFactory {
diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java
index 010a304e..f0a2c09a 100644
--- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java
+++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java
@@ -2,17 +2,18 @@ package com.stardust.autojs.engine;
import android.support.annotation.CallSuper;
-import com.stardust.autojs.runtime.exception.ScriptException;
+import com.stardust.autojs.execution.ScriptExecution;
import com.stardust.autojs.script.ScriptSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by Stardust on 2017/4/2.
*
*
- * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String)} ()}, and then can be
+ * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String, int)} ()}, and then can be
* used to execute script with {@link ScriptEngine#execute(ScriptSource)} in the **same** thread.
* When the execution finish successfully, the engine should be destroy in the thread that created it.
*
@@ -46,6 +47,9 @@ public interface ScriptEngine {
Exception getUncaughtException();
+ void setId(int id);
+
+ int getId();
/**
* @hide
@@ -68,7 +72,7 @@ public interface ScriptEngine {
private OnDestroyListener mOnDestroyListener;
private boolean mDestroyed = false;
private Exception mUncaughtException;
-
+ private volatile AtomicInteger mId = new AtomicInteger(ScriptExecution.NO_ID);
@Override
public synchronized void setTag(String key, Object value) {
@@ -116,5 +120,15 @@ public interface ScriptEngine {
public Exception getUncaughtException() {
return mUncaughtException;
}
+
+ @Override
+ public void setId(int id) {
+ mId.compareAndSet(ScriptExecution.NO_ID, id);
+ }
+
+ @Override
+ public int getId() {
+ return mId.get();
+ }
}
}
diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java
index c73a8cda..91507838 100644
--- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java
+++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import com.stardust.autojs.execution.ScriptExecution;
import com.stardust.autojs.script.ScriptSource;
import com.stardust.util.Supplier;
@@ -96,37 +97,34 @@ public class ScriptEngineManager {
@Nullable
- public ScriptEngine createEngine(String name) {
+ public ScriptEngine createEngine(String name, int id) {
Supplier s = mEngineSuppliers.get(name);
if (s == null) {
return null;
}
ScriptEngine engine = s.get();
+ engine.setId(id);
putProperties(engine);
addEngine(engine);
return engine;
}
@Nullable
- public ScriptEngine createEngineOfSource(ScriptSource source) {
- return createEngine(source.getEngineName());
+ public ScriptEngine createEngineOfSource(ScriptSource source, int id) {
+ return createEngine(source.getEngineName(), id);
}
-
@NonNull
- public ScriptEngine createEngineByNameOrThrow(String name) {
- ScriptEngine engine = createEngine(name);
+ public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source, int id) {
+ ScriptEngine engine = createEngineOfSource(source, id);
if (engine == null)
- throw new ScriptEngineFactory.EngineNotFoundException("name: " + name);
+ throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString());
return engine;
}
@NonNull
public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source) {
- ScriptEngine engine = createEngineOfSource(source);
- if (engine == null)
- throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString());
- return engine;
+ return createEngineOfSourceOrThrow(source, ScriptExecution.NO_ID);
}
public void registerEngine(String name, Supplier supplier) {
diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java
index d59e68ac..298b2323 100644
--- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java
+++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java
@@ -77,4 +77,14 @@ public class ScriptEngineProxy implements ScriptEngine urlToSourceInfo =
+ Collections.synchronizedMap(new HashMap());
+
+ /**
+ * Table mapping function names to information about the function.
+ */
+ private final Map functionNames =
+ Collections.synchronizedMap(new HashMap());
+
+ /**
+ * Table mapping functions to information about the function.
+ */
+ private final Map functionToSource =
+ Collections.synchronizedMap(new HashMap());
+
+ /**
+ * ContextFactory.Listener instance attached to {@link #contextFactory}.
+ */
+ private DimIProxy listener;
+
+ /**
+ * Sets the GuiCallback object to use.
+ */
+ public void setGuiCallback(DebugCallback callback) {
+ this.callback = callback;
+ }
+
+ /**
+ * Tells the debugger to break at the next opportunity.
+ */
+ public void setBreak() {
+ this.breakFlag = true;
+ }
+
+ /**
+ * Sets the ScopeProvider to be used.
+ */
+ public void setScopeProvider(ScopeProvider scopeProvider) {
+ this.scopeProvider = scopeProvider;
+ }
+
+ /**
+ * Sets the ScopeProvider to be used.
+ */
+ public void setSourceProvider(final SourceProvider sourceProvider) {
+ this.sourceProvider = sourceProvider;
+ }
+
+ /**
+ * Switches context to the stack frame with the given index.
+ */
+ public void contextSwitch(int frameIndex) {
+ this.frameIndex = frameIndex;
+ }
+
+ /**
+ * Sets whether the debugger should break on exceptions.
+ */
+ public void setBreakOnExceptions(boolean breakOnExceptions) {
+ this.breakOnExceptions = breakOnExceptions;
+ }
+
+ /**
+ * Sets whether the debugger should break on function entering.
+ */
+ public void setBreakOnEnter(boolean breakOnEnter) {
+ this.breakOnEnter = breakOnEnter;
+ }
+
+ /**
+ * Sets whether the debugger should break on function return.
+ */
+ public void setBreakOnReturn(boolean breakOnReturn) {
+ this.breakOnReturn = breakOnReturn;
+ }
+
+ /**
+ * Attaches the debugger to the given ContextFactory.
+ */
+ public void attachTo(ScriptEngineService scriptEngineService, ContextFactory factory) {
+ detach();
+ this.contextFactory = factory;
+ this.scriptEngineService = scriptEngineService;
+ this.listener = new DimIProxy(this, IPROXY_LISTEN);
+ scriptEngineService.registerEngineLifecycleCallback(this.listener);
+ }
+
+ /**
+ * Detaches the debugger from the current ContextFactory.
+ */
+ public void detach() {
+ if (listener != null) {
+ scriptEngineService.unregisterEngineLifecycleCallback(listener);
+ contextFactory = null;
+ scriptEngineService = null;
+ listener = null;
+ }
+ }
+
+ /**
+ * Releases resources associated with this debugger.
+ */
+ public void dispose() {
+ detach();
+ }
+
+ /**
+ * Returns the FunctionSource object for the given script or function.
+ */
+ private FunctionSource getFunctionSource(DebuggableScript fnOrScript) {
+ FunctionSource fsource = functionSource(fnOrScript);
+ if (fsource == null) {
+ String url = getNormalizedUrl(fnOrScript);
+ SourceInfo si = sourceInfo(url);
+ if (si == null) {
+ if (!fnOrScript.isGeneratedScript()) {
+ // Not eval or Function, try to load it from URL
+ String source = loadSource(url);
+ if (source != null) {
+ DebuggableScript top = fnOrScript;
+ for (; ; ) {
+ DebuggableScript parent = top.getParent();
+ if (parent == null) {
+ break;
+ }
+ top = parent;
+ }
+ registerTopScript(top, source);
+ fsource = functionSource(fnOrScript);
+ }
+ }
+ }
+ }
+ return fsource;
+ }
+
+ /**
+ * Loads the script at the given URL.
+ */
+ private String loadSource(String sourceUrl) {
+ String source = null;
+ int hash = sourceUrl.indexOf('#');
+ if (hash >= 0) {
+ sourceUrl = sourceUrl.substring(0, hash);
+ }
+ try {
+ InputStream is;
+ openStream:
+ {
+ if (sourceUrl.indexOf(':') < 0) {
+ // Can be a file name
+ try {
+ if (sourceUrl.startsWith("~/")) {
+ String home = SecurityUtilities.getSystemProperty("user.home");
+ if (home != null) {
+ String pathFromHome = sourceUrl.substring(2);
+ File f = new File(new File(home), pathFromHome);
+ if (f.exists()) {
+ is = new FileInputStream(f);
+ break openStream;
+ }
+ }
+ }
+ File f = new File(sourceUrl);
+ if (f.exists()) {
+ is = new FileInputStream(f);
+ break openStream;
+ }
+ } catch (SecurityException ex) {
+ }
+ // No existing file, assume missed http://
+ if (sourceUrl.startsWith("//")) {
+ sourceUrl = "http:" + sourceUrl;
+ } else if (sourceUrl.startsWith("/")) {
+ sourceUrl = "http://127.0.0.1" + sourceUrl;
+ } else {
+ sourceUrl = "http://" + sourceUrl;
+ }
+ }
+
+ is = (new URL(sourceUrl)).openStream();
+ }
+
+ try {
+ source = Kit.readReader(new InputStreamReader(is));
+ } finally {
+ is.close();
+ }
+ } catch (IOException ex) {
+ System.err.println
+ ("Failed to load source from " + sourceUrl + ": " + ex);
+ }
+ return source;
+ }
+
+ /**
+ * Registers the given script as a top-level script in the debugger.
+ */
+ private void registerTopScript(DebuggableScript topScript, String source) {
+ if (!topScript.isTopLevel()) {
+ throw new IllegalArgumentException();
+ }
+ String url = getNormalizedUrl(topScript);
+ DebuggableScript[] functions = getAllFunctions(topScript);
+ if (sourceProvider != null) {
+ final String providedSource = sourceProvider.getSource(topScript);
+ if (providedSource != null) {
+ source = providedSource;
+ }
+ }
+
+ final SourceInfo sourceInfo = new SourceInfo(source, functions, url);
+
+ synchronized (urlToSourceInfo) {
+ SourceInfo old = urlToSourceInfo.get(url);
+ if (old != null) {
+ sourceInfo.copyBreakpointsFrom(old);
+ }
+ urlToSourceInfo.put(url, sourceInfo);
+ for (int i = 0; i != sourceInfo.functionSourcesTop(); ++i) {
+ FunctionSource fsource = sourceInfo.functionSource(i);
+ String name = fsource.name();
+ if (name.length() != 0) {
+ functionNames.put(name, fsource);
+ }
+ }
+ }
+
+ synchronized (functionToSource) {
+ for (int i = 0; i != functions.length; ++i) {
+ FunctionSource fsource = sourceInfo.functionSource(i);
+ functionToSource.put(functions[i], fsource);
+ }
+ }
+
+ callback.updateSourceText(sourceInfo);
+ }
+
+ /**
+ * Returns the FunctionSource object for the given function or script.
+ */
+ private FunctionSource functionSource(DebuggableScript fnOrScript) {
+ return functionToSource.get(fnOrScript);
+ }
+
+ /**
+ * Returns an array of all function names.
+ */
+ public String[] functionNames() {
+ synchronized (urlToSourceInfo) {
+ return functionNames.keySet().toArray(new String[functionNames.size()]);
+ }
+ }
+
+ /**
+ * Returns the FunctionSource object for the function with the given name.
+ */
+ public FunctionSource functionSourceByName(String functionName) {
+ return functionNames.get(functionName);
+ }
+
+ /**
+ * Returns the SourceInfo object for the given URL.
+ */
+ public SourceInfo sourceInfo(String url) {
+ return urlToSourceInfo.get(url);
+ }
+
+ /**
+ * Returns the source URL for the given script or function.
+ */
+ private String getNormalizedUrl(DebuggableScript fnOrScript) {
+ String url = fnOrScript.getSourceName();
+ if (url == null) {
+ url = "";
+ } else {
+ // Not to produce window for eval from different lines,
+ // strip line numbers, i.e. replace all #[0-9]+\(eval\) by
+ // (eval)
+ // Option: similar teatment for Function?
+ char evalSeparator = '#';
+ StringBuilder sb = null;
+ int urlLength = url.length();
+ int cursor = 0;
+ for (; ; ) {
+ int searchStart = url.indexOf(evalSeparator, cursor);
+ if (searchStart < 0) {
+ break;
+ }
+ String replace = null;
+ int i = searchStart + 1;
+ while (i != urlLength) {
+ int c = url.charAt(i);
+ if (!('0' <= c && c <= '9')) {
+ break;
+ }
+ ++i;
+ }
+ if (i != searchStart + 1) {
+ // i points after #[0-9]+
+ if ("(eval)".regionMatches(0, url, i, 6)) {
+ cursor = i + 6;
+ replace = "(eval)";
+ }
+ }
+ if (replace == null) {
+ break;
+ }
+ if (sb == null) {
+ sb = new StringBuilder();
+ sb.append(url.substring(0, searchStart));
+ }
+ sb.append(replace);
+ }
+ if (sb != null) {
+ if (cursor != urlLength) {
+ sb.append(url.substring(cursor));
+ }
+ url = sb.toString();
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Returns an array of all functions in the given script.
+ */
+ private static DebuggableScript[] getAllFunctions
+ (DebuggableScript function) {
+ ObjArray functions = new ObjArray();
+ collectFunctions_r(function, functions);
+ DebuggableScript[] result = new DebuggableScript[functions.size()];
+ functions.toArray(result);
+ return result;
+ }
+
+ /**
+ * Helper function for {@link #getAllFunctions(DebuggableScript)}.
+ */
+ private static void collectFunctions_r(DebuggableScript function,
+ ObjArray array) {
+ array.add(function);
+ for (int i = 0; i != function.getFunctionCount(); ++i) {
+ collectFunctions_r(function.getFunction(i), array);
+ }
+ }
+
+ /**
+ * Clears all breakpoints.
+ */
+ public void clearAllBreakpoints() {
+ for (SourceInfo si : urlToSourceInfo.values()) {
+ si.removeAllBreakpoints();
+ }
+ }
+
+ /**
+ * Called when a breakpoint has been hit.
+ */
+ private void handleBreakpointHit(StackFrame frame, Context cx) {
+ breakFlag = false;
+ interrupted(cx, frame, null);
+ }
+
+ /**
+ * Called when a script exception has been thrown.
+ */
+ private void handleExceptionThrown(Context cx, Throwable ex,
+ StackFrame frame) {
+ if (breakOnExceptions) {
+ ContextData cd = frame.contextData();
+ if (cd.lastProcessedException != ex) {
+ interrupted(cx, frame, ex);
+ cd.lastProcessedException = ex;
+ }
+ }
+ }
+
+ /**
+ * Returns the current ContextData object.
+ */
+ public ContextData currentContextData() {
+ return interruptedContextData;
+ }
+
+ /**
+ * Sets the action to perform to end interruption.
+ */
+ public void setReturnValue(int returnValue) {
+ synchronized (monitor) {
+ this.returnValue = returnValue;
+ monitor.notify();
+ }
+ }
+
+ /**
+ * Resumes execution of script.
+ */
+ public void go() {
+ synchronized (monitor) {
+ this.returnValue = GO;
+ monitor.notifyAll();
+ }
+ }
+
+ /**
+ * Evaluates the given script.
+ */
+ public String eval(String expr) {
+ String result = "undefined";
+ if (expr == null) {
+ return result;
+ }
+ ContextData contextData = currentContextData();
+ if (contextData == null || frameIndex >= contextData.frameCount()) {
+ return result;
+ }
+ StackFrame frame = contextData.getFrame(frameIndex);
+ if (contextData.eventThreadFlag) {
+ Context cx = Context.getCurrentContext();
+ result = do_eval(cx, frame, expr);
+ } else {
+ synchronized (monitor) {
+ if (insideInterruptLoop) {
+ evalRequest = expr;
+ evalFrame = frame;
+ monitor.notify();
+ do {
+ try {
+ monitor.wait();
+ } catch (InterruptedException exc) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ } while (evalRequest != null);
+ result = evalResult;
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Compiles the given script.
+ */
+ public void compileScript(String url, String text) {
+ DimIProxy action = new DimIProxy(this, IPROXY_COMPILE_SCRIPT);
+ action.url = url;
+ action.text = text;
+ action.withContext();
+ }
+
+ /**
+ * Evaluates the given script.
+ */
+ public void evalScript(final String url, final String text) {
+ DimIProxy action = new DimIProxy(this, IPROXY_EVAL_SCRIPT);
+ action.url = url;
+ action.text = text;
+ action.withContext();
+ }
+
+ /**
+ * Converts the given script object to a string.
+ */
+ public String objectToString(Object object) {
+ DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_TO_STRING);
+ action.object = object;
+ action.withContext();
+ return action.stringResult;
+ }
+
+ /**
+ * Returns whether the given string is syntactically valid script.
+ */
+ public boolean stringIsCompilableUnit(String str) {
+ DimIProxy action = new DimIProxy(this, IPROXY_STRING_IS_COMPILABLE);
+ action.text = str;
+ action.withContext();
+ return action.booleanResult;
+ }
+
+ /**
+ * Returns the value of a property on the given script object.
+ */
+ public Object getObjectProperty(Object object, Object id) {
+ DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_PROPERTY);
+ action.object = object;
+ action.id = id;
+ action.withContext();
+ return action.objectResult;
+ }
+
+ /**
+ * Returns an array of the property names on the given script object.
+ */
+ public Object[] getObjectIds(Object object) {
+ DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_IDS);
+ action.object = object;
+ action.withContext();
+ return action.objectArrayResult;
+ }
+
+ /**
+ * Returns the value of a property on the given script object.
+ */
+ private Object getObjectPropertyImpl(Context cx, Object object,
+ Object id) {
+ Scriptable scriptable = (Scriptable) object;
+ Object result;
+ if (id instanceof String) {
+ String name = (String) id;
+ if (name.equals("this")) {
+ result = scriptable;
+ } else if (name.equals("__proto__")) {
+ result = scriptable.getPrototype();
+ } else if (name.equals("__parent__")) {
+ result = scriptable.getParentScope();
+ } else {
+ result = ScriptableObject.getProperty(scriptable, name);
+ if (result == ScriptableObject.NOT_FOUND) {
+ result = Undefined.instance;
+ }
+ }
+ } else {
+ int index = ((Integer) id).intValue();
+ result = ScriptableObject.getProperty(scriptable, index);
+ if (result == ScriptableObject.NOT_FOUND) {
+ result = Undefined.instance;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns an array of the property names on the given script object.
+ */
+ private Object[] getObjectIdsImpl(Context cx, Object object) {
+ if (!(object instanceof Scriptable) || object == Undefined.instance) {
+ return Context.emptyArgs;
+ }
+
+ Object[] ids;
+ Scriptable scriptable = (Scriptable) object;
+ if (scriptable instanceof DebuggableObject) {
+ ids = ((DebuggableObject) scriptable).getAllIds();
+ } else {
+ ids = scriptable.getIds();
+ }
+
+ Scriptable proto = scriptable.getPrototype();
+ Scriptable parent = scriptable.getParentScope();
+ int extra = 0;
+ if (proto != null) {
+ ++extra;
+ }
+ if (parent != null) {
+ ++extra;
+ }
+ if (extra != 0) {
+ Object[] tmp = new Object[extra + ids.length];
+ System.arraycopy(ids, 0, tmp, extra, ids.length);
+ ids = tmp;
+ extra = 0;
+ if (proto != null) {
+ ids[extra++] = "__proto__";
+ }
+ if (parent != null) {
+ ids[extra++] = "__parent__";
+ }
+ }
+
+ return ids;
+ }
+
+ /**
+ * Interrupts script execution.
+ */
+ private void interrupted(Context cx, final StackFrame frame,
+ Throwable scriptException) {
+ ContextData contextData = frame.contextData();
+ boolean eventThreadFlag = callback.isGuiEventThread();
+ contextData.eventThreadFlag = eventThreadFlag;
+
+ boolean recursiveEventThreadCall = false;
+
+ interruptedCheck:
+ synchronized (eventThreadMonitor) {
+ if (eventThreadFlag) {
+ if (interruptedContextData != null) {
+ recursiveEventThreadCall = true;
+ break interruptedCheck;
+ }
+ } else {
+ while (interruptedContextData != null) {
+ try {
+ eventThreadMonitor.wait();
+ } catch (InterruptedException exc) {
+ return;
+ }
+ }
+ }
+ interruptedContextData = contextData;
+ }
+
+ if (recursiveEventThreadCall) {
+ // XXX: For now the following is commented out as on Linux
+ // too deep recursion of dispatchNextGuiEvent causes GUI lockout.
+ // Note: it can make GUI unresponsive if long-running script
+ // will be called on GUI thread while processing another interrupt
+ if (false) {
+ // Run event dispatch until gui sets a flag to exit the initial
+ // call to interrupted.
+ while (this.returnValue == -1) {
+ try {
+ callback.dispatchNextGuiEvent();
+ } catch (InterruptedException exc) {
+ }
+ }
+ }
+ return;
+ }
+
+ if (interruptedContextData == null) Kit.codeBug();
+
+ try {
+ do {
+ int frameCount = contextData.frameCount();
+ this.frameIndex = frameCount - 1;
+
+ final String threadTitle = Thread.currentThread().toString();
+ final String alertMessage;
+ if (scriptException == null) {
+ alertMessage = null;
+ } else {
+ alertMessage = scriptException.toString();
+ }
+
+ int returnValue = -1;
+ if (!eventThreadFlag) {
+ synchronized (monitor) {
+ if (insideInterruptLoop) Kit.codeBug();
+ this.insideInterruptLoop = true;
+ this.evalRequest = null;
+ this.returnValue = -1;
+ callback.enterInterrupt(frame, threadTitle,
+ alertMessage);
+ try {
+ for (; ; ) {
+ try {
+ monitor.wait();
+ } catch (InterruptedException exc) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ if (evalRequest != null) {
+ this.evalResult = null;
+ try {
+ evalResult = do_eval(cx, evalFrame,
+ evalRequest);
+ } finally {
+ evalRequest = null;
+ evalFrame = null;
+ monitor.notify();
+ }
+ continue;
+ }
+ if (this.returnValue != -1) {
+ returnValue = this.returnValue;
+ break;
+ }
+ }
+ } finally {
+ insideInterruptLoop = false;
+ }
+ }
+ } else {
+ this.returnValue = -1;
+ callback.enterInterrupt(frame, threadTitle, alertMessage);
+ while (this.returnValue == -1) {
+ try {
+ callback.dispatchNextGuiEvent();
+ } catch (InterruptedException exc) {
+ }
+ }
+ returnValue = this.returnValue;
+ }
+ switch (returnValue) {
+ case STEP_OVER:
+ contextData.breakNextLine = true;
+ contextData.stopAtFrameDepth = contextData.frameCount();
+ break;
+ case STEP_INTO:
+ contextData.breakNextLine = true;
+ contextData.stopAtFrameDepth = -1;
+ break;
+ case STEP_OUT:
+ if (contextData.frameCount() > 1) {
+ contextData.breakNextLine = true;
+ contextData.stopAtFrameDepth
+ = contextData.frameCount() - 1;
+ }
+ break;
+ }
+ } while (false);
+ } finally {
+ synchronized (eventThreadMonitor) {
+ interruptedContextData = null;
+ eventThreadMonitor.notifyAll();
+ }
+ }
+
+ }
+
+ /**
+ * Evaluates script in the given stack frame.
+ */
+ private static String do_eval(Context cx, StackFrame frame, String expr) {
+ String resultString;
+ Debugger saved_debugger = cx.getDebugger();
+ Object saved_data = cx.getDebuggerContextData();
+ int saved_level = cx.getOptimizationLevel();
+
+ cx.setDebugger(null, null);
+ cx.setOptimizationLevel(-1);
+ cx.setGeneratingDebug(false);
+ try {
+ Callable script = (Callable) cx.compileString(expr, "", 0, null);
+ Object result = script.call(cx, frame.scope, frame.thisObj,
+ ScriptRuntime.emptyArgs);
+ if (result == Undefined.instance) {
+ resultString = "";
+ } else {
+ resultString = ScriptRuntime.toString(result);
+ }
+ } catch (Exception exc) {
+ resultString = exc.getMessage();
+ } finally {
+ cx.setGeneratingDebug(true);
+ cx.setOptimizationLevel(saved_level);
+ cx.setDebugger(saved_debugger, saved_data);
+ }
+ if (resultString == null) {
+ resultString = "null";
+ }
+ return resultString;
+ }
+
+ /**
+ * Proxy class to implement debug interfaces without bloat of class
+ * files.
+ */
+ private class DimIProxy
+ implements ContextAction, ScriptEngineManager.EngineLifecycleCallback, Debugger {
+
+ /**
+ * The debugger.
+ */
+ private Dim dim;
+
+ /**
+ * The interface implementation type. One of the IPROXY_* constants
+ * defined in {@link Dim}.
+ */
+ private int type;
+
+ /**
+ * The URL origin of the script to compile or evaluate.
+ */
+ private String url;
+
+ /**
+ * The text of the script to compile, evaluate or test for compilation.
+ */
+ private String text;
+
+ /**
+ * The object to convert, get a property from or enumerate.
+ */
+ private Object object;
+
+ /**
+ * The property to look up in {@link #object}.
+ */
+ private Object id;
+
+ /**
+ * The boolean result of the action.
+ */
+ private boolean booleanResult;
+
+ /**
+ * The String result of the action.
+ */
+ private String stringResult;
+
+ /**
+ * The Object result of the action.
+ */
+ private Object objectResult;
+
+ /**
+ * The Object[] result of the action.
+ */
+ private Object[] objectArrayResult;
+
+ /**
+ * Creates a new DimIProxy.
+ */
+ private DimIProxy(Dim dim, int type) {
+ this.dim = dim;
+ this.type = type;
+ }
+
+ // ContextAction
+
+ /**
+ * Performs the action given by {@link #type}.
+ */
+ public Object run(Context cx) {
+ switch (type) {
+ case IPROXY_COMPILE_SCRIPT:
+ cx.compileString(text, url, 1, null);
+ break;
+
+ case IPROXY_EVAL_SCRIPT: {
+ Scriptable scope = null;
+ if (dim.scopeProvider != null) {
+ scope = dim.scopeProvider.getScope();
+ }
+ if (scope == null) {
+ scope = new ImporterTopLevel(cx);
+ }
+ cx.evaluateString(scope, text, url, 1, null);
+ }
+ break;
+
+ case IPROXY_STRING_IS_COMPILABLE:
+ booleanResult = cx.stringIsCompilableUnit(text);
+ break;
+
+ case IPROXY_OBJECT_TO_STRING:
+ if (object == Undefined.instance) {
+ stringResult = "undefined";
+ } else if (object == null) {
+ stringResult = "null";
+ } else if (object instanceof NativeCall) {
+ stringResult = "[object Call]";
+ } else {
+ stringResult = Context.toString(object);
+ }
+ break;
+
+ case IPROXY_OBJECT_PROPERTY:
+ objectResult = dim.getObjectPropertyImpl(cx, object, id);
+ break;
+
+ case IPROXY_OBJECT_IDS:
+ objectArrayResult = dim.getObjectIdsImpl(cx, object);
+ break;
+
+ default:
+ throw Kit.codeBug();
+ }
+ return null;
+ }
+
+ /**
+ * Performs the action given by {@link #type} with the attached
+ * {@link ContextFactory}.
+ */
+ private void withContext() {
+ dim.contextFactory.call(this);
+ }
+
+ @Override
+ public void onEngineCreate(ScriptEngine engine) {
+ if (type != IPROXY_LISTEN) Kit.codeBug();
+ if (!(engine instanceof RhinoJavaScriptEngine) ||
+ !callback.shouldAttachDebugger((RhinoJavaScriptEngine) engine)) {
+ return;
+ }
+
+ Context cx = ((RhinoJavaScriptEngine) engine).getContext();
+ ContextData contextData = new ContextData();
+ Debugger debugger = new DimIProxy(dim, IPROXY_DEBUG);
+ cx.setDebugger(debugger, contextData);
+ cx.setGeneratingDebug(true);
+ cx.setOptimizationLevel(-1);
+ engine.setTag(TAG, Dim.this);
+ }
+
+ @Override
+ public void onEngineRemove(ScriptEngine engine) {
+ if (type != IPROXY_LISTEN) Kit.codeBug();
+ }
+
+ // Debugger
+
+ /**
+ * Returns a StackFrame for the given function or script.
+ */
+ public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) {
+ if (type != IPROXY_DEBUG) Kit.codeBug();
+
+ FunctionSource item = dim.getFunctionSource(fnOrScript);
+ if (item == null) {
+ // Can not debug if source is not available
+ return null;
+ }
+ return new StackFrame(cx, dim, item);
+ }
+
+ /**
+ * Called when compilation is finished.
+ */
+ public void handleCompilationDone(Context cx,
+ DebuggableScript fnOrScript,
+ String source) {
+ if (type != IPROXY_DEBUG) Kit.codeBug();
+
+ if (!fnOrScript.isTopLevel()) {
+ return;
+ }
+ dim.registerTopScript(fnOrScript, source);
+ }
+ }
+
+ /**
+ * Class to store information about a stack.
+ */
+ public static class ContextData {
+
+ /**
+ * The stack frames.
+ */
+ private ObjArray frameStack = new ObjArray();
+
+ /**
+ * Whether the debugger should break at the next line in this context.
+ */
+ private boolean breakNextLine;
+
+ /**
+ * The frame depth the debugger should stop at. Used to implement
+ * "step over" and "step out".
+ */
+ private int stopAtFrameDepth = -1;
+
+ /**
+ * Whether this context is in the event thread.
+ */
+ private boolean eventThreadFlag;
+
+ /**
+ * The last exception that was processed.
+ */
+ private Throwable lastProcessedException;
+
+ /**
+ * Returns the ContextData for the given Context.
+ */
+ public static ContextData get(Context cx) {
+ return (ContextData) cx.getDebuggerContextData();
+ }
+
+ /**
+ * Returns the number of stack frames.
+ */
+ public int frameCount() {
+ return frameStack.size();
+ }
+
+ /**
+ * Returns the stack frame with the given index.
+ */
+ public StackFrame getFrame(int frameNumber) {
+ int num = frameStack.size() - frameNumber - 1;
+ return (StackFrame) frameStack.get(num);
+ }
+
+ /**
+ * Pushes a stack frame on to the stack.
+ */
+ private void pushFrame(StackFrame frame) {
+ frameStack.push(frame);
+ }
+
+ /**
+ * Pops a stack frame from the stack.
+ */
+ private void popFrame() {
+ frameStack.pop();
+ }
+ }
+
+ /**
+ * Object to represent one stack frame.
+ */
+ public static class StackFrame implements DebugFrame {
+
+ /**
+ * The debugger.
+ */
+ private Dim dim;
+
+ /**
+ * The ContextData for the Context being debugged.
+ */
+ private ContextData contextData;
+
+ /**
+ * The scope.
+ */
+ private Scriptable scope;
+
+ /**
+ * The 'this' object.
+ */
+ private Scriptable thisObj;
+
+ /**
+ * Information about the function.
+ */
+ private FunctionSource fsource;
+
+ /**
+ * Array of breakpoint state for each source line.
+ */
+ private boolean[] breakpoints;
+
+ /**
+ * Current line number.
+ */
+ private int lineNumber;
+
+ /**
+ * Creates a new StackFrame.
+ */
+ private StackFrame(Context cx, Dim dim, FunctionSource fsource) {
+ this.dim = dim;
+ this.contextData = ContextData.get(cx);
+ this.fsource = fsource;
+ this.breakpoints = fsource.sourceInfo().breakpoints;
+ this.lineNumber = fsource.firstLine();
+ }
+
+ /**
+ * Called when the stack frame is entered.
+ */
+ public void onEnter(Context cx, Scriptable scope,
+ Scriptable thisObj, Object[] args) {
+ contextData.pushFrame(this);
+ this.scope = scope;
+ this.thisObj = thisObj;
+ if (dim.breakOnEnter) {
+ dim.handleBreakpointHit(this, cx);
+ }
+ }
+
+ /**
+ * Called when the current position has changed.
+ */
+ public void onLineChange(Context cx, int lineno) {
+ this.lineNumber = lineno;
+
+ if (!breakpoints[lineno] && !dim.breakFlag) {
+ boolean lineBreak = contextData.breakNextLine;
+ if (lineBreak && contextData.stopAtFrameDepth >= 0) {
+ lineBreak = (contextData.frameCount()
+ <= contextData.stopAtFrameDepth);
+ }
+ if (!lineBreak) {
+ return;
+ }
+ contextData.stopAtFrameDepth = -1;
+ contextData.breakNextLine = false;
+ }
+
+ dim.handleBreakpointHit(this, cx);
+ }
+
+ /**
+ * Called when an exception has been thrown.
+ */
+ public void onExceptionThrown(Context cx, Throwable exception) {
+ dim.handleExceptionThrown(cx, exception, this);
+ }
+
+ /**
+ * Called when the stack frame has been left.
+ */
+ public void onExit(Context cx, boolean byThrow,
+ Object resultOrException) {
+ if (dim.breakOnReturn && !byThrow) {
+ dim.handleBreakpointHit(this, cx);
+ }
+ contextData.popFrame();
+ }
+
+ /**
+ * Called when a 'debugger' statement is executed.
+ */
+ public void onDebuggerStatement(Context cx) {
+ dim.handleBreakpointHit(this, cx);
+ }
+
+ /**
+ * Returns the SourceInfo object for the function.
+ */
+ public SourceInfo sourceInfo() {
+ return fsource.sourceInfo();
+ }
+
+ /**
+ * Returns the ContextData object for the Context.
+ */
+ public ContextData contextData() {
+ return contextData;
+ }
+
+ /**
+ * Returns the scope object for this frame.
+ */
+ public Object scope() {
+ return scope;
+ }
+
+ /**
+ * Returns the 'this' object for this frame.
+ */
+ public Object thisObj() {
+ return thisObj;
+ }
+
+ /**
+ * Returns the source URL.
+ */
+ public String getUrl() {
+ return fsource.sourceInfo().url();
+ }
+
+ /**
+ * Returns the current line number.
+ */
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ /**
+ * Returns the current function name.
+ */
+ public String getFunctionName() {
+ return fsource.name();
+ }
+ }
+
+ /**
+ * Class to store information about a function.
+ */
+ public static class FunctionSource {
+
+ /**
+ * Information about the source of the function.
+ */
+ private SourceInfo sourceInfo;
+
+ /**
+ * Line number of the first line of the function.
+ */
+ private int firstLine;
+
+ /**
+ * The function name.
+ */
+ private String name;
+
+ /**
+ * Creates a new FunctionSource.
+ */
+ private FunctionSource(SourceInfo sourceInfo, int firstLine,
+ String name) {
+ if (name == null) throw new IllegalArgumentException();
+ this.sourceInfo = sourceInfo;
+ this.firstLine = firstLine;
+ this.name = name;
+ }
+
+ /**
+ * Returns the SourceInfo object that describes the source of the
+ * function.
+ */
+ public SourceInfo sourceInfo() {
+ return sourceInfo;
+ }
+
+ /**
+ * Returns the line number of the first line of the function.
+ */
+ public int firstLine() {
+ return firstLine;
+ }
+
+ /**
+ * Returns the name of the function.
+ */
+ public String name() {
+ return name;
+ }
+ }
+
+ /**
+ * Class to store information about a script source.
+ */
+ public static class SourceInfo {
+
+ /**
+ * An empty array of booleans.
+ */
+ private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0];
+
+ /**
+ * The script.
+ */
+ private String source;
+
+ /**
+ * The URL of the script.
+ */
+ private String url;
+
+ /**
+ * Array indicating which lines can have breakpoints set.
+ */
+ private boolean[] breakableLines;
+
+ /**
+ * Array indicating whether a breakpoint is set on the line.
+ */
+ private boolean[] breakpoints;
+
+ /**
+ * Array of FunctionSource objects for the functions in the script.
+ */
+ private FunctionSource[] functionSources;
+
+ /**
+ * Creates a new SourceInfo object.
+ */
+ private SourceInfo(String source, DebuggableScript[] functions,
+ String normilizedUrl) {
+ this.source = source;
+ this.url = normilizedUrl;
+
+ int N = functions.length;
+ int[][] lineArrays = new int[N][];
+ for (int i = 0; i != N; ++i) {
+ lineArrays[i] = functions[i].getLineNumbers();
+ }
+
+ int minAll = 0, maxAll = -1;
+ int[] firstLines = new int[N];
+ for (int i = 0; i != N; ++i) {
+ int[] lines = lineArrays[i];
+ if (lines == null || lines.length == 0) {
+ firstLines[i] = -1;
+ } else {
+ int min, max;
+ min = max = lines[0];
+ for (int j = 1; j != lines.length; ++j) {
+ int line = lines[j];
+ if (line < min) {
+ min = line;
+ } else if (line > max) {
+ max = line;
+ }
+ }
+ firstLines[i] = min;
+ if (minAll > maxAll) {
+ minAll = min;
+ maxAll = max;
+ } else {
+ if (min < minAll) {
+ minAll = min;
+ }
+ if (max > maxAll) {
+ maxAll = max;
+ }
+ }
+ }
+ }
+
+ if (minAll > maxAll) {
+ // No line information
+ this.breakableLines = EMPTY_BOOLEAN_ARRAY;
+ this.breakpoints = EMPTY_BOOLEAN_ARRAY;
+ } else {
+ if (minAll < 0) {
+ // Line numbers can not be negative
+ throw new IllegalStateException(String.valueOf(minAll));
+ }
+ int linesTop = maxAll + 1;
+ this.breakableLines = new boolean[linesTop];
+ this.breakpoints = new boolean[linesTop];
+ for (int i = 0; i != N; ++i) {
+ int[] lines = lineArrays[i];
+ if (lines != null && lines.length != 0) {
+ for (int j = 0; j != lines.length; ++j) {
+ int line = lines[j];
+ this.breakableLines[line] = true;
+ }
+ }
+ }
+ }
+ this.functionSources = new FunctionSource[N];
+ for (int i = 0; i != N; ++i) {
+ String name = functions[i].getFunctionName();
+ if (name == null) {
+ name = "";
+ }
+ this.functionSources[i]
+ = new FunctionSource(this, firstLines[i], name);
+ }
+ }
+
+ /**
+ * Returns the source text.
+ */
+ public String source() {
+ return this.source;
+ }
+
+ /**
+ * Returns the script's origin URL.
+ */
+ public String url() {
+ return this.url;
+ }
+
+ /**
+ * Returns the number of FunctionSource objects stored in this object.
+ */
+ public int functionSourcesTop() {
+ return functionSources.length;
+ }
+
+ /**
+ * Returns the FunctionSource object with the given index.
+ */
+ public FunctionSource functionSource(int i) {
+ return functionSources[i];
+ }
+
+ /**
+ * Copies the breakpoints from the given SourceInfo object into this
+ * one.
+ */
+ private void copyBreakpointsFrom(SourceInfo old) {
+ int end = old.breakpoints.length;
+ if (end > this.breakpoints.length) {
+ end = this.breakpoints.length;
+ }
+ for (int line = 0; line != end; ++line) {
+ if (old.breakpoints[line]) {
+ this.breakpoints[line] = true;
+ }
+ }
+ }
+
+ /**
+ * Returns whether the given line number can have a breakpoint set on
+ * it.
+ */
+ public boolean breakableLine(int line) {
+ return (line < this.breakableLines.length)
+ && this.breakableLines[line];
+ }
+
+ /**
+ * Returns whether there is a breakpoint set on the given line.
+ */
+ public boolean breakpoint(int line) {
+ if (!breakableLine(line)) {
+ throw new IllegalArgumentException(String.valueOf(line));
+ }
+ return line < this.breakpoints.length && this.breakpoints[line];
+ }
+
+ /**
+ * Sets or clears the breakpoint flag for the given line.
+ */
+ public boolean breakpoint(int line, boolean value) {
+ if (!breakableLine(line)) {
+ throw new IllegalArgumentException(String.valueOf(line));
+ }
+ boolean changed;
+ synchronized (breakpoints) {
+ if (breakpoints[line] != value) {
+ breakpoints[line] = value;
+ changed = true;
+ } else {
+ changed = false;
+ }
+ }
+ return changed;
+ }
+
+ /**
+ * Removes all breakpoints from the script.
+ */
+ public void removeAllBreakpoints() {
+ synchronized (breakpoints) {
+ for (int line = 0; line != breakpoints.length; ++line) {
+ breakpoints[line] = false;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java
index b91b0157..b90069d5 100644
--- a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java
+++ b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java
@@ -372,7 +372,7 @@ public class ScriptRuntime {
try {
events.emit("exit");
} catch (Exception ignored) {
- console.error("exception on exit: " + ignored);
+ console.error("exception on exit: " + ScriptEngineService.getScriptTrace(ignored));
}
ignoresException(threads::shutDownAll);
ignoresException(events::recycle);
diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java b/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java
index e9022c46..4d3774d5 100644
--- a/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java
+++ b/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java
@@ -3,11 +3,14 @@ package com.stardust.autojs.runtime.exception;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.RhinoException;
+import java.util.concurrent.atomic.AtomicInteger;
+
/**
* Created by Stardust on 2017/1/29.
*/
public class ScriptException extends RuntimeException {
+
public ScriptException(String message, Throwable cause) {
super(message, cause);
}
diff --git a/common/release/output.json b/common/release/output.json
index fc21bded..8103d30f 100644
--- a/common/release/output.json
+++ b/common/release/output.json
@@ -1 +1 @@
-[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":401},"path":"commonRelease-4.0.0 Alpha1.apk","properties":{"packageId":"org.autojs.autojs","split":"","minSdkVersion":"17"}}]
\ No newline at end of file
+[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":408},"path":"commonRelease-4.0.2 Alpha3.apk","properties":{"packageId":"org.autojs.autojs","split":"","minSdkVersion":"17"}}]
\ No newline at end of file
diff --git a/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java b/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java
index cc01165d..fd931d09 100644
--- a/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java
+++ b/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java
@@ -10,6 +10,7 @@ import com.stardust.auojs.inrt.BuildConfig;
import com.stardust.auojs.inrt.LogActivity;
import com.stardust.auojs.inrt.Pref;
import com.stardust.auojs.inrt.autojs.AutoJs;
+import com.stardust.autojs.ScriptEngineService;
import com.stardust.autojs.execution.ExecutionConfig;
import com.stardust.autojs.execution.ScriptExecution;
import com.stardust.autojs.project.ProjectConfig;
@@ -73,7 +74,7 @@ public class AssetsProjectLauncher {
mScriptExecution = AutoJs.getInstance().getScriptEngineService().execute(source, new ExecutionConfig()
.executePath(mProjectDir));
} catch (Exception e) {
- AutoJs.getInstance().getGlobalConsole().error(e);
+ AutoJs.getInstance().getGlobalConsole().error(ScriptEngineService.getScriptTrace(e));
}
}