blob: 98122a8d673069448846a1b5bb8b4aa7639c5b98 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2021 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.debug.core.model;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.PlatformObject;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.internal.core.DebugCoreMessages;
import org.eclipse.debug.internal.core.NullStreamsProxy;
import org.eclipse.debug.internal.core.StreamsProxy;
/**
* Standard implementation of an <code>IProcess</code> that wrappers a system
* process (<code>java.lang.Process</code>).
* <p>
* Clients may subclass this class. Clients that need to replace the implementation
* of a streams proxy associated with an <code>IProcess</code> should subclass this
* class. Generally clients should not instantiate this class directly, but should
* instead call <code>DebugPlugin.newProcess(...)</code>, which can delegate to an
* <code>IProcessFactory</code> if one is referenced by the associated launch configuration.
* </p>
* @see org.eclipse.debug.core.model.IProcess
* @see org.eclipse.debug.core.IProcessFactory
* @since 3.0
*/
public class RuntimeProcess extends PlatformObject implements IProcess {
private static final int TERMINATION_TIMEOUT = 5000; // ms
/**
* The launch this process is contained in
*/
private ILaunch fLaunch;
/**
* The system process represented by this <code>IProcess</code>
*/
private Process fProcess;
/**
* This process's exit value
*/
private int fExitValue;
/**
* The monitor which listens for this runtime process' system process
* to terminate.
*/
private final ProcessMonitorThread fMonitor;
/**
* The streams proxy for this process
*/
private IStreamsProxy fStreamsProxy;
/**
* The name of the process
*/
private String fName;
/**
* Whether this process has been terminated
*/
private boolean fTerminated;
/**
* Table of client defined attributes
*/
private Map<String, String> fAttributes;
/**
* Whether output from the process should be captured or swallowed
*/
private boolean fCaptureOutput = true;
/**
* Whether the descendants of this process should be terminated too
*/
private boolean fTerminateDescendants = true;
/**
* Constructs a RuntimeProcess on the given system process
* with the given name, adding this process to the given
* launch.
*
* @param launch the parent launch of this process
* @param process underlying system process
* @param name the label used for this process
* @param attributes map of attributes used to initialize the attributes
* of this process, or <code>null</code> if none
*/
public RuntimeProcess(ILaunch launch, Process process, String name, Map<String, String> attributes) {
setLaunch(launch);
initializeAttributes(attributes);
fProcess = process;
fName = name;
fTerminated = true;
try {
fExitValue = process.exitValue();
} catch (IllegalThreadStateException e) {
fTerminated = false;
}
String captureOutput = launch.getAttribute(DebugPlugin.ATTR_CAPTURE_OUTPUT);
fCaptureOutput = !("false".equals(captureOutput)); //$NON-NLS-1$
try {
ILaunchConfiguration launchConfiguration = launch.getLaunchConfiguration();
if (launchConfiguration != null) {
fTerminateDescendants = launchConfiguration.getAttribute(DebugPlugin.ATTR_TERMINATE_DESCENDANTS, true);
}
} catch (CoreException e) {
DebugPlugin.log(e);
}
fStreamsProxy = createStreamsProxy();
fMonitor = new ProcessMonitorThread();
fMonitor.start();
launch.addProcess(this);
fireCreationEvent();
}
/**
* Initialize the attributes of this process to those in the given map.
*
* @param attributes attribute map or <code>null</code> if none
*/
private void initializeAttributes(Map<String, String> attributes) {
if (attributes != null) {
attributes.forEach(this::setAttribute);
}
}
/**
* @see ITerminate#canTerminate()
*/
@Override
public synchronized boolean canTerminate() {
return !fTerminated;
}
/**
* @see IProcess#getLabel()
*/
@Override
public String getLabel() {
return fName;
}
/**
* Sets the launch this process is contained in
*
* @param launch the launch this process is contained in
*/
protected void setLaunch(ILaunch launch) {
fLaunch = launch;
}
/**
* @see IProcess#getLaunch()
*/
@Override
public ILaunch getLaunch() {
return fLaunch;
}
/**
* Returns the underlying system process associated with this process.
*
* @return system process
*/
protected Process getSystemProcess() {
return fProcess;
}
/**
* @see ITerminate#isTerminated()
*/
@Override
public synchronized boolean isTerminated() {
return fTerminated;
}
/**
* @see ITerminate#terminate()
*/
@Override
public void terminate() throws DebugException {
if (!isTerminated()) {
if (fStreamsProxy instanceof StreamsProxy) {
((StreamsProxy) fStreamsProxy).kill();
}
Process process = getSystemProcess();
if (process == null) {
return;
}
List<ProcessHandle> descendants = Collections.emptyList();
if (fTerminateDescendants) {
try { // List of descendants of process is only a snapshot!
descendants = process.descendants().collect(Collectors.toList());
} catch (UnsupportedOperationException e) {
// JVM may not support toHandle() -> assume no descendants
}
}
process.destroy();
descendants.forEach(ProcessHandle::destroy);
// await termination of process and descendants
try { // (in total don't wait longer than TERMINATION_TIMEOUT)
long waitStart = System.currentTimeMillis();
if (process.waitFor(TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS)) {
fExitValue = process.exitValue();
if (waitFor(descendants, waitStart)) {
return;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// clean-up
fMonitor.killThread();
IStatus status = new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.TARGET_REQUEST_FAILED, DebugCoreMessages.RuntimeProcess_terminate_failed, null);
throw new DebugException(status);
}
}
/**
* Awaits the termination of the processes of the given ProcessHandles.
* <p>
* If all of the specified processes terminate before {@code waitStart} +
* {@link #TERMINATION_TIMEOUT} this methods returns {@code true}. If any
* process has not terminated until the so specified timeout this methods
* aborts waiting and returns {@code false}.
* </p>
*
* @param descendants the list of handles to the processes to await
* @param waitStart the time when await of the process termination started
* @return true if each process has terminated (before timeout), else false
* @throws InterruptedException if the current thread was interrupted while
* waiting
*/
private boolean waitFor(List<ProcessHandle> descendants, long waitStart) throws InterruptedException {
try {
for (ProcessHandle handle : descendants) {
long remainingTime = TERMINATION_TIMEOUT - (System.currentTimeMillis() - waitStart);
// await termination of this descendant
handle.onExit().get(remainingTime, TimeUnit.MILLISECONDS);
}
return true;
} catch (ExecutionException e) { // should not happen
throw new IllegalStateException(e.getCause());
} catch (TimeoutException e) {
return false; // any sub-processes timed out
}
}
/**
* Notification that the system process associated with this process
* has terminated.
*/
protected void terminated() {
setAttribute(DebugPlugin.ATTR_TERMINATE_TIMESTAMP, Long.toString(System.currentTimeMillis()));
if (fStreamsProxy instanceof StreamsProxy) {
((StreamsProxy)fStreamsProxy).close();
}
// Avoid calling IProcess.exitValue() inside a sync section (Bug 311813).
int exitValue = -1;
boolean running = false;
try {
exitValue = fProcess.exitValue();
} catch (IllegalThreadStateException ie) {
running = true;
}
synchronized (this) {
fTerminated= true;
if (!running) {
fExitValue = exitValue;
}
fProcess= null;
}
fireTerminateEvent();
}
/**
* @see IProcess#getStreamsProxy()
*/
@Override
public IStreamsProxy getStreamsProxy() {
if (!fCaptureOutput) {
return null;
}
return fStreamsProxy;
}
/**
* Creates and returns the streams proxy associated with this process.
*
* @return streams proxy
*/
protected IStreamsProxy createStreamsProxy() {
if (!fCaptureOutput) {
return new NullStreamsProxy(getSystemProcess());
}
String encoding = getLaunch().getAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING);
Charset charset = null;
if (encoding != null) {
try {
charset = Charset.forName(encoding);
} catch (UnsupportedCharsetException | IllegalCharsetNameException e) {
DebugPlugin.log(e);
}
}
return new StreamsProxy(getSystemProcess(), charset);
}
/**
* Fires a creation event.
*/
protected void fireCreationEvent() {
fireEvent(new DebugEvent(this, DebugEvent.CREATE));
}
/**
* Fires the given debug event.
*
* @param event debug event to fire
*/
protected void fireEvent(DebugEvent event) {
DebugPlugin manager= DebugPlugin.getDefault();
if (manager != null) {
manager.fireDebugEventSet(new DebugEvent[]{event});
}
}
/**
* Fires a terminate event.
*/
protected void fireTerminateEvent() {
fireEvent(new DebugEvent(this, DebugEvent.TERMINATE));
}
/**
* Fires a change event.
*/
protected void fireChangeEvent() {
fireEvent(new DebugEvent(this, DebugEvent.CHANGE));
}
/**
* @see IProcess#setAttribute(String, String)
*/
@Override
public void setAttribute(String key, String value) {
if (fAttributes == null) {
fAttributes = new HashMap<>(5);
}
Object origVal = fAttributes.get(key);
if (origVal != null && origVal.equals(value)) {
return; //nothing changed.
}
fAttributes.put(key, value);
fireChangeEvent();
}
/**
* @see IProcess#getAttribute(String)
*/
@Override
public String getAttribute(String key) {
if (fAttributes == null) {
return null;
}
return fAttributes.get(key);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getAdapter(Class<T> adapter) {
if (adapter.equals(IProcess.class)) {
return (T) this;
}
if (adapter.equals(IDebugTarget.class)) {
ILaunch launch = getLaunch();
IDebugTarget[] targets = launch.getDebugTargets();
for (IDebugTarget target : targets) {
if (this.equals(target.getProcess())) {
return (T) target;
}
}
return null;
}
if (adapter.equals(ILaunch.class)) {
return (T) getLaunch();
}
//CONTEXTLAUNCHING
if(adapter.equals(ILaunchConfiguration.class)) {
return (T) getLaunch().getLaunchConfiguration();
}
return super.getAdapter(adapter);
}
/**
* @see IProcess#getExitValue()
*/
@Override
public synchronized int getExitValue() throws DebugException {
if (isTerminated()) {
return fExitValue;
}
throw new DebugException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.TARGET_REQUEST_FAILED, DebugCoreMessages.RuntimeProcess_Exit_value_not_available_until_process_terminates__1, null));
}
/**
* Monitors a system process, waiting for it to terminate, and
* then notifies the associated runtime process.
*/
private class ProcessMonitorThread extends Thread {
/**
* Whether the thread has been told to exit.
*/
private volatile boolean fExit;
/**
* @see Thread#run()
*/
@Override
public void run() {
Process fOSProcess = RuntimeProcess.this.getSystemProcess();
if (!fExit && fOSProcess != null) {
try {
fOSProcess.waitFor();
} catch (InterruptedException ie) {
// clear interrupted state
Thread.interrupted();
} finally {
RuntimeProcess.this.terminated();
}
}
}
/**
* Creates a new process monitor and starts monitoring the process for
* termination.
*/
private ProcessMonitorThread() {
super(DebugCoreMessages.ProcessMonitorJob_0);
setDaemon(true);
}
/**
* Kills the monitoring thread.
*
* This method is to be useful for dealing with the error
* case of an underlying process which has not informed this
* monitor of its termination.
*/
private void killThread() {
fExit = true;
this.interrupt(); // ignored if monitor thread is not yet running
}
}
}