| /******************************************************************************* |
| * Copyright (c) 2009, 2015 QNX Software Systems 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: |
| * QNX Software Systems - Initial API and implementation |
| * Hewlett-Packard Development Company - fix for bug 109733 |
| * Wind River Systems - Modified for new DSF Reference Implementation |
| * Marc Khouzam (Ericsson) - Display exit code in process console (Bug 402054) |
| *******************************************************************************/ |
| |
| package org.eclipse.cdt.dsf.mi.service.command; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.PipedInputStream; |
| import java.io.PipedOutputStream; |
| import java.util.concurrent.CancellationException; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.RejectedExecutionException; |
| |
| import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor; |
| 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.ImmediateRequestMonitor; |
| import org.eclipse.cdt.dsf.concurrent.Query; |
| import org.eclipse.cdt.dsf.concurrent.ThreadSafe; |
| import org.eclipse.cdt.dsf.concurrent.ThreadSafeAndProhibitedFromDsfExecutor; |
| import org.eclipse.cdt.dsf.datamodel.DMContexts; |
| import org.eclipse.cdt.dsf.debug.service.IProcesses; |
| import org.eclipse.cdt.dsf.debug.service.IProcesses.IProcessDMContext; |
| import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext; |
| import org.eclipse.cdt.dsf.debug.service.IRunControl.IExitedDMEvent; |
| import org.eclipse.cdt.dsf.debug.service.IRunControl.IStartedDMEvent; |
| import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlShutdownDMEvent; |
| 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.gdb.internal.GdbPlugin; |
| import org.eclipse.cdt.dsf.mi.service.IMICommandControl; |
| import org.eclipse.cdt.dsf.mi.service.IMIContainerDMContext; |
| import org.eclipse.cdt.dsf.mi.service.IMIProcessDMContext; |
| import org.eclipse.cdt.dsf.mi.service.MIProcesses; |
| import org.eclipse.cdt.dsf.mi.service.command.commands.CLICommand; |
| import org.eclipse.cdt.dsf.mi.service.command.events.MIThreadGroupExitedEvent; |
| import org.eclipse.cdt.dsf.mi.service.command.output.MIGDBShowExitCodeInfo; |
| import org.eclipse.cdt.dsf.mi.service.command.output.MIOOBRecord; |
| import org.eclipse.cdt.dsf.mi.service.command.output.MIOutput; |
| import org.eclipse.cdt.dsf.mi.service.command.output.MITargetStreamOutput; |
| import org.eclipse.cdt.dsf.service.DsfServiceEventHandler; |
| import org.eclipse.cdt.dsf.service.DsfServicesTracker; |
| import org.eclipse.cdt.dsf.service.DsfSession; |
| import org.eclipse.cdt.utils.pty.PTY; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| |
| /** |
| * This Process implementation tracks one of the inferiors that is being debugged |
| * by GDB. The process object, although not displayed in the Debug view, is used to |
| * channel the STDIO of the inferior process to the console view. |
| * |
| * @see org.eclipse.debug.core.model.IProcess |
| */ |
| public class MIInferiorProcess extends Process implements IEventListener, ICommandListener { |
| |
| // Indicates that the inferior has been started |
| // It implies the ContainerDMContext is fully-formed |
| // with the pid of the process (as a parent dmc) |
| private boolean fStarted; |
| |
| // Indicates that the inferior has been terminated |
| private boolean fTerminated; |
| |
| private OutputStream fOutputStream; |
| private InputStream fInputStream; |
| |
| private PipedOutputStream fInputStreamPiped; |
| |
| private PipedInputStream fErrorStream; |
| private PipedOutputStream fErrorStreamPiped; |
| |
| private final DsfSession fSession; |
| |
| private final IMICommandControl fCommandControl; |
| private CommandFactory fCommandFactory; |
| |
| private IContainerDMContext fContainerDMContext; |
| |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| private boolean fDisposed = false; |
| |
| /** |
| * Counter for tracking console commands sent by services. |
| * |
| * The CLI 'monitor' command produces target output which should |
| * not be written to the target console, since it is in response to a CLI |
| * command. In fact, CLI commands should never have their output sent |
| * to the target console. |
| * |
| * This counter is incremented any time a CLI command is seen. It is |
| * decremented whenever a CLI command is finished. When counter |
| * value is 0, the inferior process writes the target output. |
| */ |
| private int fSuppressTargetOutputCounter = 0; |
| |
| @ThreadSafe |
| Integer fExitCode = null; |
| |
| /** |
| * @returns whether the inferior has been started, which means |
| * we can obtain its process id. |
| * @since 4.7 |
| */ |
| protected boolean isStarted() { |
| return fStarted; |
| } |
| |
| /** @since 4.7 */ |
| protected IContainerDMContext getContainer() { |
| return fContainerDMContext; |
| } |
| |
| /** @since 4.7 */ |
| protected synchronized boolean isTerminated() { |
| return fTerminated; |
| } |
| |
| /** |
| * Creates an inferior process object which uses the given output stream |
| * to write the user standard input into. |
| * |
| * @param container The process that this inferior represents |
| * @param gdbOutputStream The output stream to use to write user IO into. |
| * @since 4.0 |
| */ |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| public MIInferiorProcess(IContainerDMContext container, OutputStream gdbOutputStream) { |
| this(container, gdbOutputStream, null); |
| } |
| |
| /** |
| * Creates an inferior process object which uses the given terminal |
| * to write the user standard input into. |
| * |
| * @param container The process that this inferior represents |
| * @param p The terminal to use to write user IO into. |
| * @since 4.0 |
| */ |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| public MIInferiorProcess(IContainerDMContext container, PTY p) { |
| this(container, null, p); |
| } |
| |
| /** @since 4.7 */ |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| protected MIInferiorProcess(IContainerDMContext container, final OutputStream gdbOutputStream, PTY pty) { |
| fSession = DsfSession.getSession(container.getSessionId()); |
| fSession.addServiceEventListener(this, null); |
| |
| fContainerDMContext = container; |
| IMIProcessDMContext processDmc = DMContexts.getAncestorOfType(fContainerDMContext, IMIProcessDMContext.class); |
| if (processDmc != null && processDmc.getProcId() != MIProcesses.UNKNOWN_PROCESS_ID) { |
| // If we already know the pid, it implies the process is already started. |
| // It also means we won't get the IStartedDMEvent for the process. |
| // This happens when the inferior is restarted, in which case, this class |
| // is created after the process is running and the IStartedDMEvent was sent. |
| fStarted = true; |
| } |
| |
| DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), fSession.getId()); |
| fCommandControl = tracker.getService(IMICommandControl.class); |
| tracker.dispose(); |
| |
| fCommandFactory = fCommandControl.getCommandFactory(); |
| |
| fCommandControl.addEventListener(this); |
| fCommandControl.addCommandListener(this); |
| |
| if (pty != null) { |
| fOutputStream = pty.getOutputStream(); |
| fInputStream = pty.getInputStream(); |
| fInputStreamPiped = null; |
| } else { |
| fOutputStream = new OutputStream() { |
| @Override |
| public void write(int b) throws IOException { |
| gdbOutputStream.write(b); |
| } |
| }; |
| |
| fInputStreamPiped = new PipedOutputStream(); |
| PipedInputStream inputStream = null; |
| try { |
| // Using a LargePipedInputStream see https://bugs.eclipse.org/bugs/show_bug.cgi?id=223154 |
| inputStream = new LargePipedInputStream(fInputStreamPiped); |
| } catch (IOException e) { |
| } |
| fInputStream = inputStream; |
| |
| } |
| |
| // Note: We do not have any err stream from gdb/mi so this gdb |
| // err channel instead. |
| fErrorStreamPiped = new PipedOutputStream(); |
| PipedInputStream errorStream = null; |
| try { |
| // Using a LargePipedInputStream see https://bugs.eclipse.org/bugs/show_bug.cgi?id=223154 |
| errorStream = new LargePipedInputStream(fErrorStreamPiped); |
| } catch (IOException e) { |
| } |
| fErrorStream = errorStream; |
| } |
| |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| public void dispose() { |
| fSession.removeServiceEventListener(this); |
| |
| fCommandControl.removeEventListener(this); |
| fCommandControl.removeCommandListener(this); |
| |
| closeIO(); |
| |
| setTerminated(); |
| |
| fDisposed = true; |
| } |
| |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| protected boolean isDisposed() { |
| return fDisposed; |
| } |
| |
| @Override |
| public OutputStream getOutputStream() { |
| return fOutputStream; |
| } |
| |
| @Override |
| public InputStream getInputStream() { |
| return fInputStream; |
| } |
| |
| @Override |
| public InputStream getErrorStream() { |
| return fErrorStream; |
| } |
| |
| @ThreadSafeAndProhibitedFromDsfExecutor("fSession#getExecutor") |
| public synchronized void waitForSync() throws InterruptedException { |
| assert !fSession.getExecutor().isInExecutorThread(); |
| |
| while (!fTerminated) { |
| wait(100); |
| } |
| } |
| |
| /** |
| * @see java.lang.Process#waitFor() |
| */ |
| @ThreadSafeAndProhibitedFromDsfExecutor("fSession#getExecutor") |
| @Override |
| public int waitFor() throws InterruptedException { |
| assert !fSession.getExecutor().isInExecutorThread(); |
| |
| waitForSync(); |
| return exitValue(); |
| } |
| |
| @ThreadSafeAndProhibitedFromDsfExecutor("fSession#getExecutor") |
| @Override |
| public int exitValue() { |
| assert !fSession.getExecutor().isInExecutorThread(); |
| |
| synchronized (this) { |
| if (fExitCode != null) { |
| return fExitCode; |
| } |
| } |
| |
| // Fetch the exit code using $_exitcode of GDB when doing single |
| // process debugging (GDB <= 7.1) |
| // Note that for GDB 7.2, there is no proper solution for the exit code |
| // because although we support multi-process, $_exitcode was still the |
| // only way to get the exit code, and that variable does not work properly |
| // with multi-process (it is re-used by the different processes). |
| // We use it still for GDB 7.2, since the single-process case is the most common. |
| try { |
| Query<Integer> exitCodeQuery = new Query<Integer>() { |
| @Override |
| protected void execute(final DataRequestMonitor<Integer> rm) { |
| // Guard against session disposed. |
| if (!DsfSession.isSessionActive(fSession.getId())) { |
| rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_STATE, |
| "Debug session already shut down.", null)); //$NON-NLS-1$ |
| rm.done(); |
| return; |
| } |
| |
| if (isDisposed()) { |
| rm.setData(0); |
| rm.done(); |
| } else if (!fTerminated) { |
| // This will cause ExecutionException to be thrown with a CoreException, |
| // which will in turn contain the IllegalThreadStateException. |
| rm.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_STATE, |
| "Inferior is still running.", new IllegalThreadStateException())); //$NON-NLS-1$ |
| rm.done(); |
| } else { |
| // The exitCode from GDB does not seem to be handled for multi-process |
| // so there is no point is specifying the container |
| fCommandControl.queueCommand( |
| fCommandFactory.createMIGDBShowExitCode(fCommandControl.getContext()), |
| new DataRequestMonitor<MIGDBShowExitCodeInfo>(fSession.getExecutor(), rm) { |
| @Override |
| protected void handleSuccess() { |
| rm.setData(getData().getCode()); |
| rm.done(); |
| } |
| }); |
| |
| } |
| } |
| }; |
| fSession.getExecutor().execute(exitCodeQuery); |
| |
| int exitCode = exitCodeQuery.get(); |
| synchronized (this) { |
| fExitCode = exitCode; |
| } |
| return exitCode; |
| } catch (RejectedExecutionException e) { |
| } catch (InterruptedException e) { |
| } catch (CancellationException e) { |
| } catch (ExecutionException e) { |
| if (e.getCause() instanceof CoreException |
| && ((CoreException) e.getCause()).getStatus().getException() instanceof RuntimeException) { |
| throw (RuntimeException) ((CoreException) e.getCause()).getStatus().getException(); |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * @see java.lang.Process#destroy() |
| */ |
| @Override |
| public void destroy() { |
| try { |
| fSession.getExecutor().execute(new DsfRunnable() { |
| @Override |
| public void run() { |
| doDestroy(); |
| } |
| }); |
| } catch (RejectedExecutionException e) { |
| // Session disposed. |
| } |
| closeIO(); |
| } |
| |
| private void closeIO() { |
| try { |
| if (fOutputStream != null) |
| fOutputStream.close(); |
| // Make sure things get GCed |
| fOutputStream = null; |
| } catch (IOException e) { |
| } |
| try { |
| if (fInputStream != null) |
| fInputStream.close(); |
| // Make sure things get GCed |
| fInputStream = null; |
| } catch (IOException e) { |
| } |
| try { |
| if (fInputStreamPiped != null) |
| fInputStreamPiped.close(); |
| // Make sure things get GCed |
| fInputStreamPiped = null; |
| } catch (IOException e) { |
| } |
| try { |
| if (fErrorStream != null) |
| fErrorStream.close(); |
| // Make sure things get GCed |
| fErrorStream = null; |
| } catch (IOException e) { |
| } |
| try { |
| if (fErrorStreamPiped != null) |
| fErrorStreamPiped.close(); |
| // Make sure things get GCed |
| fErrorStreamPiped = null; |
| } catch (IOException e) { |
| } |
| } |
| |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| private void doDestroy() { |
| if (isDisposed() || !fSession.isActive() || fTerminated) |
| return; |
| |
| DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), fSession.getId()); |
| IProcesses procService = tracker.getService(IProcesses.class); |
| tracker.dispose(); |
| if (procService != null) { |
| IProcessDMContext procDmc = DMContexts.getAncestorOfType(fContainerDMContext, IProcessDMContext.class); |
| procService.terminate(procDmc, new ImmediateRequestMonitor()); |
| } else { |
| setTerminated(); |
| } |
| } |
| |
| @ConfinedToDsfExecutor("fSession#getExecutor") |
| private synchronized void setTerminated() { |
| if (fTerminated) |
| return; |
| fTerminated = true; |
| closeIO(); |
| notifyAll(); |
| } |
| |
| public OutputStream getPipedOutputStream() { |
| return fInputStreamPiped; |
| } |
| |
| public OutputStream getPipedErrorStream() { |
| return fErrorStreamPiped; |
| } |
| |
| @Override |
| public void eventReceived(Object output) { |
| for (MIOOBRecord oobr : ((MIOutput) output).getMIOOBRecords()) { |
| if (oobr instanceof MITargetStreamOutput) { |
| if (fSuppressTargetOutputCounter > 0) |
| return; |
| MITargetStreamOutput tgtOut = (MITargetStreamOutput) oobr; |
| if (fInputStreamPiped != null && tgtOut.getString() != null) { |
| try { |
| fInputStreamPiped.write(tgtOut.getString().getBytes()); |
| fInputStreamPiped.flush(); |
| } catch (IOException e) { |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void commandQueued(ICommandToken token) { |
| // No action |
| } |
| |
| @Override |
| public void commandSent(ICommandToken token) { |
| if (token.getCommand() instanceof CLICommand<?>) { |
| fSuppressTargetOutputCounter++; |
| } |
| } |
| |
| @Override |
| public void commandRemoved(ICommandToken token) { |
| // No action |
| } |
| |
| @Override |
| public void commandDone(ICommandToken token, ICommandResult result) { |
| if (token.getCommand() instanceof CLICommand<?>) { |
| fSuppressTargetOutputCounter--; |
| } |
| } |
| |
| /** |
| * @since 4.0 |
| */ |
| @DsfServiceEventHandler |
| public void eventDispatched(IExitedDMEvent e) { |
| if (e.getDMContext() instanceof IMIContainerDMContext) { |
| // For multi-process, make sure the exited event |
| // is actually for this inferior. |
| if (e.getDMContext().equals(fContainerDMContext)) { |
| // With recent changes, we notice a restart by the exited/started |
| // events, and only then create the new inferior; this means the new |
| // inferior will no longer receive the exited event for the old inferior. |
| // But it does not hurt to leave the below if check just in case. |
| assert fStarted : "Exited event should only be received for a started inferior"; //$NON-NLS-1$ |
| if (fStarted) { |
| // Only mark this process as terminated if it was already |
| // started. This was to protect ourselves in the case of |
| // a restart, where we used to create the new inferior before |
| // killing the old one. In that case we would get the exited |
| // event of the old inferior, which we had to ignore. |
| // |
| setTerminated(); |
| } |
| } |
| } |
| } |
| |
| /** @since 4.2 */ |
| @DsfServiceEventHandler |
| public void eventDispatched(MIThreadGroupExitedEvent e) { |
| } |
| |
| /** |
| * @since 4.0 |
| */ |
| @DsfServiceEventHandler |
| public void eventDispatched(IStartedDMEvent e) { |
| if (e.getDMContext() instanceof IMIContainerDMContext) { |
| // Mark the inferior started if the event is for this inferior. |
| // We may get other started events in the case of a restart |
| if (!fStarted) { |
| // For multi-process, make sure the started event |
| // is actually for this inferior. |
| // We must compare the groupId and not the full context |
| // because the container that we currently hold is incomplete |
| // because the pid was not determined yet. |
| String inferiorGroup = ((IMIContainerDMContext) fContainerDMContext).getGroupId(); |
| |
| if (inferiorGroup == null || inferiorGroup.length() == 0) { |
| // Single process case, so we know we have started |
| fStarted = true; |
| // Store the fully-formed container |
| fContainerDMContext = (IMIContainerDMContext) e.getDMContext(); |
| } else { |
| String startedGroup = ((IMIContainerDMContext) e.getDMContext()).getGroupId(); |
| if (inferiorGroup.equals(startedGroup)) { |
| fStarted = true; |
| // Store the fully-formed container |
| fContainerDMContext = (IMIContainerDMContext) e.getDMContext(); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @since 4.0 |
| */ |
| @DsfServiceEventHandler |
| public void eventDispatched(ICommandControlShutdownDMEvent e) { |
| dispose(); |
| } |
| } |