| /******************************************************************************* |
| * Copyright (c) 2000, 2015 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.debug.core; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.debug.core.DebugEvent; |
| import org.eclipse.debug.core.DebugException; |
| import org.eclipse.debug.core.DebugPlugin; |
| import org.eclipse.jdt.debug.core.IJavaLineBreakpoint; |
| import org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget; |
| |
| import com.sun.jdi.VMDisconnectedException; |
| import com.sun.jdi.VirtualMachine; |
| import com.sun.jdi.event.Event; |
| import com.sun.jdi.event.EventIterator; |
| import com.sun.jdi.event.EventQueue; |
| import com.sun.jdi.event.EventSet; |
| import com.sun.jdi.event.ThreadDeathEvent; |
| import com.sun.jdi.event.VMDeathEvent; |
| import com.sun.jdi.event.VMDisconnectEvent; |
| import com.sun.jdi.event.VMStartEvent; |
| import com.sun.jdi.request.EventRequest; |
| |
| /** |
| * Dispatches events generated by an underlying VM. There is one event |
| * dispatcher per JDI debug target. |
| * <p> |
| * Event listeners register with a debug target to handle specific event |
| * requests. A debug target forwards event listeners and requests to its event |
| * dispatcher. As events are received from the underlying VM, those listeners |
| * that registered to handle the specific events are notified. |
| * </p> |
| * <p> |
| * Events are processed in event sets. It is possible that one event can trigger |
| * more than one event request to be processed. In such cases all event requests |
| * triggered by that one event are processed, and each event listener votes on |
| * whether the thread in which the event occurred should be resumed. A thread is |
| * only resumed in if all event handlers agree that the thread should be |
| * resumed. |
| * </p> |
| */ |
| |
| public class EventDispatcher implements Runnable { |
| /** |
| * The debug target this event dispatcher belongs to. |
| */ |
| private JDIDebugTarget fTarget; |
| /** |
| * Whether this dispatcher is shutdown. |
| */ |
| private volatile boolean fShutdown; |
| /** |
| * Table of event listeners. Table is a mapping of <code>EventRequest</code> |
| * to <code>IJDIEventListener</code>. |
| */ |
| private HashMap<EventRequest, IJDIEventListener> fEventHandlers; |
| |
| /** |
| * Queue of debug model events to fire, created when processing events on |
| * the target VM. Keyed by event sets, processed independently. |
| */ |
| private Map<EventSet, List<DebugEvent>> fSetToQueue = new HashMap<>(); |
| |
| /** |
| * Constructs a new event dispatcher listening for events originating from |
| * the specified debug target's underlying VM. |
| * |
| * @param target |
| * the target this event dispatcher belongs to |
| */ |
| public EventDispatcher(JDIDebugTarget target) { |
| fEventHandlers = new HashMap<>(10); |
| fTarget = target; |
| fShutdown = false; |
| } |
| |
| /** |
| * Dispatch the given event set. |
| * |
| * @param eventSet |
| * events to dispatch |
| */ |
| private void dispatch(EventSet eventSet) { |
| if (isShutdown()) { |
| return; |
| } |
| if (JDIDebugOptions.DEBUG_JDI_EVENTS) { |
| EventIterator eventIter = eventSet.eventIterator(); |
| StringBuilder buf = new StringBuilder("JDI Event Set: {\n"); //$NON-NLS-1$ |
| while (eventIter.hasNext()) { |
| buf.append(eventIter.next()); |
| if (eventIter.hasNext()) { |
| buf.append(", "); //$NON-NLS-1$ |
| } |
| } |
| buf.append("}\n"); //$NON-NLS-1$ |
| JDIDebugOptions.trace(buf.toString()); |
| } |
| EventIterator iter = eventSet.eventIterator(); |
| IJDIEventListener[] listeners = new IJDIEventListener[eventSet.size()]; |
| boolean vote = false; |
| boolean resume = true; |
| int index = -1; |
| List<Event> deferredEvents = null; |
| while (iter.hasNext()) { |
| index++; |
| if (isShutdown()) { |
| return; |
| } |
| Event event = iter.nextEvent(); |
| if (event == null) { |
| continue; |
| } |
| // Dispatch events to registered listeners, if any |
| IJDIEventListener listener = fEventHandlers.get(event.request()); |
| listeners[index] = listener; |
| if (listener != null) { |
| if (listener instanceof IJavaLineBreakpoint) { |
| // Event dispatch to conditional breakpoints is deferred |
| // until after |
| // other listeners vote. |
| try { |
| if (((IJavaLineBreakpoint) listener).isConditionEnabled()) { |
| if (deferredEvents == null) { |
| deferredEvents = new ArrayList<>(5); |
| } |
| deferredEvents.add(event); |
| continue; |
| } |
| } catch (CoreException exception) { |
| JDIDebugPlugin.log(exception); |
| } |
| } |
| vote = true; |
| resume = listener.handleEvent(event, fTarget, !resume, eventSet) && resume; |
| continue; |
| } |
| |
| // Dispatch VM start/end events |
| if (event instanceof VMDeathEvent) { |
| fTarget.handleVMDeath((VMDeathEvent) event); |
| shutdown(); // stop listening for events |
| } else if (event instanceof VMDisconnectEvent) { |
| fTarget.handleVMDisconnect((VMDisconnectEvent) event); |
| shutdown(); // stop listening for events |
| } else if (event instanceof VMStartEvent) { |
| fTarget.handleVMStart((VMStartEvent) event); |
| } else { |
| // not handled |
| } |
| } |
| |
| // process deferred conditional breakpoint events |
| if (deferredEvents != null) { |
| Iterator<Event> deferredIter = deferredEvents.iterator(); |
| while (deferredIter.hasNext()) { |
| if (isShutdown()) { |
| return; |
| } |
| Event event = deferredIter.next(); |
| if (event == null) { |
| continue; |
| } |
| // Dispatch events to registered listeners, if any |
| IJDIEventListener listener = fEventHandlers |
| .get(event.request()); |
| if (listener != null) { |
| vote = true; |
| resume = listener.handleEvent(event, fTarget, !resume, eventSet) && resume; |
| continue; |
| } |
| } |
| } |
| |
| List<Runnable> threadDeathRunnables = new ArrayList<>(); |
| |
| // notify handlers of the end result |
| index = -1; |
| iter = eventSet.eventIterator(); |
| while (iter.hasNext()) { |
| index++; |
| Event event = iter.nextEvent(); |
| // notify registered listener, if any |
| IJDIEventListener listener = listeners[index]; |
| if (listener != null) { |
| if (event instanceof ThreadDeathEvent) { |
| final boolean res = resume; |
| threadDeathRunnables.add(() -> listener.eventSetComplete(event, fTarget, !res, eventSet)); |
| } else { |
| listener.eventSetComplete(event, fTarget, !resume, eventSet); |
| } |
| } |
| } |
| |
| // fire queued DEBUG events |
| fireEvents(eventSet); |
| |
| // Queue runnables which will remove terminated threads once other queued events are proceeded |
| threadDeathRunnables.forEach(runnable -> DebugPlugin.getDefault().asyncExec(runnable)); |
| |
| if (vote && resume) { |
| try { |
| eventSet.resume(); |
| } catch (VMDisconnectedException e) { |
| } catch (RuntimeException e) { |
| try { |
| fTarget.targetRequestFailed( |
| JDIDebugMessages.EventDispatcher_0, e); |
| } catch (DebugException de) { |
| JDIDebugPlugin.log(de); |
| } |
| } |
| } |
| } |
| |
| private boolean requiresExpressionEvaluation(EventSet eventSet) { |
| EventIterator iter = eventSet.eventIterator(); |
| while (iter.hasNext()) { |
| Event event = iter.nextEvent(); |
| if (event == null) { |
| continue; |
| } |
| IJDIEventListener listener = fEventHandlers.get(event.request()); |
| if (listener instanceof IJavaLineBreakpoint) { |
| try { |
| if (((IJavaLineBreakpoint) listener).isConditionEnabled()) { |
| return true; |
| } |
| } |
| catch (CoreException e) { |
| return true; // assume the worst |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** @noreference public for test purposes */ |
| public abstract class AbstractDispatchJob extends Job { |
| protected AbstractDispatchJob(String name) { |
| super(name); |
| } |
| |
| @Override |
| public boolean belongsTo(Object family) { |
| return family == EventDispatcher.class || family == EventDispatcher.this; |
| } |
| |
| @Override |
| public String toString() { |
| try { |
| return super.toString() + " for [" + fTarget.getName() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| catch (DebugException e) { |
| return super.toString(); |
| } |
| } |
| } |
| |
| /** |
| * Continuously reads events that are coming from the event queue, until |
| * this event dispatcher is shutdown. A debug target starts a thread on this |
| * method on startup. |
| * |
| * @see #shutdown() |
| */ |
| @Override |
| public void run() { |
| VirtualMachine vm = fTarget.getVM(); |
| if (vm != null) { |
| EventQueue q = vm.eventQueue(); |
| while (!isShutdown()) { |
| try { |
| EventSet eventSet; |
| try { |
| // Get the next event set. |
| eventSet = q.remove(1000); |
| } catch (VMDisconnectedException e) { |
| break; |
| } |
| |
| if (eventSet != null) { |
| if (!requiresExpressionEvaluation(eventSet)) { |
| dispatch(eventSet); |
| } else { |
| // 269231 always evaluate expressions in a separate job to avoid deadlocks |
| Job job = new AbstractDispatchJob("JDI Expression Evaluation Event Dispatch") { //$NON-NLS-1$ |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| dispatch(eventSet); |
| return Status.OK_STATUS; |
| } |
| }; |
| job.setSystem(true); |
| job.schedule(); |
| } |
| } |
| } catch (InterruptedException e) { |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Shutdown this event dispatcher - i.e. causes this event dispatcher to |
| * stop reading and dispatching events from the event queue. The thread |
| * associated with this runnable will exit. |
| */ |
| public void shutdown() { |
| fShutdown = true; |
| Job.getJobManager().cancel(this); |
| } |
| |
| /** |
| * Returns whether this event dispatcher has been shutdown. |
| * |
| * @return whether this event dispatcher has been shutdown |
| */ |
| private boolean isShutdown() { |
| return fShutdown; |
| } |
| |
| /** |
| * Registers the given listener for with the given event request. When an |
| * event is received from the underlying VM, that is associated with the |
| * given event request, the listener will be notified. |
| * |
| * @param listener |
| * the listener to register |
| * @param request |
| * the event request associated with events the listener is |
| * interested in |
| */ |
| public void addJDIEventListener(IJDIEventListener listener, |
| EventRequest request) { |
| fEventHandlers.put(request, listener); |
| } |
| |
| /** |
| * De-registers the given listener and event request. The listener will no |
| * longer be notified of events associated with the request. Listeners are |
| * responsible for deleting the associated event request if required. |
| * |
| * @param listener |
| * the listener to de-register |
| * @param request |
| * the event request to de-register |
| */ |
| public void removeJDIEventListener(IJDIEventListener listener, EventRequest request) { |
| fEventHandlers.remove(request); |
| } |
| |
| /** |
| * Adds the given event to the queue of debug events to fire when done |
| * dispatching events from the given event set. |
| * |
| * @param event |
| * the event to queue |
| * @param set |
| * event set the event is associated with |
| */ |
| public void queue(DebugEvent event, EventSet set) { |
| synchronized (fSetToQueue) { |
| List<DebugEvent> list = fSetToQueue.get(set); |
| if (list == null) { |
| list = new ArrayList<>(5); |
| fSetToQueue.put(set, list); |
| } |
| list.add(event); |
| } |
| } |
| |
| /** |
| * Fires debug events in the event queue associated with the given event |
| * set, and clears the queue. |
| * @param set the set to fire events for |
| */ |
| private void fireEvents(EventSet set) { |
| DebugPlugin plugin = DebugPlugin.getDefault(); |
| if (plugin != null) { // check that not in the process of shutting down |
| List<DebugEvent> list = null; |
| synchronized (fSetToQueue) { |
| list = fSetToQueue.remove(set); |
| } |
| if (list != null) { |
| DebugEvent[] events = list.toArray(new DebugEvent[list.size()]); |
| plugin.fireDebugEventSet(events); |
| } |
| } |
| } |
| |
| } |