blob: d0afacd02de4b65e038c05f097157893a8fb26bc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2011 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.wst.jsdt.debug.internal.rhino.debugger;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
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 org.eclipse.wst.jsdt.debug.internal.rhino.transport.EventPacket;
import org.eclipse.wst.jsdt.debug.internal.rhino.transport.JSONConstants;
import org.eclipse.wst.jsdt.debug.internal.rhino.transport.JSONUtil;
import org.eclipse.wst.jsdt.debug.transport.TransportService;
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}
* <br><br>
* Events fired:
* <ul>
* <li><b>Thread enter event</b> - when a new context is created see: {@link #contextCreated(Context)}</li>
* <li><b>Thread exit event</b> - when a context is exited see: {@link #contextReleased(Context)}</li>
* <li><b>VM death event</b> - if the debugger dies: this event can only be received if the underlying communication channel has not been interrupted</li>
* </ul>
* @since 1.0
*/
public class RhinoDebuggerImpl implements Debugger, ContextFactory.Listener {
public static final DebuggableScript[] NO_SCRIPTS = new DebuggableScript[0];
private static final String RHINO_SCHEME = "rhino"; //$NON-NLS-1$
private final Map threadToThreadId = new HashMap();
private final Map threadIdToData = 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();
/**
* Mapping of the URI string to the {@link ScriptSource}
*/
private HashMap/*<String, ScriptSource>*/ uriToScript = new HashMap();
/**
* Mapping of the id to the {@link ScriptSource}
*/
private HashMap/*<Long, ScriptSource>*/ idToScript = new HashMap();
private final DebugSessionManager sessionManager;
/**
* This constructor will only accept a <code>transport</code> argument
* of <code>socket</code>. I.e. <code>transport=socket</code>.<br><br>
*
* To use a differing {@link TransportService} pleas use the other constructor:
* {@link #RhinoDebugger(TransportService, String, boolean)}
*
* @param configString the configuration string, for example: <code>transport=socket,suspend=y,address=9000</code>
*/
public RhinoDebuggerImpl(String configString) {
sessionManager = DebugSessionManager.create(configString);
}
/**
* This constructor allows you to specify a custom {@link TransportService} to use other than <code>socket</code>.
*
* @param transportService the {@link TransportService} to use for debugger communication
* @param address the address to communicate on
* @param startSuspended if the debugger should wait while accepting a connection. The wait time for stating suspended is not indefinite,
* @param trace if the debugger should be in tracing mode, reporting debug statements to the console
* and is equal to 300000ms.
*/
public RhinoDebuggerImpl(TransportService transportService, String address, boolean startSuspended, boolean trace) {
sessionManager = new DebugSessionManager( transportService, address, startSuspended, trace);
}
/*
* (non-Javadoc)
*
* @see org.mozilla.javascript.debug.Debugger#getFrame(org.mozilla.javascript.Context, org.mozilla.javascript.debug.DebuggableScript)
*/
public synchronized DebugFrame getFrame(Context context, DebuggableScript debuggableScript) {
ScriptSource script = getScript(debuggableScript);
if(script != null && !script.isStdIn()) {
ContextData contextData = (ContextData) context.getDebuggerContextData();
ThreadData thread = (ThreadData) threadIdToData.get(contextData.getThreadId());
FunctionSource function = script.getFunction(debuggableScript);
return thread.getFrame(context, function, script);
}
return null;
}
/**
* Returns the root {@link ScriptSource} context
*
* @param script
* @return the root {@link ScriptSource} context
*/
private ScriptSource getScript(DebuggableScript script) {
synchronized (uriToScript) {
DebuggableScript root = script;
while (!root.isTopLevel()) {
root = root.getParent();
}
URI uri = getSourceUri(root, parseSourceProperties(root.getSourceName()));
if(uri != null) {
return (ScriptSource) uriToScript.get(uri);
}
}
return null;
}
/*
* (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 script, String source) {
if (!script.isTopLevel()) {
return;
}
Map properties = parseSourceProperties(script.getSourceName());
URI uri = getSourceUri(script, properties);
if(uri == null) {
//if the source cannot be located don't load or notify
return;
}
final ScriptSource newscript = new ScriptSource(script, source, uri, script.isGeneratedScript(), properties);
synchronized (uriToScript) {
ScriptSource old = (ScriptSource) uriToScript.remove(uri);
Long id = null;
if(old != null) {
//recycle the id for a re-loaded script
//https://bugs.eclipse.org/bugs/show_bug.cgi?id=306832
id = old.getId();
idToScript.remove(id);
newscript.setId(id);
//clean up the cache of breakpoints
old.clearBreakpoints(this);
}
else {
//a totally new script is loaded
id = scriptId();
newscript.setId(id);
}
uriToScript.put(uri, newscript);
idToScript.put(id, newscript);
}
ContextData contextData = (ContextData) context.getDebuggerContextData();
contextData.scriptLoaded(newscript);
}
/**
* Composes a {@link URI} representing the path to the source of the given script
*
* @param script the script to create a {@link URI} for
* @param properties any special properties @see {@link #parseSourceProperties(String)}
* @return the {@link URI} for the source or <code>null</code>
*/
private URI getSourceUri(DebuggableScript script, Map properties) {
String sourceName = script.getSourceName();
if(properties != null) {
String jsonName = (String) properties.get(JSONConstants.NAME);
if (jsonName != null)
sourceName = jsonName;
}
// handle null sourceName
if (sourceName == null)
return null;
// handle input from the Rhino Shell
if (sourceName.equals("<stdin>")) { //$NON-NLS-1$
sourceName = "stdin"; //$NON-NLS-1$
}
if(sourceName.equals("<command>")) { //$NON-NLS-1$
sourceName = "command"; //$NON-NLS-1$
}
else {
// try to parse it as a file
File sourceFile = new File(sourceName);
if (sourceFile.exists())
return sourceFile.toURI();
//try to just create a URI from the name
try {
return new URI(sourceName);
} catch(URISyntaxException e) {
//do nothing and fall through
}
}
//fall back to creating a rhino specific URI from the script source name as a path
try {
if (! (sourceName.charAt(0) == '/'))
sourceName = "/" + sourceName; //$NON-NLS-1$
return new URI(RHINO_SCHEME, null, sourceName, null);
} catch (URISyntaxException e) {
return null;
}
}
/**
* Returns any special properties specified in the source name or <code>null</code>
*
* @param sourceName
* @return any special properties specified in the source name or <code>null</code>
*/
Map parseSourceProperties(String sourceName) {
if (sourceName != null && sourceName.charAt(0) == '{') {
try {
Object json = JSONUtil.read(sourceName);
if (json instanceof Map) {
return (Map) json;
}
} catch (RuntimeException e) {
// ignore
}
}
return null;
}
/**
* Returns the next script id to use
*
* @return the next id
*/
synchronized Long scriptId() {
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);
sendThreadEvent(JSONConstants.ENTER, threadId);
}
threadData.contextCreated(context);
}
/**
* Sends a thread event for the given type
*
* @param type the type of event to send
* @param threadId the id of the thread the even is for
*
* @see JSONConstants#ENTER
* @see JSONConstants#EXIT
*/
private void sendThreadEvent(String type, Long threadId) {
EventPacket threadEvent = new EventPacket(JSONConstants.THREAD);
Map body = threadEvent.getBody();
body.put(JSONConstants.TYPE, type);
body.put(JSONConstants.THREAD_ID, threadId);
sendEvent(threadEvent);
}
/*
* (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);
sendThreadEvent(JSONConstants.EXIT, 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 StackFrame 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(idToScript.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 ScriptSource getScript(Long scriptId) {
return (ScriptSource) idToScript.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:
* <ul>
* <li>no such script exists with the given id</li>
* <li>the given line number is not a valid line number</li>
* </ul>
* <p>
* If a breakpoint already exists at the given location it is removed and the new breakpoint is set.
* </p>
* @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 Breakpoint setBreakpoint(Long scriptId, Integer lineNumber, String functionName, String condition, Long threadId) {
ScriptSource script = (ScriptSource) idToScript.get(scriptId);
if (script == null || !script.isValid(lineNumber, functionName)) {
return null;
}
Breakpoint newbreakpoint = new Breakpoint(nextBreakpointId(), script, lineNumber, functionName, condition, threadId);
Breakpoint oldbp = script.getBreakpoint(lineNumber, functionName);
if(oldbp != null) {
breakpoints.remove(oldbp.breakpointId);
}
breakpoints.put(newbreakpoint.breakpointId, newbreakpoint);
script.addBreakpoint(newbreakpoint);
return newbreakpoint;
}
/**
* @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 Breakpoint clearBreakpoint(Long breakpointId) {
Breakpoint breakpoint = (Breakpoint) breakpoints.remove(breakpointId);
if (breakpoint != null) {
breakpoint.delete();
}
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 boolean sendEvent(EventPacket event) {
return sessionManager.sendEvent(event);
}
/**
* 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 Breakpoint getBreakpoint(Long breakpointId) {
return (Breakpoint) breakpoints.get(breakpointId);
}
/**
* Gets the thread for the thread with the given id, returns <code>null</code> if no such thread exists with the given id
*
* @param threadId
* @return the thread data for the thread with the given id or <code>null</code>
*/
public synchronized ThreadData getThreadData(Long threadId) {
return (ThreadData) threadIdToData.get(threadId);
}
/**
* @return the complete list of thread ids known to the debugger
*/
public synchronized List getThreadIds() {
return new ArrayList(threadIdToData.keySet());
}
/**
* Caches the current thread as disabled
*/
public synchronized void disableThread() {
disabledThreads.add(Thread.currentThread());
}
/**
* Removes the current thread as being disabled
*/
public synchronized void enableThread() {
disabledThreads.remove(Thread.currentThread());
}
public void start() {
sessionManager.start(this);
}
public void stop() {
sessionManager.stop();
}
/**
* Returns if a {@link DebugSession} has successfully connected to this debugger.
*
* @return <code>true</code> if the debugger has a connected {@link DebugSession} <code>false</code> otherwise
*/
public boolean isConnected() {
return sessionManager.isConnected();
}
}