blob: 555d4a25068818a47dfafe7833d3df867943ed1e [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.model;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointListener;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.IDebugEventSetListener;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchListener;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.e4.languages.javascript.jsdi.ScriptReference;
import org.eclipse.e4.languages.javascript.jsdi.ThreadReference;
import org.eclipse.e4.languages.javascript.jsdi.VirtualMachine;
import org.eclipse.e4.languages.javascript.jsdi.event.DebuggerStatementEvent;
import org.eclipse.e4.languages.javascript.jsdi.event.Event;
import org.eclipse.e4.languages.javascript.jsdi.event.EventSet;
import org.eclipse.e4.languages.javascript.jsdi.event.ScriptLoadEvent;
import org.eclipse.e4.languages.javascript.jsdi.event.ThreadEnterEvent;
import org.eclipse.e4.languages.javascript.jsdi.event.ThreadExitEvent;
import org.eclipse.e4.languages.javascript.jsdi.event.VMDeathEvent;
import org.eclipse.e4.languages.javascript.jsdi.event.VMDisconnectEvent;
import org.eclipse.e4.languages.javascript.jsdi.request.DebuggerStatementRequest;
import org.eclipse.e4.languages.javascript.jsdi.request.ThreadEnterRequest;
import org.eclipse.e4.languages.javascript.jsdi.request.ThreadExitRequest;
import org.eclipse.osgi.util.NLS;
/**
* JavaScript debug target
*
* @since 1.0
*/
public class JSDIDebugTarget extends JSDIDebugElement implements IDebugTarget, IDebugEventSetListener, ILaunchListener, IJSDIEventListener, IBreakpointListener {
static final String DEFAULT_NAME = ModelMessages.JSDIDebugTarget_jsdi_debug_target;
private final IProcess process;
private final VirtualMachine vm;
private final ILaunch launch;
private final boolean supportsTerminate;
private final boolean supportsDisconnect;
private final String name;
private final EventDispatcher eventDispatcher;
private ArrayList threads = new ArrayList();
private ArrayList breakpoints = new ArrayList();
private boolean disconnected = false;
private boolean terminating = false;
private boolean terminated = false;
private boolean suspended = false;
private ThreadEnterRequest threadEnterRequest;
private ThreadExitRequest threadExitRequest;
private DebuggerStatementRequest debuggerStatementRequest;
/**
* Constructor
*
* @param vm
* @param process
* @param launch
* @param name
* @param supportsTerminate
* @param supportsDisconnect
*/
public JSDIDebugTarget(VirtualMachine vm, IProcess process, ILaunch launch, String name, boolean supportsTerminate, boolean supportsDisconnect) {
super(null);
this.vm = vm;
this.process = process;
this.launch = launch;
this.supportsTerminate = supportsTerminate;
this.supportsDisconnect = supportsDisconnect;
if (name != null) {
this.name = name;
} else if (vm.name() != null) {
this.name = vm.name();
} else {
this.name = DEFAULT_NAME;
}
this.eventDispatcher = new EventDispatcher(this);
// TODO: consider calling this outside of constructor
initialize();
}
/**
* Initialize any threads and breakpoints existing at the time this target has been created
*/
public synchronized void initialize() {
// perform initializations
initializeThreads();
initializeBreakpoints();
getLaunch().addDebugTarget(this);
DebugPlugin plugin = DebugPlugin.getDefault();
plugin.addDebugEventListener(this);
plugin.getLaunchManager().addLaunchListener(this);
fireCreationEvent();
// begin handling/dispatching events after the creation event is handled by all listeners
plugin.asyncExec(new Runnable() {
public void run() {
Thread t = new Thread(eventDispatcher, "JSDIDebugModel.EventDispatcher"); //$NON-NLS-1$
t.setDaemon(true);
t.start();
}
});
}
/**
* Shuts down the target
*/
public synchronized void shutdown() {
try {
if (supportsTerminate) {
terminate();
} else if (supportsDisconnect) {
disconnect();
}
} catch (DebugException e) {
e.printStackTrace();
} finally {
cleanup();
fireTerminateEvent();
}
}
/**
* Cleans up the state of the target
*/
void cleanup() {
DebugPlugin plugin = DebugPlugin.getDefault();
plugin.getLaunchManager().removeLaunchListener(this);
plugin.removeDebugEventListener(this);
try {
removeAllBreakpoints();
removeAllThreads();
} finally {
getEventDispatcher().shutdown();
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.DebugElement#getDebugTarget()
*/
public IDebugTarget getDebugTarget() {
return this;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.DebugElement#getLaunch()
*/
public ILaunch getLaunch() {
return launch;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDebugTarget#getName()
*/
public String getName() throws DebugException {
return name;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDebugTarget#getProcess()
*/
public IProcess getProcess() {
return process;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ITerminate#canTerminate()
*/
public boolean canTerminate() {
return supportsTerminate && isAvailable();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ITerminate#isTerminated()
*/
public boolean isTerminated() {
return terminated;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDisconnect#canDisconnect()
*/
public boolean canDisconnect() {
return supportsDisconnect && isAvailable();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDisconnect#isDisconnected()
*/
public boolean isDisconnected() {
return disconnected;
}
/**
* Returns all of the scripts currently loaded in the VM that have the matching name.
*
* @param name
* @return the complete list of scripts loaded in the VM that have the given name.
*/
public synchronized List allScriptsByName(String name) {
List byname = new ArrayList();
List scripts = getVM().allScripts();
ScriptReference script = null;
for (Iterator iter = scripts.iterator(); iter.hasNext();) {
script = (ScriptReference) iter.next();
if (script.sourcePath().equals(name)) {
byname.add(script);
}
}
return byname;
}
/**
* Collects all of the current threads from the {@link VirtualMachine} and adds them to the cached list
*/
private synchronized void initializeThreads() {
threadEnterRequest = vm.eventRequestManager().createThreadEnterRequest();
threadEnterRequest.setEnabled(true);
eventDispatcher.addJSDIEventListener(this, threadEnterRequest);
threadExitRequest = vm.eventRequestManager().createThreadExitRequest();
threadExitRequest.setEnabled(true);
eventDispatcher.addJSDIEventListener(this, threadExitRequest);
List allThreads = vm.allThreads();
ThreadReference threadReference = null;
for (Iterator iterator = allThreads.iterator(); iterator.hasNext();) {
threadReference = (ThreadReference) iterator.next();
createThread(threadReference, false);
}
}
/**
* Removes all threads from the target
*/
private synchronized void removeAllThreads() {
Iterator iter = getThreadIterator();
while (iter.hasNext()) {
JSDIThread thread = (JSDIThread) iter.next();
try {
thread.terminate();
} catch (DebugException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
threads.clear();
eventDispatcher.removeJSDIEventListener(threadEnterRequest);
eventDispatcher.removeJSDIEventListener(threadExitRequest);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDebugTarget#getThreads()
*/
public synchronized IThread[] getThreads() throws DebugException {
return (IThread[]) threads.toArray(new IThread[this.threads.size()]);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDebugTarget#hasThreads()
*/
public synchronized boolean hasThreads() throws DebugException {
return !threads.isEmpty();
}
/**
* Installs all JavaScript breakpoints that currently exist in the breakpoint manager
*/
synchronized void initializeBreakpoints() {
debuggerStatementRequest = vm.eventRequestManager().createDebuggerStatementRequest();
debuggerStatementRequest.setEnabled(true);
eventDispatcher.addJSDIEventListener(this, debuggerStatementRequest);
IBreakpointManager manager = DebugPlugin.getDefault().getBreakpointManager();
manager.addBreakpointListener(this);
IBreakpoint[] managerBreakpoints = manager.getBreakpoints();
for (int i = 0; i < managerBreakpoints.length; i++) {
breakpointAdded(managerBreakpoints[i]);
}
}
/**
* Removes all breakpoints from this target
*/
private synchronized void removeAllBreakpoints() {
Iterator iter = ((ArrayList) ((ArrayList) this.breakpoints).clone()).iterator();
JSDIBreakpoint breakpoint = null;
while (iter.hasNext()) {
breakpoint = (JSDIBreakpoint) iter.next();
breakpoint.removeFromTarget(this);
}
breakpoints.clear();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDebugTarget#supportsBreakpoint(org.eclipse.debug.core.model.IBreakpoint)
*/
public boolean supportsBreakpoint(IBreakpoint breakpoint) {
return JSDIDebugModel.MODEL_ID.equals(breakpoint.getModelIdentifier());
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ISuspendResume#canResume()
*/
public boolean canResume() {
if ((isSuspended() || canResumeThreads()) && isAvailable()) {
if (threads.size() == 0) {
return true;
}
Iterator iter = getThreadIterator();
while (iter.hasNext()) {
IThread thread = (IThread) iter.next();
if (thread.canResume()) {
// if at least 1 thread can resume the target can be resumed
return true;
}
}
}
return false;
}
/**
* @return true if any one of the threads in the target can be resumed, false otherwise
*/
private boolean canResumeThreads() {
Iterator iter = getThreadIterator();
IThread thread = null;
while (iter.hasNext()) {
thread = (IThread) iter.next();
if (thread.canResume()) {
return true;
}
}
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ISuspendResume#canSuspend()
*/
public boolean canSuspend() {
if (!isSuspended() && isAvailable()) {
Iterator iter = getThreadIterator();
while (iter.hasNext()) {
IThread thread = (IThread) iter.next();
if (thread.isSuspended()) {
// do not allow the target to suspend if there is already a suspended thread
return false;
}
}
return true;
}
return false;
}
/**
* Returns an iterator over the collection of threads. The returned iterator is made on a copy of the thread list so that it is thread safe. This method should always be used instead of getThreadList().iterator()
*
* @return an iterator over the collection of threads
*/
private Iterator getThreadIterator() {
List threadList;
synchronized (this.threads) {
threadList = (List) this.threads.clone();
}
return threadList.iterator();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ISuspendResume#isSuspended()
*/
public boolean isSuspended() {
return suspended;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ISuspendResume#resume()
*/
public void resume() throws DebugException {
if (!isSuspended() || !isAvailable()) {
// no-op if the target is not suspended or not ready
return;
}
// if we are resuming the target resume all of the threads before resuming the target
// this gives the threads a chance to save state, etc before the VM is resumed
Iterator iter = getThreadIterator();
JSDIThread thread = null;
while (iter.hasNext()) {
thread = (JSDIThread) iter.next();
if (thread.isSuspended()) {
thread.targetResume();
}
}
this.suspended = false;
if (vm != null) {
vm.resume();
}
fireResumeEvent(DebugEvent.CLIENT_REQUEST);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ISuspendResume#suspend()
*/
public void suspend() throws DebugException {
if (isSuspended() || !isAvailable()) {
// no-op if the target is suspended or not ready
return;
}
try {
suspended = true;
if (this.vm != null) {
this.vm.suspend();
}
// set all owned, un-suspended threads as suspended if we suspend the target
Iterator iter = getThreadIterator();
while (iter.hasNext()) {
JSDIThread thread = (JSDIThread) iter.next();
if (!thread.isSuspended()) {
thread.markSuspended();
}
}
} finally {
fireSuspendEvent(DebugEvent.CLIENT_REQUEST);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IDisconnect#disconnect()
*/
public void disconnect() throws DebugException {
if (!isAvailable()) {
// already done
return;
}
if (!supportsDisconnect) {
notSupported(NLS.bind(ModelMessages.JSDIDebugTarget_not_support_disconnect, getName()), null);
}
try {
vm.dispose();
} finally {
cleanup();
disconnected = true;
fireTerminateEvent();
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.ITerminate#terminate()
*/
public void terminate() throws DebugException {
if (!isAvailable()) {
// already done
return;
}
if (!supportsTerminate) {
notSupported(NLS.bind(ModelMessages.JSDIDebugTarget_not_support_terminate, getName()), null);
}
terminating = true;
try {
// first terminate the VM
if (vm != null) {
vm.terminate();
}
// next terminate the underlying process
if (process != null) {
process.terminate();
}
terminated = true;
} finally {
cleanup();
terminating = false;
fireTerminateEvent();
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IMemoryBlockRetrieval#getMemoryBlock(long, long)
*/
public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
notSupported(ModelMessages.JSDIDebugTarget_unsupported_operation, null);
return null;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.IMemoryBlockRetrieval#supportsStorageRetrieval()
*/
public boolean supportsStorageRetrieval() {
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.e4.languages.javascript.debug.model.JSDIDebugElement#getVM()
*/
public VirtualMachine getVM() {
return vm;
}
/**
* @return if the target is available to be disconnected or terminated
*/
boolean isAvailable() {
return !(terminated || terminating || disconnected);
}
/**
* @return the event dispatcher
*/
public EventDispatcher getEventDispatcher() {
return eventDispatcher;
}
/**
* Delegate method to create a new {@link JSDIThread} and add it to the list of threads
*
* @param thread
* the underlying {@link ThreadReference}
* @return a new {@link JSDIThread}
*/
private synchronized JSDIThread createThread(ThreadReference thread, boolean fireEvent) {
if (isDisconnected()) {
return null;
}
JSDIThread jsdiThread = findThread(thread);
if (jsdiThread != null) {
return jsdiThread;
}
jsdiThread = new JSDIThread(this, thread);
threads.add(jsdiThread);
if (fireEvent)
jsdiThread.fireCreationEvent();
return jsdiThread;
}
/**
* Terminates the given {@link ThreadReference} if the target is not disconnected
*
* @param thread
*/
private synchronized void terminateThread(ThreadReference thread) {
if (isDisconnected()) {
return;
}
JSDIThread jsdiThread = findThread(thread);
if (jsdiThread == null) {
return;
}
threads.remove(jsdiThread);
jsdiThread.markTerminated();
jsdiThread.fireTerminateEvent();
}
/**
* Finds the {@link JSDIThread} mapped to the given {@link ThreadReference}
*
* @param thread
* @return the mapped {@link JSDIThread} or <code>null</code>
*/
public synchronized JSDIThread findThread(ThreadReference thread) {
for (Iterator iterator = threads.iterator(); iterator.hasNext();) {
JSDIThread jsdiThread = (JSDIThread) iterator.next();
if (jsdiThread.matches(thread)) {
return jsdiThread;
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.model.DebugElement#getAdapter(java.lang.Class)
*/
public Object getAdapter(Class adapter) {
if (adapter == JSDIDebugTarget.class) {
return this;
}
return super.getAdapter(adapter);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.IBreakpointListener#breakpointAdded(org.eclipse.debug.core.model.IBreakpoint)
*/
public synchronized void breakpointAdded(IBreakpoint breakpoint) {
if (!isAvailable() || !supportsBreakpoint(breakpoint)) {
// no-op either not ready or we don't care about the given breakpoint
return;
}
try {
((JSDIBreakpoint) breakpoint).addToTarget(this);
synchronized (this.breakpoints) {
this.breakpoints.add(breakpoint);
}
} catch (CoreException ce) {
// TODO need logging
ce.printStackTrace();
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.IBreakpointListener#breakpointChanged(org.eclipse.debug.core.model.IBreakpoint, org.eclipse.core.resources.IMarkerDelta)
*/
public synchronized void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) {
breakpointRemoved(breakpoint, delta);
breakpointAdded(breakpoint);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.IBreakpointListener#breakpointRemoved(org.eclipse.debug.core.model.IBreakpoint, org.eclipse.core.resources.IMarkerDelta)
*/
public synchronized void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
if (!isAvailable() || !supportsBreakpoint(breakpoint)) {
// no-op either not ready or we don't care about the breakpoint
return;
}
((JSDIBreakpoint) breakpoint).removeFromTarget(this);
synchronized (this.breakpoints) {
this.breakpoints.remove(breakpoint);
}
// remove cached breakpoints from threads
if (this.threads != null) {
for (Iterator iter = this.threads.iterator(); iter.hasNext();) {
((JSDIThread) iter.next()).removeBreakpoint((JSDIBreakpoint) breakpoint);
}
}
}
/**
* Returns the live list of breakpoints currently set in this target
*
* @return the live list of breakpoints
*/
public List getBreakpoints() {
return this.breakpoints;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.debug.core.IDebugEventSetListener#handleDebugEvents(org.eclipse.debug.core.DebugEvent[])
*/
public void handleDebugEvents(DebugEvent[] events) {
if (events.length == 1) {
DebugEvent event = events[0];
if (event.getSource().equals(getProcess()) && event.getKind() == DebugEvent.TERMINATE) {
shutdown();
}
}
}
/**
* @see ILaunchListener#launchRemoved(ILaunch)
*/
public void launchRemoved(ILaunch launch) {
if (!isAvailable()) {
return;
}
if (launch.equals(getLaunch())) {
// This target has been unregistered, but it hasn't successfully terminated.
// Update internal state to reflect that it is disconnected
disconnected();
}
}
/**
* delegate to clean up if the target has been disconnected and a framework method has been called
*/
protected void disconnected() {
if (!isDisconnected()) {
disconnected = true;
shutdown();
}
}
/**
* @see ILaunchListener#launchAdded(ILaunch)
*/
public void launchAdded(ILaunch launch) {
// ignore
}
/**
* @see ILaunchListener#launchChanged(ILaunch)
*/
public void launchChanged(ILaunch launch) {
// ignore
}
/*
* (non-Javadoc)
*
* @see org.eclipse.e4.languages.javascript.debug.model.IJSDIEventListener#eventSetComplete(org.eclipse.e4.languages.javascript.jsdi.event.Event, org.eclipse.e4.languages.javascript.debug.model.JSDIDebugTarget, boolean, org.eclipse.e4.languages.javascript.jsdi.event.EventSet)
*/
public void eventSetComplete(Event event, JSDIDebugTarget target, boolean suspend, EventSet eventSet) {
// thread enter
// thread exit
// script?
if (event instanceof DebuggerStatementEvent) {
DebuggerStatementEvent debuggerStatementEvent = (DebuggerStatementEvent) event;
ThreadReference threadReference = debuggerStatementEvent.thread();
JSDIThread thread = findThread(threadReference);
thread.fireSuspendEvent(DebugEvent.BREAKPOINT);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.e4.languages.javascript.debug.model.IJSDIEventListener#handleEvent(org.eclipse.e4.languages.javascript.jsdi.event.Event, org.eclipse.e4.languages.javascript.debug.model.JSDIDebugTarget, boolean, org.eclipse.e4.languages.javascript.jsdi.event.EventSet)
*/
public synchronized boolean handleEvent(Event event, JSDIDebugTarget target, boolean suspendVote, EventSet eventSet) {
if (event instanceof ThreadEnterEvent) {
ThreadEnterEvent threadEnterEvent = (ThreadEnterEvent) event;
createThread(threadEnterEvent.thread(), true);
return false;
}
if (event instanceof ThreadExitEvent) {
ThreadExitEvent threadExitEvent = (ThreadExitEvent) event;
terminateThread(threadExitEvent.thread());
return false;
}
if (event instanceof ScriptLoadEvent) {
// TODO - handle script load event
return true;
}
if (event instanceof DebuggerStatementEvent) {
DebuggerStatementEvent debuggerStatementEvent = (DebuggerStatementEvent) event;
ThreadReference threadReference = debuggerStatementEvent.thread();
JSDIThread thread = findThread(threadReference);
thread.markSuspended();
return false;
}
// handle VM events i.e. death / disconnect
if (event instanceof VMDeathEvent) {
try {
if (!this.terminated) {
eventCleanup();
}
} finally {
shutdown();
}
return false;
}
if (event instanceof VMDisconnectEvent) {
try {
if (!this.disconnected) {
eventCleanup();
}
} finally {
shutdown();
}
return false;
}
throw new IllegalArgumentException(NLS.bind(ModelMessages.JSDIDebugTarget_recieved_unknown_event, event.toString()));
}
void eventCleanup() {
try {
cleanup();
} finally {
this.disconnected = true;
this.terminated = true;
fireTerminateEvent();
}
}
}