/*******************************************************************************
* Copyright (c) 2006, 2016 IBM Corporation 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:
* IBM Corporation - Jeff Briggs, Henry Hughes, Ryan Morse
* Red Hat - ongoing maintenance
*******************************************************************************/
package org.eclipse.linuxtools.systemtap.structures.runnable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.linuxtools.internal.systemtap.structures.StructuresPlugin;
import org.eclipse.linuxtools.systemtap.structures.LoggingStreamDaemon;
import org.eclipse.linuxtools.systemtap.structures.listeners.IGobblerListener;
import org.eclipse.linuxtools.tools.launch.core.factory.RuntimeProcessFactory;
/**
* A class to spawn a separate thread to run a <code>Process</code>.
* @author Ryan Morse
* @since 2.0
*/
public class Command implements Runnable {
/*
* Bug in the exec command prevents using a single string. Forced
* to use a workaround in order to run commands with spaces.
*
* http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4365120
* http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4109888
*/
/**
* @since 2.0
*/
protected boolean stopped = false;
private boolean started = false;
/**
* @since 2.0
*/
protected StreamGobbler inputGobbler = null;
/**
* @since 2.0
*/
protected StreamGobbler errorGobbler = null;
private boolean disposed = false;
private List<IGobblerListener> inputListeners = new ArrayList<>(); //Only used to allow adding listeners before creating the StreamGobbler
private List<IGobblerListener> errorListeners = new ArrayList<>(); //Only used to allow adding listeners before creating the StreamGobbler
private int returnVal = Integer.MAX_VALUE;
/**
* @since 3.0
*/
protected final String[] cmd;
/**
* @since 3.0
*/
protected final String[] envVars;
protected Process process = null;
/**
* @since 2.1
*/
protected final IProject project;
private final LoggingStreamDaemon logger;
public static final int ERROR_STREAM = 0;
public static final int INPUT_STREAM = 1;
/**
* Spawns the new thread that this class will run in. From the Runnable
* interface spawning the new thread automatically calls the run() method.
* This must be called by the implementing class in order to start the
* StreamGobbler.
* @param cmd The entire command to run
* @param envVars List of all environment variables to use
* @since 2.0
*/
public Command(String[] cmd, String... envVars) {
this(cmd, envVars, null);
}
/**
* Spawns the new thread that this class will run in. From the Runnable
* interface spawning the new thread automatically calls the run() method.
* This must be called by the implementing class in order to start the
* StreamGobbler.
* @param cmd The entire command to run
* @param envVars List of all environment variables to use, or <code>null</code> if none
* @param project The project this script belongs to, or <code>null</code> if projectless
* @since 2.1
*/
public Command(String[] cmd, String[] envVars, IProject project) {
this.cmd = cmd != null ? Arrays.copyOf(cmd, cmd.length) : null;
this.envVars = envVars != null ? Arrays.copyOf(envVars, envVars.length) : null;
this.project = project;
logger = new LoggingStreamDaemon();
addInputStreamListener(logger);
}
/**
* Starts the <code>Thread</code> that the new <code>Process</code> will run in.
* This must be called in order to get the process to start running.
* Note that this method only takes effect the first time it is called.
* @throws CoreException If initializing failed.
*/
public void start() throws CoreException {
if (started || stopped) {
return;
}
IStatus status = init();
if (status.isOK()) {
Thread t = new Thread(this, cmd[0]);
t.start();
started = true;
} else {
cleanUpAfterStop();
returnVal = Integer.MIN_VALUE;
throw new CoreException(status);
}
}
/**
* Starts up the process that will execute the provided command and registers
* the <code>StreamGobblers</code> with their respective streams.
* @return The status from the initializing process
* @since 2.0
*/
protected IStatus init() {
try {
process = RuntimeProcessFactory.getFactory().exec(cmd, envVars, project);
} catch (IOException e) {
return new Status(IStatus.ERROR, StructuresPlugin.PLUGIN_ID, e.getMessage(), e);
}
if (process == null) {
return new Status(IStatus.ERROR, StructuresPlugin.PLUGIN_ID, Messages.Command_failedToRunSystemtap);
}
errorGobbler = new StreamGobbler(process.getErrorStream());
inputGobbler = new StreamGobbler(process.getInputStream());
transferListeners();
return Status.OK_STATUS;
}
/**
* This transfers any listeners which may have been added
* to the command before the process has been constructed
* properly to the process itself.
* @since 2.0
*/
protected void transferListeners() {
for (IGobblerListener listener : inputListeners) {
inputGobbler.addDataListener(listener);
}
for (IGobblerListener listener : errorListeners) {
errorGobbler.addDataListener(listener);
}
}
/**
* This method handles checking the status of the running <code>Process</code>. It
* is called when the new Thread is created, and thus should never be called by
* any implementing program. To run call the {@link #start} method.
*/
@Override
public void run() {
errorGobbler.start();
inputGobbler.start();
try {
process.waitFor();
cleanUpAfterStop();
} catch (InterruptedException e) {
// This thread was interrupted while waiting for
// the process to exit. Destroy the process just
// to make sure it exits.
stop();
}
}
/**
* Performs cleanup operations for when the process ends:
* Stops the <code>StreamGobblers</code> from monitering
* the dead process and unregisters the StreamListener.
* Also wakes up any threads waiting on this command.
* @since 3.1
*/
protected synchronized void cleanUpAfterStop() {
if (!stopped) {
if (errorGobbler != null) {
errorGobbler.stop();
}
if (inputGobbler != null) {
inputGobbler.stop();
}
removeInputStreamListener(logger);
stopped = true;
notifyAll(); // Wake up threads waiting for this command to stop.
}
}
/**
* Stops the process from running and performs post-stop cleanup if necessary.
*/
public synchronized void stop() {
if (!stopped) {
if (process != null) {
process.destroy();
}
cleanUpAfterStop();
}
}
/**
* Method to check whether or not the process is running.
* @return The execution status.
*/
public boolean isRunning() {
return !stopped;
}
/**
* Method to check whether or not the process has began to run.
* @return <code>false</code> before the process begins to run or
* if initialization of the process has failed; <code>true</code> otherwise.
* @since 3.0
*/
public boolean hasStarted() {
return started;
}
/**
* Method to check if this class has already been disposed.
* @return Status of the class.
*/
public boolean isDisposed() {
return disposed;
}
/**
* The return value of the process.
* 2^231-1 if the process is still running.
* -2^231 if there was an error creating the process
* @return The return value generated from running the provided command.
*/
public int getReturnValue() {
return returnVal;
}
/**
* Registers the provided <code>IGobblerListener</code> with the InputStream
* @param listener A listener to monitor the InputStream from the Process
*/
public void addInputStreamListener(IGobblerListener listener) {
if (inputGobbler != null) {
inputGobbler.addDataListener(listener);
} else {
inputListeners.add(listener);
}
}
/**
* Registers the provided <code>IGobblerListener</code> with the ErrorStream
* @param listener A listener to monitor the ErrorStream from the Process
*/
public void addErrorStreamListener(IGobblerListener listener) {
if (errorGobbler != null) {
errorGobbler.addDataListener(listener);
} else {
errorListeners.add(listener);
}
}
/**
* Removes the provided listener from those monitoring the InputStream.
* @param listener An <code>IGobblerListener</code> that is monitoring the stream.
*/
public void removeInputStreamListener(IGobblerListener listener) {
if (inputGobbler != null) {
inputGobbler.removeDataListener(listener);
} else {
inputListeners.remove(listener);
}
}
/**
* Removes the provided listener from those monitoring the ErrorStream.
* @param listener An <code>IGobblerListener</code> that is monitoring the stream.
*/
public void removeErrorStreamListener(IGobblerListener listener) {
if (errorGobbler != null) {
errorGobbler.removeDataListener(listener);
} else {
errorListeners.remove(listener);
}
}
/**
* Saves the input stream data to a permanent file. Any new data on the
* stream will automatically be saved to the file.
* @param file The file to save the InputStream to.
* @return True if the save was successful, false otherwise.
*/
public boolean saveLog(File file) {
return logger.saveLog(file);
}
/**
* Disposes of all internal components of this class. Nothing in the class should be
* referenced after this is called.
*/
public synchronized void dispose() {
if (!disposed) {
stop();
if (inputListeners != null) {
inputListeners.clear();
}
inputListeners = null;
if (errorListeners != null) {
errorListeners.clear();
}
errorListeners = null;
if (inputGobbler != null) {
inputGobbler.dispose();
}
inputGobbler = null;
if (errorGobbler != null) {
errorGobbler.dispose();
}
errorGobbler = null;
logger.dispose();
process = null;
disposed = true;
}
}
/**
* @return The process of this command.
* @since 3.0
*/
public Process getProcess() {
return process;
}
}