blob: 1fc151259e6a77f029a2521241441a19105ac3b5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009 IBM Corporation and others All rights reserved. This
* program and the accompanying materials are made available under the terms of
* the Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors: IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.e4.languages.javascript.debug.rhino;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.eclipse.e4.languages.javascript.debug.connect.SocketTransportService;
import org.eclipse.e4.languages.javascript.debug.connect.DebugRuntime;
import org.eclipse.e4.languages.javascript.debug.connect.Connection;
import org.eclipse.e4.languages.javascript.debug.connect.DisconnectException;
import org.eclipse.e4.languages.javascript.debug.connect.EventPacket;
import org.eclipse.e4.languages.javascript.debug.connect.Request;
import org.eclipse.e4.languages.javascript.debug.connect.Response;
import org.eclipse.e4.languages.javascript.debug.connect.TimeoutException;
import org.eclipse.e4.languages.javascript.debug.connect.TransportService;
import org.eclipse.e4.languages.javascript.debug.connect.TransportService.ListenerKey;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.debug.DebugFrame;
import org.mozilla.javascript.debug.DebuggableScript;
import org.mozilla.javascript.debug.Debugger;
/**
* Rhino implementation of {@link Debugger}
*
* @since 1.0
*/
public class RhinoDebugger implements Debugger, ContextFactory.Listener, Runnable {
private static final String SUSPEND = "suspend"; //$NON-NLS-1$
private static final String ADDRESS = "address"; //$NON-NLS-1$
private static final String SOCKET = "socket"; //$NON-NLS-1$
private static final String TRANSPORT = "transport"; //$NON-NLS-1$
private final Thread requestHandlerThread = new Thread(this, "RhinoDebugger - Request Handler"); //$NON-NLS-1$
private final RequestHandler requestHandler = new RequestHandler(this);
private volatile boolean shutdown = false;
private DebugRuntime runtime;
private final Map threadToThreadId = new HashMap();
private final Map threadIdToData = new HashMap();
private final Map scripts = new HashMap();
private final Map debuggableScripts = new HashMap();
private final Map breakpoints = new HashMap();
private long currentThreadId = 0L;
private long currentBreakpointId = 0L;
private long currentScriptId = 0L;
private ArrayList disabledThreads = new ArrayList();
private final TransportService transportService;
private final String address;
private boolean startSuspended;
private ListenerKey listenerKey;
private volatile Connection connection;
/**
* Constructor
*
* @param configString
*/
public RhinoDebugger(String configString) {
Map config = parseConfigString(configString);
StringBuffer buffer = new StringBuffer();
buffer.append("\nRhino attaching debugger\n"); //$NON-NLS-1$
buffer.append("Start at time: ").append(DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(Calendar.getInstance().getTime())); //$NON-NLS-1$
buffer.append("\nListening to "); //$NON-NLS-1$
String transport = (String) config.get(TRANSPORT);
if (SOCKET.equals(transport)) {
this.transportService = new SocketTransportService();
buffer.append("socket on "); //$NON-NLS-1$
} else {
// TODO NLS this
throw new IllegalArgumentException("transport: "+ transport); //$NON-NLS-1$
}
this.address = (String) config.get(ADDRESS);
buffer.append("port ").append(this.address); //$NON-NLS-1$
this.startSuspended = Boolean.valueOf((String) config.get(SUSPEND)).booleanValue();
System.err.println(buffer.toString());
}
/**
* Parses the command line configuration string
*
* @param configString
* @return the map of command line args
*/
private static Map parseConfigString(String configString) {
Map config = new HashMap();
StringTokenizer tokenizer = new StringTokenizer(configString, ","); //$NON-NLS-1$
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
int equalsIndex = token.indexOf('=');
if (equalsIndex == -1)
config.put(token, null);
else
config.put(token.substring(0, equalsIndex), token.substring(equalsIndex + 1));
}
return config;
}
/**
* Constructor
*
* @param transportService
* @param address
* @param startSuspended
*/
public RhinoDebugger(TransportService transportService, String address, boolean startSuspended) {
this.transportService = transportService;
this.address = address;
this.startSuspended = startSuspended;
try {
if (startSuspended) {
listenerKey = transportService.startListening(address);
acceptConnection(300000);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* @return true if the <code>suspend=true</code> command line argument is set
*/
public boolean isStartSuspended() {
return startSuspended;
}
/**
* Suspend the debugger waiting for a runtime to connect, polling at the given timeout interval
*
* @param timeout
* @return true when a runtime has been found
*/
public synchronized boolean suspendForRuntime(long timeout) {
while (runtime == null)
try {
wait(timeout);
} catch (InterruptedException e) {
// TODO log this
e.printStackTrace();
}
return runtime != null;
}
/**
* Starts the debugger
*/
public void start() {
try {
if (listenerKey == null) {
listenerKey = transportService.startListening(address);
}
} catch (IOException e) {
// TODO log this
e.printStackTrace();
}
requestHandlerThread.start();
}
/**
* Stops the debugger
*/
public void stop() {
shutdown = true;
try {
requestHandlerThread.interrupt();
requestHandlerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
transportService.stopListening(listenerKey);
} catch (IOException e) {
// TODO log this
e.printStackTrace();
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run() {
try {
while (!shutdown) {
try {
acceptConnection(10000);
} catch (IOException e) {
if (connection == null)
continue;
}
while (!shutdown && connection.isOpen()) {
try {
Request request = runtime.receiveRequest(1000);
Response response = requestHandler.handleRequest(request);
runtime.sendResponse(response);
} catch (TimeoutException e) {
// ignore
} catch (DisconnectException e) {
break;
}
}
closeConnection();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Close the active connection
*
* @throws IOException
*/
private void closeConnection() throws IOException {
if (connection != null) {
runtime.dispose();
setRuntime(null);
connection.close();
connection = null;
}
}
/**
* Waits for a connection for the given timeout
*
* @param timeout
* @throws IOException
*/
private void acceptConnection(long timeout) throws IOException {
if (connection == null) {
connection = transportService.accept(listenerKey, timeout, timeout);
setRuntime(new DebugRuntime(connection));
}
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.Debugger#getFrame(org.mozilla.javascript.Context, org.mozilla.javascript.debug.DebuggableScript)
*/
public DebugFrame getFrame(Context context, DebuggableScript debuggableScript) {
ScriptImpl script = getScript(debuggableScript);
ContextData contextData = (ContextData) context.getDebuggerContextData();
ThreadData thread = (ThreadData) threadIdToData.get(contextData.getThreadId());
return thread.getFrame(context, debuggableScript, script);
}
/**
* Returns the debuggable script context
*
* @param debuggableScript
* @return the debuggable script context
*/
private ScriptImpl getScript(DebuggableScript debuggableScript) {
while (!debuggableScript.isTopLevel()) {
debuggableScript = debuggableScript.getParent();
}
return (ScriptImpl) debuggableScripts.get(debuggableScript);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.Debugger#handleCompilationDone(org.mozilla.javascript.Context, org.mozilla.javascript.debug.DebuggableScript, java.lang.String)
*/
public void handleCompilationDone(Context context, DebuggableScript debuggableScript, String source) {
if (!debuggableScript.isTopLevel()) {
return;
}
Long scriptId = nextScriptId();
ScriptImpl script = new ScriptImpl(scriptId, debuggableScript, source);
scripts.put(scriptId, script);
debuggableScripts.put(debuggableScript, script);
ContextData contextData = (ContextData) context.getDebuggerContextData();
contextData.scriptLoaded(script);
}
/**
* Returns the next script id to use
*
* @return the next id
*/
private synchronized Long nextScriptId() {
return new Long(currentScriptId++);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.ContextFactory.Listener#contextCreated(org.mozilla.javascript.Context)
*/
public synchronized void contextCreated(Context context) {
Thread thread = Thread.currentThread();
if (disabledThreads.contains(thread)) {
return;
}
Long threadId = (Long) threadToThreadId.get(thread);
if (threadId == null) {
threadId = new Long(currentThreadId++);
threadToThreadId.put(thread, threadId);
}
ThreadData threadData = (ThreadData) threadIdToData.get(threadId);
if (threadData == null) {
threadData = new ThreadData(threadId, this);
threadIdToData.put(threadId, threadData);
}
threadData.contextCreated(context);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.ContextFactory.Listener#contextReleased(org.mozilla.javascript.Context)
*/
public synchronized void contextReleased(Context context) {
Thread thread = Thread.currentThread();
if (disabledThreads.contains(thread)) {
return;
}
Long threadId = (Long) threadToThreadId.get(thread);
if (threadId == null) {
return;
}
ThreadData threadData = (ThreadData) threadIdToData.get(threadId);
threadData.contextReleased(context);
if (!threadData.hasContext()) {
threadToThreadId.remove(thread);
threadIdToData.remove(threadId);
}
}
/**
* Resumes a thread with the given id for the given step type. Has no effect if no such thread exists
*
* @param threadId
* @param stepType
*/
public synchronized void resume(Long threadId, String stepType) {
ThreadData threadData = (ThreadData) threadIdToData.get(threadId);
if (threadData != null) {
threadData.resume(stepType);
}
}
/**
* Resumes all threads currently in the debugger
*/
public synchronized void resumeAll() {
for (Iterator it = threadIdToData.keySet().iterator(); it.hasNext();) {
Long threadId = (Long) it.next();
resume(threadId, null);
}
}
/**
* Suspend the thread with the given id. Has no effect if no such thread exists
*
* @param threadId
*/
public synchronized void suspend(Long threadId) {
ThreadData threadData = (ThreadData) threadIdToData.get(threadId);
if (threadData != null) {
threadData.suspend();
}
}
/**
* Suspend all threads currently in the debugger
*/
public synchronized void suspendAll() {
for (Iterator it = threadIdToData.keySet().iterator(); it.hasNext();) {
Long threadId = (Long) it.next();
suspend(threadId);
}
}
/**
* Disconnects the debugger
*/
public void disconnect() {
}
/**
* Returns all of the stack frame ids for the thread with the given id. Returns an empty list if no such thread exists, never <code>null</code>
*
* @param threadId
* @return the complete list of stack frame ids from the thread with the given id
*/
public synchronized List getFrameIds(Long threadId) {
ThreadData threadData = (ThreadData) threadIdToData.get(threadId);
if (threadData == null) {
return Collections.EMPTY_LIST;
}
return threadData.getFrameIds();
}
/**
* Returns a {@link DebugFrame} with the given id from the thread with the given thread id. Returns <code>null</code> if the no such thread exists with the given id and / or no such {@link DebugFrame} exists with the given id
*
* @param threadId
* @param frameId
* @return the {@link DebugFrame} with the given id from the thread with the given id
*/
public synchronized DebugFrameImpl getFrame(Long threadId, Long frameId) {
ThreadData threadData = (ThreadData) threadIdToData.get(threadId);
if (threadData != null) {
return threadData.getFrame(frameId);
}
return null;
}
/**
* @return the ids of all of the scripts currently known to the debugger
*/
public synchronized List getScriptIds() {
return new ArrayList(scripts.keySet());
}
/**
* Returns the script with the given id or <code>null</code> if no such script exists with the given id
*
* @param scriptId
* @return the script with the given id or <code>null</code>
*/
public synchronized ScriptImpl getScript(Long scriptId) {
return (ScriptImpl) scripts.get(scriptId);
}
/**
* @return the complete collection of breakpoints currently known to the debugger
*/
public synchronized Collection getBreakpoints() {
return breakpoints.keySet();
}
/**
* Creates a breakpoint in the script with the given id and the given breakpoint attributes. Returns the new breakpoint or <code>null</code> if no such script exists with the given id
*
* @param scriptId
* @param lineNumber
* @param functionName
* @param condition
* @param threadId
* @return the new breakpoint or <code>null</code> if no script exists with the given id
*/
public synchronized BreakpointImpl setBreakpoint(Long scriptId, Integer lineNumber, String functionName, String condition, Long threadId) {
ScriptImpl script = (ScriptImpl) scripts.get(scriptId);
if (!script.isValid(lineNumber, functionName)) {
return null;
}
BreakpointImpl breakpoint = new BreakpointImpl(nextBreakpointId(), script, lineNumber, functionName, condition, threadId);
breakpoints.put(breakpoint.getId(), breakpoint);
script.addBreakpoint(breakpoint);
return breakpoint;
}
/**
* @return the next unique breakpoint id to use
*/
private synchronized Long nextBreakpointId() {
return new Long(currentBreakpointId++);
}
/**
* Clears the breakpoint out of the cache with the given id and returns it. Returns <code>null</code> if no breakpoint exists with the given id.
*
* @param breakpointId
* @return the removed breakpoint or <code>null</code>
*/
public synchronized BreakpointImpl clearBreakpoint(Long breakpointId) {
BreakpointImpl breakpoint = (BreakpointImpl) breakpoints.remove(breakpointId);
if (breakpoint != null) {
ScriptImpl script = breakpoint.getScript();
script.removeBreakpoint(breakpoint);
}
return breakpoint;
}
/**
* Sends the given {@link EventPacket} using the underlying {@link DebugRuntime} and returns if it was sent successfully
*
* @param event
* @return true if the event was sent successfully, false otherwise
*/
public synchronized boolean sendEvent(EventPacket event) {
try {
if (runtime != null) {
runtime.sendEvent(event);
return true;
}
} catch (DisconnectException e) {
// ignore
e.printStackTrace(); // for now for debugging purposes
}
return false;
}
/**
* Sets the {@link DebugRuntime} for this debugger to use
*
* @param runtime
*/
private synchronized void setRuntime(DebugRuntime runtime) {
this.runtime = runtime;
notify();
}
/**
* Gets a breakpoint with the given id, returns <code>null</code> if no such breakpoint exists with the given id
*
* @param breakpointId
* @return the breakpoint with the given id or <code>null</code>
*/
public BreakpointImpl getBreakpoint(Long breakpointId) {
return (BreakpointImpl) breakpoints.get(breakpointId);
}
/**
* Gets the thread for the thread with the given id, returns <code>null</code> if no such thread exists with the goven id
*
* @param threadId
* @return the thread data for the thread with the given id or <code>null</code>
*/
public ThreadData getThreadData(Long threadId) {
return (ThreadData) threadIdToData.get(threadId);
}
/**
* @return the complete list of thread ids known to the debugger
*/
public List getThreadIds() {
return new ArrayList(threadIdToData.keySet());
}
/**
* Caches the current thread as disabled
*/
public void disableThread() {
disabledThreads.add(Thread.currentThread());
}
/**
* Removes the current thread as being disabled
*/
public void enableThread() {
disabledThreads.remove(Thread.currentThread());
}
}