/******************************************************************************* * Copyright (c) 2008, 2015 Wind River Systems 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: * Wind River Systems - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.examples.dsf.pda.service; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor; import org.eclipse.cdt.dsf.concurrent.DsfRunnable; import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants; import org.eclipse.cdt.dsf.concurrent.RequestMonitor; import org.eclipse.cdt.dsf.concurrent.ThreadSafe; import org.eclipse.cdt.dsf.debug.service.command.ICommand; import org.eclipse.cdt.dsf.debug.service.command.ICommandControl; import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService; import org.eclipse.cdt.dsf.debug.service.command.ICommandListener; import org.eclipse.cdt.dsf.debug.service.command.ICommandResult; import org.eclipse.cdt.dsf.debug.service.command.ICommandToken; import org.eclipse.cdt.dsf.debug.service.command.IEventListener; import org.eclipse.cdt.dsf.service.AbstractDsfService; import org.eclipse.cdt.dsf.service.DsfSession; import org.eclipse.cdt.examples.dsf.pda.PDAPlugin; import org.eclipse.cdt.examples.dsf.pda.service.commands.AbstractPDACommand; import org.eclipse.cdt.examples.dsf.pda.service.commands.PDACommandResult; import org.eclipse.cdt.examples.dsf.pda.service.commands.PDAExitCommand; 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.osgi.framework.BundleContext; /** * Service that handles communication with a PDA debugger back end. */ public class PDACommandControl extends AbstractDsfService implements ICommandControlService { // Structure used to store command information in services internal queues. private static class CommandHandle { final private ICommandToken fToken; final private AbstractPDACommand<PDACommandResult> fCommand; final private DataRequestMonitor<PDACommandResult> fRequestMonitor; CommandHandle(ICommandToken token, AbstractPDACommand<PDACommandResult> c, DataRequestMonitor<PDACommandResult> rm) { fToken = token; fCommand = c; fRequestMonitor = rm; } } private PDABackend fBackend; // Queue of commands waiting to be sent to the debugger. As long as commands // are in this queue, they can still be removed by clients. private final List<CommandHandle> fCommandQueue = new LinkedList<CommandHandle>(); // Queue of commands that are being sent to the debugger. This queue is read // by the send job, so as soon as commands are inserted into this queue, they can // be considered as sent. @ThreadSafe private final BlockingQueue<CommandHandle> fTxCommands = new LinkedBlockingQueue<CommandHandle>(); // Flag indicating that the PDA debugger started private boolean fStarted = false; // Flag indicating that the PDA debugger has been disconnected @ThreadSafe private boolean fTerminated = false; // Data Model context of this command control. private PDAVirtualMachineDMContext fDMContext; // Synchronous listeners for commands and events. private final List<ICommandListener> fCommandListeners = new ArrayList<ICommandListener>(); private final List<IEventListener> fEventListeners = new ArrayList<IEventListener>(); // Sockets for communicating with PDA debugger @ThreadSafe private PrintWriter fRequestWriter; @ThreadSafe private BufferedReader fRequestReader; @ThreadSafe private BufferedReader fEventReader; // Jobs servicing the sockets. private EventDispatchJob fEventDispatchJob; private CommandSendJob fCommandSendJob; /** * Command control constructor. * @param session The DSF session that this service is a part of. */ public PDACommandControl(DsfSession session) { super(session); } @Override public void initialize(final RequestMonitor rm) { // Call the super-class to perform initialization first. super.initialize( new RequestMonitor(getExecutor(), rm) { @Override protected void handleSuccess() { doInitialize(rm); } }); } private void doInitialize(final RequestMonitor rm) { fBackend = getServicesTracker().getService(PDABackend.class); // Create the control's data model context. fDMContext = new PDAVirtualMachineDMContext(getSession().getId(), fBackend.getPorgramName()); // Add a listener for PDA events to track the started/terminated state. addEventListener(new IEventListener() { @Override public void eventReceived(Object output) { if ("started 1".equals(output)) { setStarted(); } else if ("terminated".equals(output)) { setTerminated(); } } }); // Get intput/output streams from the backend service. // fRequestWriter = new PrintWriter(fBackend.getRequestOutputStream()); fRequestReader = new BufferedReader(new InputStreamReader(fBackend.getRequestInputStream())); fEventReader = new BufferedReader(new InputStreamReader(fBackend.getEventInputStream())); fEventDispatchJob = new EventDispatchJob(); fEventDispatchJob.schedule(); fCommandSendJob = new CommandSendJob(); fCommandSendJob.schedule(); // Register the service with OSGi as the last step in initialization of // the service. register( new String[] { ICommandControl.class.getName(), ICommandControlService.class.getName(), PDACommandControl.class.getName() }, new Hashtable<String,String>()); rm.done(); } @Override public void shutdown(final RequestMonitor requestMonitor) { // Unregister the service first, so that clients may no longer gain access to it. unregister(); if (!isTerminated()) { // If the debugger is still connected, send it the exit command. terminate(new RequestMonitor(getExecutor(), requestMonitor) { @Override protected void handleCompleted() { // Mark the command control as terminated. setTerminated(); // Ignore any error resulting from the exit command. // Errors will most likely result if the PDA process is // already terminated. requestMonitor.done(); } }); } else { requestMonitor.done(); } } @Override protected BundleContext getBundleContext() { return PDAPlugin.getBundleContext(); } /** * Job that services the send command queue. */ private class CommandSendJob extends Job { CommandSendJob() { super("PDA Command Send"); setSystem(true); } @Override protected IStatus run(IProgressMonitor monitor) { while (!isTerminated()) { synchronized (fTxCommands) { try { // Remove command from send queue. final CommandHandle commandHandle = fTxCommands.take(); // Send the request to PDA fRequestWriter.println(commandHandle.fCommand.getRequest()); fRequestWriter.flush(); try { // wait for reply final String response = fRequestReader.readLine(); // Process the reply in the executor thread. try { getExecutor().execute(new DsfRunnable() { @Override public void run() { processCommandDone(commandHandle, response); } }); } catch (RejectedExecutionException e) { // Acceptable race condition may see the session shut down // while we're waiting for command response. Still complete // the request monitor. assert isTerminated(); assert isTerminated(); PDAPlugin.failRequest(commandHandle.fRequestMonitor, REQUEST_FAILED, "Command control shut down."); } } catch (final IOException e) { // Process error it in the executor thread try { getExecutor().execute(new DsfRunnable() { @Override public void run() { processCommandException(commandHandle, e); } }); } catch (RejectedExecutionException re) { // Acceptable race condition... see above assert isTerminated(); PDAPlugin.failRequest(commandHandle.fRequestMonitor, REQUEST_FAILED, "Command control shut down."); } } } catch (InterruptedException e) { break; // Shutting down. } } } return Status.OK_STATUS; } } /** * Job that services the PDA event socket. */ class EventDispatchJob extends Job { public EventDispatchJob() { super("PDA Event Listner"); setSystem(true); } @Override protected IStatus run(IProgressMonitor monitor) { while (!isTerminated()) { try { // Wait for an event. final String event = fEventReader.readLine(); if (event != null) { try { // Process the event in executor thread. getExecutor().execute(new DsfRunnable() { @Override public void run() { processEventReceived(event); } }); } catch (RejectedExecutionException e) {} } else { break; } } catch (IOException e) { break; } } if (!isTerminated()) { // Exception from the event socket is an indicator that the PDA debugger // has exited. Call setTerminated() in executor thread. try { getExecutor().execute(new DsfRunnable() { @Override public void run() { setTerminated(); } }); } catch (RejectedExecutionException e) {} } return Status.OK_STATUS; } } @Override public <V extends ICommandResult> ICommandToken queueCommand(final ICommand<V> command, DataRequestMonitor<V> rm) { ICommandToken token = new ICommandToken() { @Override public ICommand<?> getCommand() { return command; } }; if (command instanceof AbstractPDACommand<?>) { // Cast from command with "<V extends ICommandResult>" to a more concrete // type to use internally in the command control. @SuppressWarnings("unchecked") AbstractPDACommand<PDACommandResult> pdaCommand = (AbstractPDACommand<PDACommandResult>)command; // Similarly, cast the request monitor to a more concrete type. @SuppressWarnings("unchecked") DataRequestMonitor<PDACommandResult> pdaRM = (DataRequestMonitor<PDACommandResult>)rm; // Add the command to the queue and notify command listeners. fCommandQueue.add( new CommandHandle(token, pdaCommand, pdaRM) ); for (ICommandListener listener : fCommandListeners) { listener.commandQueued(token); } // In a separate dispatch cycle. This allows command listeners to respond to the // command queued event. getExecutor().execute(new DsfRunnable() { @Override public void run() { processQueues(); } }); } else { PDAPlugin.failRequest(rm, INTERNAL_ERROR, "Unrecognized command: " + command); } return token; } @Override public void removeCommand(ICommandToken token) { // Removes given command from the queue and notify the listeners for (Iterator<CommandHandle> itr = fCommandQueue.iterator(); itr.hasNext();) { CommandHandle handle = itr.next(); if (token.equals(handle.fToken)) { itr.remove(); for (ICommandListener listener : fCommandListeners) { listener.commandRemoved(token); } } } } @Override public void addCommandListener(ICommandListener processor) { fCommandListeners.add(processor); } @Override public void removeCommandListener(ICommandListener processor) { fCommandListeners.remove(processor); } @Override public void addEventListener(IEventListener processor) { fEventListeners.add(processor); } @Override public void removeEventListener(IEventListener processor) { fEventListeners.remove(processor); } private void processCommandDone(CommandHandle handle, String response) { // Trace to debug output. PDAPlugin.debug("R: " + response); PDACommandResult result = null; if (response.startsWith("error:")) { // Create a generic result with the error response result = new PDACommandResult(response); // Set the error status to the request monitor. handle.fRequestMonitor.setStatus(new Status( IStatus.ERROR, PDAPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED, response, null)); } else { // Given the PDA response string, create the result using the command // that was sent. result = handle.fCommand.createResult(response); // Set the result to the request monitor and return to sender. // Note: as long as PDA sends some response, a PDA command will never // return an error. handle.fRequestMonitor.setData(result); } handle.fRequestMonitor.done(); // Notify listeners of the response for (ICommandListener listener : fCommandListeners) { listener.commandDone(handle.fToken, result); } // Process next command in queue. processQueues(); } private void processCommandException(CommandHandle handle, Throwable exception) { // If sending a command resulted in an exception, notify the client. handle.fRequestMonitor.setStatus(new Status( IStatus.ERROR, PDAPlugin.PLUGIN_ID, REQUEST_FAILED, "Exception reading request response", exception)); handle.fRequestMonitor.done(); // Notify listeners also. for (ICommandListener listener : fCommandListeners) { listener.commandDone(handle.fToken, null); } } private void processEventReceived(String event) { // Notify the listeners only. PDAPlugin.debug("E: " + event); for (IEventListener listener : fEventListeners) { listener.eventReceived(event); } } private synchronized void processQueues() { if (isTerminated()) { // If the PDA debugger is terminated. Return all submitted commands // with an error. for (CommandHandle handle : fCommandQueue) { handle.fRequestMonitor.setStatus(new Status( IStatus.ERROR, PDAPlugin.PLUGIN_ID, INVALID_STATE, "Command control is terminated", null)); handle.fRequestMonitor.done(); } fCommandQueue.clear(); } else if (fStarted && fTxCommands.isEmpty() && !fCommandQueue.isEmpty()) { // Process the queues if: // - the PDA debugger has started, // - there are no pending commands in the send queue, // - and there are commands waiting to be sent. CommandHandle handle = fCommandQueue.remove(0); fTxCommands.add(handle); PDAPlugin.debug("C: " + handle.fCommand.getRequest()); for (ICommandListener listener : fCommandListeners) { listener.commandSent(handle.fToken); } } } /** * Return the PDA Debugger top-level Data Model context. * @see PDAVirtualMachineDMContext */ @Override @ThreadSafe public PDAVirtualMachineDMContext getContext() { return fDMContext; } @Override public String getId() { return fBackend.getPorgramName(); } private void setStarted() { // Mark the command control as started and ready to process commands. fStarted = true; // Process any commands which may have been queued before the processQueues(); // Issue a data model event. getSession().dispatchEvent(new PDAStartedEvent(getContext()), getProperties()); } /** * Returns whether the PDA debugger has started and is processing commands. */ @Override public boolean isActive() { return fStarted && !isTerminated(); } @ThreadSafe private synchronized void setTerminated() { // Set terminated may be called more than once: by event listener thread, // by the terminate command, etc, so protect against sending events multiple // times. if (!fTerminated) { fTerminated = true; // Process any waiting commands, they all should return with an error. processQueues(); // Issue a data model event. getSession().dispatchEvent(new PDATerminatedEvent(getContext()), getProperties()); } } /** * Returns whether the PDA debugger has been terminated. */ @ThreadSafe public synchronized boolean isTerminated() { return fTerminated; } /** * Sends a command to PDA debugger to terminate. */ public void terminate(RequestMonitor rm) { if (!isTerminated()) { queueCommand( new PDAExitCommand(fDMContext), new DataRequestMonitor<PDACommandResult>(getExecutor(), rm)); } else { // If already terminated, indicate success. rm.done(); } } }