package com.mobilesorcery.sdk.html5.debug; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.model.IThread; import org.eclipse.wst.jsdt.debug.core.jsdi.BooleanValue; import org.eclipse.wst.jsdt.debug.core.jsdi.NullValue; import org.eclipse.wst.jsdt.debug.core.jsdi.NumberValue; import org.eclipse.wst.jsdt.debug.core.jsdi.ScriptReference; import org.eclipse.wst.jsdt.debug.core.jsdi.StringValue; import org.eclipse.wst.jsdt.debug.core.jsdi.UndefinedValue; import org.eclipse.wst.jsdt.debug.core.jsdi.VirtualMachine; import org.eclipse.wst.jsdt.debug.core.jsdi.event.EventQueue; import org.eclipse.wst.jsdt.debug.core.jsdi.request.EventRequestManager; import org.eclipse.wst.jsdt.debug.core.model.IJavaScriptDebugTarget; import org.eclipse.wst.jsdt.debug.internal.core.model.JavaScriptThread; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import com.mobilesorcery.sdk.core.CoreMoSyncPlugin; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.MoSyncTool; import com.mobilesorcery.sdk.html5.Html5Plugin; import com.mobilesorcery.sdk.html5.debug.hotreplace.FileRedefinable; import com.mobilesorcery.sdk.html5.debug.hotreplace.FunctionRedefinable; import com.mobilesorcery.sdk.html5.debug.hotreplace.ProjectRedefinable; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadBooleanValue; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadDropToFrame; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadEventQueue; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadEventRequestManager; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadNullValue; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadNumberValue; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadStackFrame; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadStringValue; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadThreadReference; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadUndefinedValue; import com.mobilesorcery.sdk.html5.debug.jsdt.SimpleScriptReference; import com.mobilesorcery.sdk.html5.live.ILiveServerCommandListener; import com.mobilesorcery.sdk.html5.live.JSODDServer; public class ReloadVirtualMachine implements VirtualMachine, ILiveServerCommandListener { private final JSODDServer server; private HashMap<String, ReloadThreadReference> threads = new HashMap<String, ReloadThreadReference>(); private final ReloadEventRequestManager requestMgr; private ReloadEventQueue eventQueue; private final NullValue nullValue; private final ReloadUndefinedValue undefValue; private IProject project; private boolean isTerminated = false; private ProjectRedefinable baseline; private ILaunch launch; private IJavaScriptDebugTarget debugTarget; private String remoteAddr; private HashMap<Class, Boolean> redefineSupport = new HashMap<Class, Boolean>(); private boolean breakOnException; private ReloadThreadReference mainThread; private int vmId; public ReloadVirtualMachine(int port) throws Exception { // TODO: PORT server = Html5Plugin.getDefault().getReloadServer(); // By default we support function redefines. redefineSupport.put(FunctionRedefinable.class, Boolean.TRUE); redefineSupport.put(FileRedefinable.class, Html5Plugin.getDefault().shouldFetchRemotely()); requestMgr = new ReloadEventRequestManager(this); eventQueue = new ReloadEventQueue(this, requestMgr); nullValue = new ReloadNullValue(this); undefValue = new ReloadUndefinedValue(this); server.addListener(this); server.startServer(this); server.registerVM(this); } private void resetEventQueue() { if (eventQueue != null) { eventQueue.close(); List exitRequests = requestMgr.threadExitRequests(); for (Object exitRequest : exitRequests) { // Don't reactivate this yet! // eventQueue.received(ReloadEventQueue.CUSTOM_EVENT, new // ReloadThreadExitEvent(this, mainThread, null, (EventRequest) // exitRequest)); } } } @Override public void resume() { for (ReloadThreadReference thread : threads.values()) { thread.resume(); } } @Override public void suspend() { for (ReloadThreadReference thread : threads.values()) { thread.suspend(); } } public void reset(int vmId, MoSyncProject project, String remoteAddr) { this.vmId = vmId; for (ReloadThreadReference thread : threads.values()) { server.reset(thread.getSessionId()); } resetEventQueue(); this.project = project.getWrappedProject(); this.remoteAddr = remoteAddr; } public int getId() { return vmId; } @Override public synchronized void terminate() { try { for (ReloadThreadReference thread : threads.values()) { int sessionId = thread.getSessionId(); server.terminate(thread.getSessionId(), sessionId == getMainThreadId()); } server.removeListener(this); server.stopServer(this); } catch (Exception e) { CoreMoSyncPlugin.getDefault().log(e); } finally { eventQueue.close(); isTerminated = true; } } @Override public String name() { return project == null ? "On-Device Debug" : project.getName(); } @Override public String description() { return "TODO"; } @Override public String version() { String versionInfo = MoSyncTool.getDefault() .getVersionInfo(MoSyncTool.BINARY_VERSION); if (versionInfo.toLowerCase().startsWith("version")) { versionInfo = versionInfo.substring("version".length()).trim(); } return versionInfo; } @Override public List allThreads() { return new ArrayList(threads.values()); } @Override public List allScripts() { ArrayList<ScriptReference> result = new ArrayList<ScriptReference>(); // Before the project has been initialized, we just send all the scripts // in the workspace... ArrayList<IProject> projects = new ArrayList<IProject>(); if (project != null) { projects.add(project); } else { projects.addAll(Arrays.asList(ResourcesPlugin.getWorkspace() .getRoot().getProjects())); } for (IProject project : projects) { JSODDSupport jsoddSupport = Html5Plugin.getDefault() .getJSODDSupport(project); if (jsoddSupport != null) { Set<IPath> allFiles = jsoddSupport.getAllFiles(); for (IPath file : allFiles) { SimpleScriptReference ref = new SimpleScriptReference(this, file); result.add(ref); } } } return result; } public IProject getProject() { return project; } @Override public void dispose() { terminate(); } @Override public UndefinedValue mirrorOfUndefined() { return undefValue; } @Override public NullValue mirrorOfNull() { return nullValue; } @Override public BooleanValue mirrorOf(boolean bool) { return new ReloadBooleanValue(this, bool); } @Override public NumberValue mirrorOf(Number number) { return new ReloadNumberValue(this, number); } @Override public StringValue mirrorOf(String string) { return new ReloadStringValue(this, string); } @Override public EventRequestManager eventRequestManager() { return requestMgr; } @Override public EventQueue eventQueue() { return eventQueue; } /** * Clears and resets all breakpoints on target. */ public void refreshBreakpoints() { // TODO: Clear per file instead. for (ReloadThreadReference thread : threads.values()) { server.refreshBreakpoints(thread.getSessionId()); } } /** * Issues a reload request to the client. * * @param resourcePath * The resource to reload. A {@code null} value will cause a full * reload of the app. * @param resourcePath * The resource to upload, relative to the project's HTML5 * location * @param reloadHint * If applicable; whether to reload the page * @return {@code true} If this virtual machine accepted the file for * updating. */ public boolean update(IFile resource) { boolean doUpdate = resource != null && resource.getProject().equals(project); if (doUpdate) { server.update(getMainThreadId(), resource); } return doUpdate; } public void reload() { server.reload(getMainThreadId()); try { debugTarget.resume(); } catch (DebugException e) { CoreMoSyncPlugin.getDefault().log(e); } } private int getMainThreadId() { ReloadThreadReference mainThread = getMainThread(); return mainThread == null ? JSODDServer.NO_SESSION : mainThread.getSessionId(); } public ReloadThreadReference getMainThread() { return mainThread; } /** * Updates a function reference on the client. * * @param key * @param source */ public void updateFunction(String key, String source) { for (ReloadThreadReference thread : threads.values()) { server.updateFunction(thread.getSessionId(), key, source); } } @Override public void received(int sessionId, String command, JSONObject json) { // TODID -- filtering is done in the eventqueue. For now. ReloadThreadReference thread = getThread(sessionId); boolean isClientSuspend = Boolean.parseBoolean("" + json.get("suspended")); if (thread == null || thread.isSuspended() && !isClientSuspend) { return; } syncThread(thread); thread.markSuspended(true); JSONArray array = (JSONArray) json.get("stack"); ReloadStackFrame[] frames = new ReloadStackFrame[array.size()]; for (int i = 0; i < array.size(); i++) { ReloadStackFrame frame = new ReloadStackFrame(this, thread, json, i); // Stack traces are reported in the reverse order. frames[array.size() - 1 - i] = frame; } if (frames.length == 0) { frames = new ReloadStackFrame[1]; frames[0] = new ReloadStackFrame(this, thread, json, -1); } thread.setFrames(frames); // suspend(); eventQueue.received(command, json); } private void syncThread(ReloadThreadReference thread) { // Another miscommunication thing with JSDT; // sometimes the threadenterevent does not // propagate to the debug target, probably // due to a concurrency mishap. // Please note that we now open up a very small // possibility for race conditions but that is // preferable in this case. IThread[] dtThreads; try { dtThreads = debugTarget.getThreads(); } catch (DebugException e) { // Just return! return; } for (IThread dtThread : dtThreads) { JavaScriptThread jsThread = (JavaScriptThread) dtThread; if (jsThread.matches(thread)) { return; } } eventQueue.received(ReloadEventQueue.THREAD_ENTER, thread); } public LocalVariableScope getLocalVariableScope(ScriptReference ref, int line) { // TODO: Faster? if (ref instanceof SimpleScriptReference) { IFile file = ((SimpleScriptReference) ref).getFile(); JSODDSupport jsoddSupport = Html5Plugin.getDefault() .getJSODDSupport(file.getProject()); LocalVariableScope scope = jsoddSupport.getScope(file, line); if (scope != null) { return scope; } } return null; } @Override public String toString() { return MessageFormat.format("JavaScript On-Device Debug VM #{0}, main thread session {1}, all threads: {2}", vmId, getMainThread(), allThreads()); } public void setCurrentLocation(String location) { mainThread.setCurrentLocation(location); } public boolean isTerminated() { return isTerminated; } public ReloadThreadReference mainThread() { return mainThread; } public void dropToFrame(int dropToFrame) throws DebugException { IThread[] threads = getJavaScriptDebugTarget().getThreads(); for (int i = 0; i < threads.length; i++) { IThread thread = threads[i]; ReloadDropToFrame.dropToFrame(thread, dropToFrame); } } public IJavaScriptDebugTarget getJavaScriptDebugTarget() { return debugTarget; } public ProjectRedefinable getBaseline() { if (baseline == null) { JSODDSupport jsoddSupport = Html5Plugin.getDefault() .getJSODDSupport(project); if (jsoddSupport != null) { setBaseline(jsoddSupport.getBaseline()); } } return baseline; } public void setBaseline(ProjectRedefinable baseline) { this.baseline = baseline; } public void setLaunch(ILaunch launch) { this.launch = launch; } public void setDebugTarget(IJavaScriptDebugTarget debugTarget) { this.debugTarget = debugTarget; } public String getRemoteAddr() { return remoteAddr; } public boolean canRedefine(IRedefinable redefinable) { synchronized (redefineSupport) { for (Map.Entry<Class, Boolean> supportsThis : redefineSupport.entrySet()) { if (redefinable != null && supportsThis.getValue() && supportsThis.getKey().isAssignableFrom( redefinable.getClass())) { return true; } } } return false; } public void setRedefineSupport(Class redefinables, boolean hasSupport) { redefineSupport.put(redefinables, hasSupport); } public void setBreakOnException(boolean breakOnException) { this.breakOnException = breakOnException; // Just refresh all breakpoints; this one does not happen often... for (ReloadThreadReference thread : threads.values()) { server.refreshBreakpoints(thread.getSessionId()); } } public boolean getBreakOnException() { return this.breakOnException; } public ReloadThreadReference getThread(String threadId) { ReloadThreadReference thread = threads.get(threadId); return thread; } public ReloadThreadReference getThread(int sessionId) { for (ReloadThreadReference thread : threads.values()) { if (thread.getSessionId() == sessionId) { return thread; } } return null; } public ReloadThreadReference resetThread(String threadId) { ReloadThreadReference thread = getThread(threadId); if (thread == null) { thread = new ReloadThreadReference(this); thread.setSessionId(server.newUniqueId()); thread.setCurrentLocation(threadId); threads.put(threadId, thread); if (mainThread == null) { mainThread = thread; } if (CoreMoSyncPlugin.getDefault().isDebugging()) { boolean isMain = getMainThread() == thread; String mainStr = isMain ? "" : "MAIN "; CoreMoSyncPlugin.trace("Assigned id {0} to {2}thread {1}", thread.getSessionId(), thread.name(), mainStr); } eventQueue.received(ReloadEventQueue.THREAD_ENTER, thread); } else { resetThread(thread); thread.markSuspended(false, false); } return thread; } private void resetThread(ReloadThreadReference thread) { server.reset(thread.setSessionId(server.newUniqueId())); } public void killThread(int threadSessionId) { String removeThis = null; for (Map.Entry<String, ReloadThreadReference> thread : threads.entrySet()) { int id = thread.getValue().getSessionId(); if (id == threadSessionId) { removeThis = thread.getKey(); } } ReloadThreadReference thread = threads.remove(removeThis); if (thread == mainThread) { mainThread = (ReloadThreadReference) (threads.values().isEmpty() ? null : threads.values().toArray()[0]); } if (thread == null) { return; } thread.terminate(); if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("Killed thread {0}", thread.getSessionId()); } } public Object evaluate(String input) throws InterruptedException, TimeoutException { return getMainThread().evaluate(input); } public void setProject(IProject project) { this.project = project; } }