blob: ff10903d7b8d8d24a0b06c9a5f222295e21bca53 [file] [log] [blame]
/*******************************************************************************
* 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();
}
}