package com.mobilesorcery.sdk.html5.live; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.URLConnection; import java.net.URLDecoder; import java.nio.charset.Charset; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.TreeSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.model.IBreakpoint; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.util.thread.ExecutorThreadPool; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.wst.jsdt.debug.core.breakpoints.IJavaScriptLineBreakpoint; import org.eclipse.wst.jsdt.debug.core.breakpoints.IJavaScriptLoadBreakpoint; import org.eclipse.wst.jsdt.debug.core.jsdi.ThreadReference; import org.eclipse.wst.jsdt.debug.core.jsdi.request.StepRequest; import org.eclipse.wst.jsdt.debug.core.model.IJavaScriptDebugTarget; import org.eclipse.wst.jsdt.debug.core.model.JavaScriptDebugModel; import org.eclipse.wst.jsdt.debug.internal.core.Constants; import org.eclipse.wst.jsdt.debug.internal.core.JavaScriptDebugPlugin; import org.eclipse.wst.jsdt.debug.internal.core.breakpoints.JavaScriptExceptionBreakpoint; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import com.mobilesorcery.sdk.core.CoreMoSyncPlugin; import com.mobilesorcery.sdk.core.LineReader.ILineHandler; import com.mobilesorcery.sdk.core.MoSyncBuilder; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.Pair; import com.mobilesorcery.sdk.core.Util; import com.mobilesorcery.sdk.html5.Html5Plugin; import com.mobilesorcery.sdk.html5.debug.JSODDSupport; import com.mobilesorcery.sdk.html5.debug.RedefineException; import com.mobilesorcery.sdk.html5.debug.RedefinitionResult; import com.mobilesorcery.sdk.html5.debug.ReloadVirtualMachine; import com.mobilesorcery.sdk.html5.debug.hotreplace.ProjectRedefinable; import com.mobilesorcery.sdk.html5.debug.jsdt.JavaScriptBreakpointDesc; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadRedefiner; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadThreadReference; import com.mobilesorcery.sdk.html5.debug.jsdt.ReloadValue; import com.mobilesorcery.sdk.html5.live.JSODDServer.InternalQueues.ITimeoutListener; import com.mobilesorcery.sdk.html5.ui.AskForRedefineResolutionDialog; import com.mobilesorcery.sdk.ui.UIUtils; public class JSODDServer implements IResourceChangeListener { static class DebuggerMessage { public static final Comparator<DebuggerMessage> COMPARATOR = new Comparator<DebuggerMessage>() { @Override public int compare(DebuggerMessage o1, DebuggerMessage o2) { int result = o1.type - o2.type; if (result == 0) { return o2.messageId - o1.messageId; } return result; } }; static AtomicInteger idCounter = new AtomicInteger(0); int messageId = idCounter.incrementAndGet(); AtomicBoolean processed = new AtomicBoolean(false); int type; Object data; private long timestamp; public DebuggerMessage(int type) { this(type, null); } public DebuggerMessage(int type, Object data) { this.type = type; this.data = data; } void setOfferTimestamp(long timestamp) { this.timestamp = timestamp; } public long getOfferTimestamp() { return timestamp; } public boolean setProcessed() { return !processed.getAndSet(true); } public int getMessageId() { return messageId; } @Override public String toString() { String id = String.format((processed.get() ? "(#%d)" : "#%d"), messageId); return id + ": " + type + ", " + data; } } static class InternalQueues { interface IMessageListener { public void received(int messageId, Object data); public int getSessionId(); } interface ITimeoutListener { public void timeoutOccurred(int sessionId); } private static final int PING_INTERVAL = 8000; private static final int POISON = -1; // TODO: Slow refactoring to make this class useful private final HashMap<Integer, PriorityBlockingQueue<DebuggerMessage>> consumers = new HashMap<Integer, PriorityBlockingQueue<DebuggerMessage>>(); private final HashMap<Integer, IMessageListener> messageListeners = new HashMap<Integer, IMessageListener>(); private ITimeoutListener timeoutListener = null; private final HashMap<Integer, Long> lastHeartbeats = new HashMap<Integer, Long>(); private final HashMap<Integer, Long> takeTimestamps = new HashMap<Integer, Long>(); private final HashSet<Integer> pendingPings = new HashSet<Integer>(); private final Object queueLock = new Object(); private Timer pinger; private DebuggerMessage poison() { return new DebuggerMessage(POISON); } private DebuggerMessage ping() { return new DebuggerMessage(PING); } public DebuggerMessage take(int sessionId) throws InterruptedException { if (sessionId == NO_SESSION) { throw new IllegalStateException("No session id"); } PriorityBlockingQueue<DebuggerMessage> consumer = null; synchronized (queueLock) { consumer = consumers.get(sessionId); PriorityBlockingQueue<DebuggerMessage> newConsumer = new PriorityBlockingQueue<DebuggerMessage>( 1024, DebuggerMessage.COMPARATOR); if (consumer != null) { consumer.drainTo(newConsumer); consumer.offer(poison()); } consumer = newConsumer; consumers.put(sessionId, consumer); takeTimestamps.put(sessionId, System.currentTimeMillis()); } DebuggerMessage result = consumer.take(); synchronized (queueLock) { // takeTimestamps.remove(sessionId); } if (result.type == PING) { pendingPings.remove(sessionId); } if (result != null && result.type == POISON) { throw new InterruptedException(); } if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("TAKE: Session id {0}: {1}", sessionId, result); } return result; } public void offer(int sessionId, DebuggerMessage msg) { synchronized (queueLock) { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("{2} - OFFER: Session id {0}: {1}", sessionId, msg, new Date().toString()); CoreMoSyncPlugin.trace("CONSUMERS: {0}", consumers); } PriorityBlockingQueue<DebuggerMessage> consumer = consumers .get(sessionId); if (consumer != null) { msg.setOfferTimestamp(System.currentTimeMillis()); consumer.offer(msg); } } } private Object await(final int sessionId, DebuggerMessage msg, int timeout) throws InterruptedException, TimeoutException { final CountDownLatch cd = new CountDownLatch(1); final Object[] result = new Object[1]; IMessageListener listener = new IMessageListener() { @Override public void received(int messageId, Object data) { result[0] = data; cd.countDown(); } @Override public int getSessionId() { return sessionId; } }; setMessageListener(sessionId, msg.getMessageId(), listener); offer(sessionId, msg); try { if (!cd.await(timeout, TimeUnit.SECONDS)) { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("{2}s message timeout (#{0}, #{1})", sessionId, msg.getMessageId(), timeout); } throw new TimeoutException(); } if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace( "WAITED FOR SESSION {0} AND GOT {1}", sessionId, result[0]); } } finally { clearMessageListener(sessionId); } return result[0]; } public void setTimeoutListener(ITimeoutListener timeoutListener) { this.timeoutListener = timeoutListener; } private synchronized void setMessageListener(int sessionId, int id, IMessageListener listener) { synchronized (queueLock) { this.messageListeners.put(id, listener); } } private synchronized void clearMessageListener(int id) { synchronized (queueLock) { this.messageListeners.remove(id); } } public void setResult(int id, Object result) { IMessageListener listener; synchronized (queueLock) { listener = messageListeners.get(id); } if (listener != null) { listener.received(id, result); } } private void broadcast(DebuggerMessage msg) { synchronized (queueLock) { for (Integer sessionId : consumers.keySet()) { offer(sessionId, msg); } } } public void killSession(int sessionId) { Set<Integer> messageListenerIds = new TreeSet<Integer>(); synchronized (queueLock) { PriorityBlockingQueue<DebuggerMessage> sessionQueue = consumers .remove(sessionId); takeTimestamps.remove(sessionId); if (sessionQueue != null) { sessionQueue.offer(poison()); } messageListenerIds.addAll(messageListeners.keySet()); } for (Integer messageListenerId : messageListenerIds) { IMessageListener listener = messageListeners .get(messageListenerId); if (listener != null && listener.getSessionId() == sessionId) { setResult(messageListenerId, null); clearMessageListener(messageListenerId); } } } public void killAllSessions() { Set<Integer> consumerIds = new TreeSet<Integer>(); synchronized (queueLock) { consumerIds.addAll(consumers.keySet()); } for (Integer sessionId : consumerIds) { killSession(sessionId); } } @Override public String toString() { StringBuffer result = new StringBuffer(); synchronized (queueLock) { for (Integer sessionId : consumers.keySet()) { result.append(consumers.get(sessionId)); result.append("\n"); } } return result.toString(); } public void startPingDeamon() { pinger = new Timer(); pinger.scheduleAtFixedRate(new TimerTask() { @Override public void run() { pingAll(); } }, PING_INTERVAL, PING_INTERVAL); } public void stopPingDeamon() { if (pinger != null) { pinger.cancel(); } pinger = null; } protected void pingAll() { // This ping is there to make sure that long polling does not // timeout // on the client side. // And if the client is disconnected/does not respond we need to // handle that too. HashMap<Integer, PriorityBlockingQueue<DebuggerMessage>> consumersCopy = new HashMap<Integer, PriorityBlockingQueue<DebuggerMessage>>(); synchronized (queueLock) { consumersCopy.putAll(consumers); } for (Integer sessionId : consumersCopy.keySet()) { Long timeOfLastTake = takeTimestamps.get(sessionId); boolean isWaitingForPing = pendingPings.contains(sessionId); long now = System.currentTimeMillis(); boolean needsPing = !isWaitingForPing && timeOfLastTake != null && now - timeOfLastTake > PING_INTERVAL; Long lastHeartbeat = lastHeartbeats.get(sessionId); long elapsedSinceLastHeartbeat = now - lastHeartbeat; boolean timeoutOccured = lastHeartbeat != null && elapsedSinceLastHeartbeat > 5 * PING_INTERVAL; if (timeoutOccured && timeoutListener != null) { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("Timeout occurred for {0}, {1} since last ping", sessionId, Util.elapsedTime(elapsedSinceLastHeartbeat)); } killSession(sessionId); timeoutListener.timeoutOccurred(sessionId); } if (needsPing) { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("Ping will be sent to {0}", sessionId); } pendingPings.add(sessionId); ping(sessionId); } } } public void ping(int sessionId) { offer(sessionId, ping()); } public void heartbeat(int sessionId) { lastHeartbeats.put(sessionId, System.currentTimeMillis()); } } private final InternalQueues queues = new InternalQueues(); private static final Charset UTF8 = Charset.forName("UTF8"); public static final int NO_SESSION = -1; private static final int PING = 0; private static final int EVAL = 5; private static final int REDEFINE = 7; private static final int RELOAD = 8; private static final int BREAKPOINT = 10; private static final int REFRESH_BREAKPOINTS = 12; private static final int RESUME = 20; private static final int DROP_TO_FRAME = 25; private static final int STEP = 30; private static final int SUSPEND = 200; private static final int DISCONNECT = 500; public static final int TERMINATE = 12000; public static final int TIMEOUT = -800; public static final String TIMEOUT_ATTR = "live.timeout"; public static final int DEFAULT_TIMEOUT = 5; public static final String SESSION_ID_ATTR = "sessionId"; private class JSODDServerHandler extends AbstractHandler { private final HashMap<Object, Thread> waitThreads = new HashMap<Object, Thread>(); @Override public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res) throws IOException { try { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace( "{3}: STARTED {0} REQUEST {1} ON THREAD {2}", req .getMethod(), target, Thread .currentThread().getName(), new Date() .toString()); } boolean preflight = "OPTIONS".equals(req.getMethod()); // Heartbeat. ReloadVirtualMachine vm = getVM(req.getRemoteAddr()); ReloadThreadReference thread = getThread(req, target); if (thread != null && thread.getSessionId() != NO_SESSION) { queues.heartbeat(thread.getSessionId()); } // Preflight. configureForPreflight(req, res); // COMMANDS JSONObject command = preflight || !targetMatches(target, "/mobile/") || targetMatches(target, "/mobile/incoming") ? null : parseCommand(req); Object result = handleFetch(target, req, res); if (result == null) { result = handleCommand(target, command, req, res, preflight); } if (result == null) { result = waitForClient(target, vm, command, req, res, preflight); } if (result != null) { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("SEND ({0}): {1}", target, result); } writeResponse(result, res); } if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace( "{3}: FINISHED {0} REQUEST {1} ON THREAD {2}", req .getMethod(), target, Thread .currentThread().getName(), new Date() .toString()); } } catch (Exception e) { e.printStackTrace(); throw new IOException(e); } } private ReloadThreadReference getThread(HttpServletRequest req, String target) throws UnsupportedEncodingException { ReloadVirtualMachine vm = getVM(req.getRemoteAddr()); String threadId = extractThreadId(target); return vm == null ? null : vm.getThread(threadId); } private void writeResponse(Object obj, HttpServletResponse res) throws CoreException, IOException { int length = 0; int status = HttpServletResponse.SC_OK; InputStream contents = null; String fallbackContentType = null; if (obj instanceof JSONObject) { obj = ((JSONObject) obj).toJSONString(); fallbackContentType = "application/json;charset=utf-8"; } if (obj instanceof String) { byte[] data = ((String) obj).getBytes(UTF8); contents = new ByteArrayInputStream(data); length = data.length; } else if (obj instanceof IFile) { // We put large files in memory too. IFile file = (IFile) obj; if (!file.exists()) { // Then we fake an empty file contents = new ByteArrayInputStream(new byte[0]); length = 0; } else { contents = file.getContents(true); length = (int) file.getLocation().toFile().length(); } } else { String errorMsg; fallbackContentType = "text/plain"; if (obj instanceof Exception) { errorMsg = ((Exception) obj).getMessage(); status = HttpServletResponse.SC_NOT_FOUND; } else { errorMsg = "Internal error: wrong type of response object: " + obj; status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; } contents = new ByteArrayInputStream(errorMsg.getBytes()); length = errorMsg.getBytes().length; } res.setStatus(status); if (fallbackContentType != null) { res.setContentType(fallbackContentType); } res.setContentLength(length); ServletOutputStream output = res.getOutputStream(); Util.transfer(contents, output); output.flush(); Util.safeClose(output); Util.safeClose(contents); } private JSONObject waitForClient(String target, ReloadVirtualMachine vm, JSONObject command, HttpServletRequest req, HttpServletResponse res, boolean preflight) throws UnsupportedEncodingException, CoreException { JSONObject result = null; String threadId = extractThreadId(target); if (threadId != null && targetMatches(target, "/mobile/incoming")) { ReloadThreadReference thread = getThread(req, target); if (thread == null) { thread = vm.resetThread(threadId); } int sessionId = thread.getSessionId(); result = pushCommandsToClient(sessionId, preflight); } else if (threadId != null && targetMatches(target, "/mobile/breakpoint")) { // RACE CONDITION WILL OCCUR HERE! if (!preflight) { Integer sessionId = extractSessionId(command); queues.heartbeat(sessionId); notifyCommandListeners(sessionId, getCommand(command), command); result = pushCommandsToClient(sessionId, preflight); } else { return new JSONObject(); } } return result; } private String extractThreadId(String target) throws UnsupportedEncodingException { String[] components = target.split("/", 4); if (components.length < 4) { return null; } String threadId = components[3]; threadId = normalizeThreadId(threadId); return threadId; } private JSONObject error(String msg) { JSONObject result = new JSONObject(); result.put("errorMsg", msg); return result; } private JSONObject pushCommandsToClient(Integer session, boolean preflight) throws CoreException { if (preflight) { return new JSONObject(); } if (session == null || session == NO_SESSION) { return error("Session not initialized"); } JSONObject result = createPing(); try { DebuggerMessage queuedElement = queues.take(session); Object queuedObject = queuedElement == null ? null : queuedElement.data; int queuedType = queuedElement == null ? -1 : queuedElement.type; if (queuedType == BREAKPOINT) { Pair<Boolean, Object> bp = (Pair<Boolean, Object>) queuedObject; result = createBreakpointJSON(getVM(session), new Object[] { bp.second }, bp.first, false, false); } else if (queuedType == RESUME) { result = newCommand("breakpoint-continue"); } else if (queuedType == STEP) { result = newCommand(getStepCommand((Integer) queuedElement.data)); } else if (queuedType == RELOAD) { String command = queuedObject == null ? "reload" : "update"; result = newCommand(command); if (queuedObject != null) { IFile resource = (IFile) queuedObject; result.put("resource", Html5Plugin.getDefault().getLocalPath(resource) .toOSString()); } } else if (queuedType == SUSPEND) { result = newCommand("suspend"); } else if (queuedType == EVAL) { result = newCommand("eval"); Pair<String, Integer> data = (Pair<String, Integer>) queuedObject; String expression = data.first; Integer stackDepth = data.second; result.put("data", expression); if (stackDepth != null) { result.put("stackDepth", stackDepth); } else { result.put("noStack", true); } } else if (queuedType == REFRESH_BREAKPOINTS) { IBreakpoint[] bps = getEnabledBreakpoints(); result = createBreakpointJSON(getVM(session), bps, true, true, true); } else if (queuedType == REDEFINE) { Pair<String, String> data = (Pair<String, String>) queuedObject; result = newCommand("update-function"); JSONArray functions = new JSONArray(); JSONObject function = new JSONObject(); function.put("key", data.first); function.put("definition", data.second); functions.add(function); result.put("functions", functions); } else if (queuedType == TERMINATE) { result = newCommand("terminate"); } else if (queuedType == DISCONNECT) { result = newCommand("disconnect"); } else if (queuedType == PING) { // Just return the ping created above! } result.put("id", queuedElement.getMessageId()); } catch (InterruptedException e) { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin .trace("Dropped connection, session #{0} (often temporarily).", session); } } return result; } private JSONObject createPing() { // We use a zero-length breakpoint list as 'ping' return createBreakpointJSON(null, new Object[0], true, false, false); } private String getStepCommand(int stepType) { switch (stepType) { case StepRequest.STEP_INTO: return "break-on-next"; case StepRequest.STEP_OUT: return "breakpoint-step-out"; case StepRequest.STEP_OVER: return "breakpoint-step-over"; default: return "breakpoint-continue"; } } private void configureForPreflight(HttpServletRequest req, HttpServletResponse res) { res.setStatus(HttpServletResponse.SC_OK); res.setContentLength(0); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", req.getHeader("Access-Control-Request-Method")); res.setHeader("Access-Control-Allow-Headers", req.getHeader("Access-Control-Request-Headers")); } private JSONObject parseCommand(HttpServletRequest req) { try { JSONObject json = (JSONObject) new JSONParser() .parse(new InputStreamReader(req.getInputStream())); if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("RECEIVED JSON COMMAND: {0}", json); } return json; } catch (Exception e) { // Ignore. e.printStackTrace(); } return null; } private Object handleFetch(String target, HttpServletRequest req, HttpServletResponse res) throws IOException { if (targetMatches(target, "/mobile/")) { return null; } Object source = null; String resource = null; IProject project = null; ReloadVirtualMachine vm = getVM(req.getRemoteAddr()); // Hm, the baseurl may be either relative to the url or to the full path... // Anyway, we must init the vm at first fetch, and at that point // we also need the project name. Subsequent requests may or may not // have the fetch/%PROJECT_NAME% prefix. if (targetMatches(target, "/fetch/")) { String[] parts = target.substring("/fetch/".length()).split( "/", 2); if (parts.length == 2) { String projectName = parts[0]; resource = parts[1]; project = ResourcesPlugin.getWorkspace().getRoot() .getProject(projectName); if (project != null) { if (vm == null) { // The session id will be assigned soon. initVM(req, MoSyncProject.create(project), null); } } } } else { if (vm != null) { resource = target; project = vm.getProject(); } } if (resource == null) { return new IOException("Could not find resource " + target); } source = doFetch(project, resource); // No caching! res.setHeader("Pragma", "no-cache"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setContentType(guessContentTypeFromName(resource)); return source; } private String guessContentTypeFromName(String name) { String contentType = URLConnection.guessContentTypeFromName(name); File file = new File(name); if ("js".equals(Util.getExtension(file))) { contentType = "text/javascript"; } else if ("css".equals(Util.getExtension(file))) { contentType = "text/css"; } else if ("html".equals(Util.getExtension(file))) { contentType = "text/html"; } if (contentType == null) { contentType = "text/plain"; } return contentType; } private Object doFetch(IProject project, String localPath) throws IOException { JSODDSupport jsoddSupport = Html5Plugin.getDefault() .getJSODDSupport(project); IFile file = project.getFile(Html5Plugin .getHTML5Folder(project).append(localPath)); if (jsoddSupport.requiresFullBuild()) { return new IOException(MessageFormat.format("Project not built. Please build project {0}.", project.getName())); } String source = jsoddSupport.getInstrumentedSource(file); if (source == null) { return file; } return source; } private Object handleCommand(String target, JSONObject command, HttpServletRequest req, HttpServletResponse res, boolean preflight) throws CoreException { if (targetMatches(target, "/mobile/init")) { // Just push the breakpoints! IBreakpoint[] bps = getEnabledBreakpoints(); JSONObject jsonBps = createPing(); ReloadVirtualMachine vm = null; if (command != null) { String projectName = (String) command.get("project"); String threadId = normalizeThreadId((String) command.get("location")); MoSyncProject project = MoSyncProject .create(ResourcesPlugin.getPlugin().getWorkspace() .getRoot().getProject(projectName)); vm = initVM(req, project, threadId); ReloadThreadReference thread = vm.resetThread(threadId); jsonBps = createBreakpointJSON(vm, bps, true, true, true); jsonBps.put(SESSION_ID_ATTR, thread.getSessionId()); } return jsonBps; } else if (targetMatches(target, "/mobile/console")) { if (command != null) { if ("print-message".equals(getCommand(command))) { String level = "" + command.get("type"); String msg = "" + command.get("message"); if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("{0}: {1}", level, msg); } for (ILineHandler consoleListener : consoleListeners) { consoleListener.newLine(level + "|" + msg); } } else if ("print-eval-result".equals(getCommand(command))) { Object result = command.get("result"); if (!command.containsKey("result")) { result = ReloadValue.UNDEFINED; } int id = ((Long) command.get("id")).intValue(); queues.setResult(id, result); } } return new JSONObject(); } return null; } private boolean targetMatches(String target, String requestType) { return target != null && target.startsWith(requestType); } private String getCommand(JSONObject command) { return (String) command.get("command"); } private JSONObject createBreakpointJSON(ReloadVirtualMachine vm, Object[] bps, boolean enabled, boolean reset, boolean sendExceptionBp) { JSONObject command = new JSONObject(); command.put("command", enabled ? "set-breakpoints" : "clear-breakpoints"); if (reset) { command.put("reset", true); } JSONArray jsonBps = new JSONArray(); for (Object bp : bps) { try { if (bp instanceof IJavaScriptLineBreakpoint) { bp = toInternalFormat((IJavaScriptLineBreakpoint) bp); } if (bp instanceof JavaScriptBreakpointDesc) { JavaScriptBreakpointDesc lineBp = (JavaScriptBreakpointDesc) bp; lineBp = syncBreakpoint(lineBp); int lineNo = bp instanceof IJavaScriptLoadBreakpoint ? -1 : lineBp.getLineNumber(); IResource resource = lineBp.getResource(); String file = resource.getType() == IResource.ROOT ? "*" : resource.getFullPath().toPortableString(); String condition = lineBp.getCondition(); int hitCount = lineBp.getHitCount(); JSONObject jsonBp = new JSONObject(); jsonBp.put("file", file); JSODDSupport jsoddSupport = resource.getType() == IResource.FILE ? Html5Plugin .getDefault().getJSODDSupport( resource.getProject()) : null; int instrumentedLine = jsoddSupport == null ? lineNo : jsoddSupport.findClosestBreakpointLine( resource.getFullPath(), lineNo); if (instrumentedLine >= 0) { lineNo = instrumentedLine; } jsonBp.put("line", lineNo); if (!Util.isEmpty(condition)) { jsonBp.put("condition", condition); jsonBp.put("conditionSuspend", lineBp.getConditionSuspend()); } if (hitCount > 0) { jsonBp.put("hitcount", hitCount); } jsonBps.add(jsonBp); } } catch (Exception e) { CoreMoSyncPlugin.getDefault().log(e); } } if (vm != null && sendExceptionBp) { command.put("breakonexceptions", vm.getBreakOnException()); } command.put("data", jsonBps); return command; } private JavaScriptBreakpointDesc syncBreakpoint(JavaScriptBreakpointDesc lineBp) { IResource resource = lineBp.getResource(); if (resource != null) { IPath path = resource.getFullPath(); int lineNo = lineBp.getLineNumber(); IJavaScriptLineBreakpoint underlyingBp = JSODDSupport.findBreakPoint(path, lineNo); if (underlyingBp != null) { try { String condition = underlyingBp.isConditionEnabled() ? lineBp.getCondition() : null; String suspendStrategy = underlyingBp.isConditionSuspendOnTrue() ? JavaScriptBreakpointDesc.SUSPEND_ON_TRUE : JavaScriptBreakpointDesc.SUSPEND_ON_CHANGE; lineBp = lineBp.setCondition(condition); lineBp = lineBp.setConditionSuspend(suspendStrategy); } catch (CoreException e) { CoreMoSyncPlugin.getDefault().log(e); } } } return lineBp; } private JavaScriptBreakpointDesc toInternalFormat( IJavaScriptLineBreakpoint lineBp) throws CoreException { boolean isLoadBp = lineBp instanceof IJavaScriptLoadBreakpoint; if (lineBp.getMarker() != null && lineBp.getMarker().exists()) { int lineNumber = lineBp instanceof IJavaScriptLoadBreakpoint ? -1 : lineBp.getLineNumber(); IResource resource = lineBp.getMarker().getResource(); String condition = lineBp.getCondition(); int hitCount = lineBp.getHitCount(); String conditionSuspend = lineBp.isConditionSuspendOnTrue() ? JavaScriptBreakpointDesc.SUSPEND_ON_TRUE : JavaScriptBreakpointDesc.SUSPEND_ON_CHANGE; return new JavaScriptBreakpointDesc(resource, lineNumber, condition, conditionSuspend, hitCount); } return null; } private JSONObject newCommand(String command) { JSONObject result = new JSONObject(); result.put("command", command); return result; } } private final Object mutex = new Object(); private Server server; private final CopyOnWriteArrayList<ILiveServerListener> lifecycleListeners = new CopyOnWriteArrayList<ILiveServerListener>(); private final CopyOnWriteArrayList<ILiveServerCommandListener> commandListeners = new CopyOnWriteArrayList<ILiveServerCommandListener>(); private final CopyOnWriteArrayList<ILineHandler> consoleListeners = new CopyOnWriteArrayList<ILineHandler>(); private final AtomicInteger uniqueId = new AtomicInteger(1); private final IdentityHashMap<Object, Object> refs = new IdentityHashMap<Object, Object>(); private final ArrayList<ReloadVirtualMachine> unassignedVMs = new ArrayList<ReloadVirtualMachine>(); private final HashMap<String, ReloadVirtualMachine> vmsByHost = new HashMap<String, ReloadVirtualMachine>(); private IPreferenceChangeListener breakOnExceptionsListener; public synchronized void startServer(Object ref) throws CoreException { refs.put(ref, true); if (server != null) { return; } try { ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE); server = new Server(getPort()); server.setThreadPool(new ExecutorThreadPool(128, 128, 120)); server.setHandler(new JSODDServerHandler()); Connector connector = new SelectChannelConnector(); connector.setPort(getPort()); connector.setMaxIdleTime(120000); server.setConnectors(new Connector[] { connector }); server.start(); queues.startPingDeamon(); queues.setTimeoutListener(new ITimeoutListener() { @Override public void timeoutOccurred(int threadId) { ReloadVirtualMachine vm = getVM(threadId); if (vm != null) { vm.killThread(threadId); if (vm.allThreads().isEmpty()) { IJavaScriptDebugTarget debugTarget = vm.getJavaScriptDebugTarget(); if (debugTarget != null) { try { debugTarget.terminate(); } catch (DebugException e) { CoreMoSyncPlugin.getDefault().log(e); } } notifyTerminateListeners(vm); } } } }); if (CoreMoSyncPlugin.getDefault().isDebugging()) { InetAddress host = InetAddress.getLocalHost(); String hostName = host.getHostName(); String hostAddr = host.getHostAddress(); CoreMoSyncPlugin.trace("Started live server at {0}:{1} ({2})", hostAddr, Integer.toString(getPort()), hostName); } } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Html5Plugin.PLUGIN_ID, e.getMessage(), e)); } } public IBreakpoint[] getEnabledBreakpoints() throws CoreException { IBreakpoint[] bps = DebugPlugin.getDefault().getBreakpointManager().getBreakpoints(JavaScriptDebugModel.MODEL_ID); ArrayList<IBreakpoint> result = new ArrayList<IBreakpoint>(); for (IBreakpoint bp : bps) { if (bp.isEnabled()) { result.add(bp); } } return result.toArray(new IBreakpoint[result.size()]); } public static String normalizeThreadId(String threadId) { return URLDecoder.decode(threadId); } public static Integer extractSessionId(JSONObject command) { Object sessionIdObj = command.get(SESSION_ID_ATTR); Integer sessionId = sessionIdObj == null ? null : Integer .parseInt(sessionIdObj.toString()); return sessionId; } public synchronized ReloadThreadReference getThread(int sessionId) { if (sessionId != NO_SESSION) { for (ReloadVirtualMachine vm : vmsByHost.values()) { for (Object thread : vm.allThreads()) { if (((ReloadThreadReference) thread).getSessionId() == sessionId) { return (ReloadThreadReference) thread; } } } } return null; } public synchronized ReloadVirtualMachine getVM(int sessionId) { ReloadThreadReference thread = getThread(sessionId); return (ReloadVirtualMachine) (thread == null ? null : thread.virtualMachine()); } public synchronized List<ReloadVirtualMachine> getVMs( boolean includeUnassigned) { ArrayList<ReloadVirtualMachine> result = new ArrayList<ReloadVirtualMachine>(); for (ReloadVirtualMachine vm : vmsByHost.values()) { if (includeUnassigned || !unassignedVMs.contains(vm)) { result.add(vm); } } return result; } private ReloadVirtualMachine getVM(String remoteAddr) { ReloadVirtualMachine vm = vmsByHost.get(remoteAddr); if (vm != null && vm.isTerminated()) { vmsByHost.remove(remoteAddr); return null; } return vm; } synchronized ReloadVirtualMachine initVM(HttpServletRequest req, MoSyncProject project, String threadId) { String remoteIp = req.getRemoteAddr(); ReloadVirtualMachine vm = getVM(remoteIp); boolean resetVM = vm == null || !Util.equals(vm.getProject(), project.getWrappedProject()) || vm.getThread(threadId) != null; if (resetVM) { boolean needsNewVm = false; if (!unassignedVMs.isEmpty()) { vm = unassignedVMs.remove(0); needsNewVm = true; } int newVMId = newUniqueId(); vm.reset(newVMId, project, remoteIp); ReloadVirtualMachine oldVm = vmsByHost.put(remoteIp, vm); if (needsNewVm && oldVm != null) { terminate(oldVm, true); } notifyInitListeners(vm, !resetVM); if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace("Assigned id #{0} to vm for project {1}", newVMId, vm.getProject()); } } return vm; } public int newUniqueId() { int traceMask = CoreMoSyncPlugin.getDefault().isDebugging() ? 0xffff : 0; return traceMask + uniqueId.incrementAndGet(); } private Object awaitEvalResult(int sessionId, String expression, Integer stackDepth, int timeout) throws InterruptedException, TimeoutException { DebuggerMessage queuedExpression = new DebuggerMessage(EVAL, new Pair<String, Integer>(expression, stackDepth)); return queues.await(sessionId, queuedExpression, timeout); } public void addListener(ILiveServerListener listener) { this.lifecycleListeners.add(listener); } public void removeListener(ILiveServerListener listener) { this.lifecycleListeners.remove(listener); } public void addListener(ILiveServerCommandListener listener) { this.commandListeners.add(listener); } public void removeListener(ILiveServerCommandListener listener) { this.commandListeners.remove(listener); } private void notifyCommandListeners(Integer sessionId, String commandName, JSONObject command) { // TODO: Send directly to the proper VM instead!! for (ILiveServerCommandListener listener : commandListeners) { listener.received(sessionId, commandName, command); } } private void notifyInitListeners(ReloadVirtualMachine vm, boolean reset) { for (ILiveServerListener listener : lifecycleListeners) { listener.inited(vm, reset); } } private void notifyTerminateListeners(ReloadVirtualMachine vm) { for (ILiveServerListener listener : lifecycleListeners) { listener.timeout(vm); } } public synchronized void stopServer(Object ref) throws CoreException { ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); refs.remove(ref); if (refs.isEmpty()) { unassignedVMs.clear(); vmsByHost.clear(); queues.setTimeoutListener(null); queues.stopPingDeamon(); queues.killAllSessions(); if (server != null) { try { server.stop(); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, Html5Plugin.PLUGIN_ID, e.getMessage(), e)); } } server = null; } } public void setLineBreakpoint(boolean enabled, JavaScriptBreakpointDesc bp) { queues.broadcast(new DebuggerMessage(BREAKPOINT, new Pair<Boolean, Object>(enabled, bp))); } private int getPort() throws IOException { return Html5Plugin.getDefault().getServerURL().getPort(); } public void setDirty(Set<IProject> dirtyProjects) { } public boolean isSuspended() { return false; } public void resume(int sessionId) { queues.offer(sessionId, new DebuggerMessage(RESUME)); } public void suspend(int sessionId) { try { queues.await(sessionId, new DebuggerMessage(SUSPEND), Integer.MAX_VALUE); } catch (Exception e) { CoreMoSyncPlugin.getDefault().log(e); } } public void step(int sessionId, int stepType) { queues.offer(sessionId, new DebuggerMessage(STEP, stepType)); } public void dropToFrame(int sessionId, int stackDepth) { queues.offer(sessionId, new DebuggerMessage(DROP_TO_FRAME, stackDepth)); } public void reset(int sessionId) { queues.killSession(sessionId); } public Object evaluate(int sessionId, String expression, Integer stackDepth) throws InterruptedException, TimeoutException { Object result = awaitEvalResult(sessionId, expression, stackDepth, getTimeout(sessionId)); return result; } public void update(int sessionId, IFile resource) { queues.offer(sessionId, new DebuggerMessage(RELOAD, resource)); } public void reload(int sessionId) { queues.offer(sessionId, new DebuggerMessage(RELOAD, null)); } public void updateFunction(int sessionId, String key, String source) { queues.offer(sessionId, new DebuggerMessage(REDEFINE, new Pair<String, String>(key, source))); } public void terminate(int sessionId, boolean main) { try { ReloadVirtualMachine vm = getVM(sessionId); if (vm != null && !vm.isTerminated()) { if (main) { DebuggerMessage msg = new DebuggerMessage(TERMINATE); queues.offer(sessionId, msg); // Just to make sure the terminate request is sent // before the server is killed. Thread.sleep(1000 * getTimeout(sessionId)); } else { DebuggerMessage msg = new DebuggerMessage(DISCONNECT); queues.offer(sessionId, msg); } queues.killSession(sessionId); } } catch (Exception e) { CoreMoSyncPlugin.getDefault().log(e); } } public void disconnect(int sessionId) { try { ReloadVirtualMachine vm = getVM(sessionId); if (vm != null && !vm.isTerminated()) { DebuggerMessage msg = new DebuggerMessage(DISCONNECT); queues.offer(sessionId, msg); } } catch (Exception e) { CoreMoSyncPlugin.getDefault().log(e); } } public void refreshBreakpoints(int sessionId) { queues.offer(sessionId, new DebuggerMessage(REFRESH_BREAKPOINTS)); } private int getTimeout(int sessionId) { // TODO: Use launch config/prefs. Hm. Prefs easier. And user-friendlier // :) return Html5Plugin.getDefault().getTimeout(); } public void addConsoleListener(ILineHandler handler) { consoleListeners.add(handler); } public void removeConsoleListener(ILineHandler handler) { consoleListeners.remove(handler); } public synchronized void registerVM(ReloadVirtualMachine vm) { unassignedVMs.add(vm); } @Override public void resourceChanged(IResourceChangeEvent event) { if (Html5Plugin.getDefault().getSourceChangeStrategy() == Html5Plugin.DO_NOTHING) { // Just return! return; } List<ReloadVirtualMachine> vms = getVMs(false); final HashMap<IProject, ProjectRedefinable> replacements = new HashMap<IProject, ProjectRedefinable>(); for (ReloadVirtualMachine vm : vms) { IProject project = vm.getProject(); if (!replacements.containsKey(project)) { ProjectRedefinable replacement = vm.getBaseline().shallowCopy(); replacements.put(project, replacement); } } IResourceDelta delta = event.getDelta(); final boolean[] requiredRewrite = new boolean[] { false }; // This code seems to be repeated elsewhere; refactor! TODO! if (delta != null) { try { delta.accept(new IResourceDeltaVisitor() { @Override public boolean visit(IResourceDelta delta) throws CoreException { IResource resource = delta.getResource(); if (resource != null && (delta.getFlags() & IResourceDelta.CONTENT) != 0) { IProject project = resource.getProject(); if (project != null && resource.getType() == IResource.FILE && !MoSyncBuilder.isInOutput(project, resource)) { ProjectRedefinable replacement = replacements .get(project); if (replacement == null) { return false; } requiredRewrite[0] = true; JSODDSupport jsoddSupport = Html5Plugin .getDefault().getJSODDSupport(project); if (delta.getKind() == IResourceDelta.REMOVED) { jsoddSupport.delete(resource.getFullPath(), replacement); } else { jsoddSupport.rewrite( resource.getFullPath(), null, replacement); } } } return true; } }); } catch (Exception e) { // TODO: handle exception } } if (!requiredRewrite[0]) { return; } int failedRedefineResolution = 0; for (ReloadVirtualMachine vm : vms) { IProject project = vm.getProject(); ProjectRedefinable replacement = replacements.get(project); boolean forceReload = Html5Plugin.getDefault() .getSourceChangeStrategy() == Html5Plugin.RELOAD; ReloadRedefiner redefiner = new ReloadRedefiner(vm, forceReload); ProjectRedefinable baseline = vm.getBaseline(); boolean updateBaseline = false; try { if (baseline == null) { throw new RedefineException( RedefinitionResult .unrecoverable("Client out of sync")); } baseline.redefine(replacement, redefiner); redefiner.commit(forceReload); updateBaseline = true; } catch (RedefineException e) { if (failedRedefineResolution == 0) { failedRedefineResolution = askForRedefineResolution(e); } switch (failedRedefineResolution) { case RedefinitionResult.RELOAD: try { redefiner.commit(true); updateBaseline = true; } catch (RedefineException nestedException) { // Ignore. } break; case RedefinitionResult.TERMINATE: terminate(vm, false); break; default: // Continue/cancel; do nothing. } } if (updateBaseline) { vm.setBaseline(replacement); } } } private void terminate(ReloadVirtualMachine vm, boolean removeLaunch) { try { IJavaScriptDebugTarget debugTarget = vm.getJavaScriptDebugTarget(); debugTarget.terminate(); if (removeLaunch) { ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); manager.removeLaunch(debugTarget.getLaunch()); } } catch (DebugException debugException) { CoreMoSyncPlugin.getDefault().log(debugException); } } private int askForRedefineResolution(final RedefineException e) { final int[] reloadStrategy = new int[] { Html5Plugin.getDefault() .getReloadStrategy() }; if (reloadStrategy[0] == RedefinitionResult.UNDETERMINED) { Display d = PlatformUI.getWorkbench().getDisplay(); UIUtils.onUiThread(d, new Runnable() { @Override public void run() { Shell shell = PlatformUI.getWorkbench() .getActiveWorkbenchWindow().getShell(); reloadStrategy[0] = AskForRedefineResolutionDialog.open( shell, e); } }, false); } return reloadStrategy[0]; } public Set<Integer> getSessions() { // MOVE TO QUEUES. synchronized (queues.queueLock) { return new HashSet<Integer>(this.queues.consumers.keySet()); } } }