/*******************************************************************************
* Copyright (c) 2006, 2017 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.ui.consolelog.structures;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.linuxtools.internal.systemtap.ui.consolelog.structures.ErrorStreamDaemon;
import org.eclipse.linuxtools.systemtap.graphing.ui.widgets.ExceptionErrorDialog;
import org.eclipse.linuxtools.systemtap.structures.runnable.Command;
import org.eclipse.linuxtools.systemtap.ui.consolelog.ScpExec;
import org.eclipse.linuxtools.systemtap.ui.consolelog.internal.Localization;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IOConsole;
/**
* This class serves as a pane in the ConsoleView. It is used to create a new Command that,
* through ConsoleDaemons, will print all the output the the console. In order to stop the
* running Command <code>StopScriptAction</code> should be used to stop this console from
* running.
* @author Ryan Morse
*/
public class ScriptConsole extends IOConsole {
private static final String STAP_CONSOLE_TYPE = "stap"; //$NON-NLS-1$
/**
* The command that will run in this console.
*/
private Command cmd = null;
/**
* A protocol for sending "stop" signals to cmd when it is forcably
* stopped by a user action.
*/
private final StopCommand stopCommand = new StopCommand();
/**
* A thread in which to asynchronously run {@link stopCommand}.
*/
private Thread stopCommandThread;
/**
* A thread used for notifying the console when {@link #cmd} has successfully stopped.
*/
private Thread onCmdStopThread;
/**
* A thread used for starting a new run of {@link #cmd}. It starts a new run only
* once a previous run of cmd has successfully stopped.
*/
private Thread onCmdStartThread;
private ErrorStreamDaemon errorDaemon;
private ConsoleStreamDaemon consoleDaemon;
/**
* @since 2.0
*/
public interface ScriptConsoleObserver {
/**
* Notifying observer of state change.
* @param started If the action is started or not.
* @param stopped If the action is stopped or not.
* @since 3.0
*/
void runningStateChanged(boolean started, boolean stopped);
}
private final Set<ScriptConsoleObserver> activeConsoleObservers = new HashSet<>();
private final Set<ScriptConsoleObserver> inactiveConsoleObservers = new HashSet<>();
/**
* Returns whether or not a ScriptConsole of the specified name exists and is running.
* @param name The name of the console (likely a script name) to check.
* @return <code>true</code> if a ScriptConsole of the given name both exists and is running,
* or <code>false</code> otherwise.
* @since 3.0
*/
public static boolean instanceIsRunning(String name) {
IConsole ic[] = ConsolePlugin.getDefault().getConsoleManager().getConsoles();
if (ic != null) {
for (IConsole consoleIterator : ic) {
if (consoleIterator instanceof ScriptConsole) {
ScriptConsole activeConsole = (ScriptConsole) consoleIterator;
if (activeConsole.getBaseName().equals(name) && activeConsole.isRunning()) {
return true;
}
}
}
}
return false;
}
/**
* This method is used to create a reference to a new <code>ScriptConsole</code>. If there
* is already a console that has the same name as that provided it will be stopped,
* cleared and returned to the caller to use. If there is no console matching the
* provided name then a new <code>ScriptConsole</code> will be created for use.
* @param name The name of the console that should be created & returned.
* @return A console of the specified name.
*/
public synchronized static ScriptConsole getInstance(String name) {
IConsole ic[] = ConsolePlugin.getDefault().getConsoleManager().getConsoles();
if (ic != null) {
ScriptConsole activeConsole;
for (IConsole consoleIterator : ic) {
if (consoleIterator instanceof ScriptConsole) {
activeConsole = (ScriptConsole) consoleIterator;
if (activeConsole.getBaseName().equals(name)) {
activeConsole.reset();
return activeConsole;
}
}
}
}
// If no console with given name exists, make a new one
ScriptConsole console = new ScriptConsole(name, null);
ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] {console});
return console;
}
/**
* This method will check to see if any scripts are currently running.
* @return boolean indicating whether any scripts are running.
* @since 2.0
*/
public static boolean anyRunning() {
IConsole ic[] = ConsolePlugin.getDefault().getConsoleManager().getConsoles();
ScriptConsole console;
for (IConsole con : ic) {
if (con instanceof ScriptConsole) {
console = (ScriptConsole)con;
if (console.isRunning()) {
return true;
}
}
}
return false;
}
/**
* This method will stop all consoles that are running.
* @since 2.0
*/
public static void stopAll() {
IConsole ic[] = ConsolePlugin.getDefault().getConsoleManager().getConsoles();
ScriptConsole console;
for (IConsole con : ic) {
if (con instanceof ScriptConsole) {
console = (ScriptConsole)con;
console.stop();
}
}
}
ScriptConsole(String name, ImageDescriptor imageDescriptor) {
super(name, STAP_CONSOLE_TYPE, imageDescriptor);
}
/**
* Creates the <code>ConsoleStreamDaemon</code> for passing data from the
* <code>LoggedCommand</code>'s InputStream to the Console.
*/
private void createConsoleDaemon() {
consoleDaemon = new ConsoleStreamDaemon(this);
}
/**
* Runs the provided command in this ScriptConsole instance.
* @param command The command and arguments to run.
* @param envVars The environment variables to use while running.
* @param remoteOptions The remote options (such as username and password) to run the script with.
* @param errorParser The parser to handle error messages generated by the command.
* @since 3.0
*/
public synchronized void run(String[] command, String[] envVars, final RemoteScriptOptions remoteOptions, IErrorParser errorParser) {
// Don't start a new command if one is already waiting to be started.
if (waitingToStart()) {
return;
}
cmd = new ScpExec(command, remoteOptions, envVars);
internalRun(errorParser);
}
/**
* Runs the provided command in this ScriptConsole instance on the current host.
* @param command The command and arguments to run.
* @param envVars The environment variables to use while running.
* @param errorParser The parser to handle error messages generated by the command.
* @param project The project that command belongs to or null.
* @since 2.1
*/
public synchronized void runLocally(String[] command, String[] envVars, IErrorParser errorParser, IProject project) {
// Don't start a new command if one is already waiting to be started.
if (waitingToStart()) {
return;
}
cmd = new Command(command, envVars, project);
internalRun(errorParser);
}
private void internalRun(IErrorParser errorParser) {
if (hasStarted()) {
reset();
}
if (errorParser != null) {
errorDaemon = new ErrorStreamDaemon(this);
}
createConsoleDaemon();
notifyConsoleObservers();
activate();
onCmdStartThread = new Thread(onCmdStart);
onCmdStartThread.start();
}
private final Runnable onCmdStart = new Runnable() {
@Override
public void run() {
// If stopping the previous command, wait for it to stop.
if (isThreadAlive(stopCommandThread)
&& stopCommand.stopcmd != cmd) {
try {
stopCommandThread.join();
} catch (InterruptedException e) {
return;
}
}
if (errorDaemon != null) {
cmd.addErrorStreamListener(errorDaemon);
}
cmd.addInputStreamListener(consoleDaemon);
onCmdStopThread = new Thread(onCmdStop);
onCmdStopThread.start();
clearConsole();
try {
cmd.start();
} catch (final CoreException e) {
ExceptionErrorDialog.openError(
Localization.getString("ScriptConsole.ErrorRunningStapTitle"), //$NON-NLS-1$
Localization.getString("ScriptConsole.ErrorRunningStapMessage"), e);//$NON-NLS-1$
cmd.dispose();
return;
}
notifyConsoleObservers();
}
};
private class StopCommand implements Runnable {
private Command stopcmd;
private ErrorStreamDaemon stopErrorDaemon;
private ConsoleStreamDaemon stopConsoleDaemon;
private boolean disposeOnStop = false;
/**
* Prepares and begins the process (in a new thread) that will stop the provided command.
* If there is already a running stop process, though, no action will be taken.
* @param stopcmd The command to stop.
* @param stopErrorDaemon The error stream gobbler of the command to stop.
* @param stopConsoleDaemon The output stream gobbler of the command to stop.
* @param disposeOnStop If <code>true</code>, the command will be disposed when it is stopped.
*/
void start(Command stopcmd, ErrorStreamDaemon stopErrorDaemon,
ConsoleStreamDaemon stopConsoleDaemon, boolean disposeOnStop) {
if (isRunning() && !isThreadAlive(stopCommandThread)) {
this.stopcmd = stopcmd;
this.stopErrorDaemon = stopErrorDaemon;
this.stopConsoleDaemon = stopConsoleDaemon;
this.disposeOnStop = disposeOnStop;
stopCommandThread = new Thread(this);
stopCommandThread.start();
}
}
@Override
public void run() {
// If the command to be stopped is still starting up, wait for it to start.
if (isThreadAlive(onCmdStartThread)
&& stopcmd == cmd) {
try {
onCmdStartThread.join();
} catch (InterruptedException e) {}
}
if (stopConsoleDaemon != null) {
stopcmd.removeInputStreamListener(stopConsoleDaemon);
stopConsoleDaemon.dispose();
}
if (stopErrorDaemon != null) {
stopcmd.removeErrorStreamListener(stopErrorDaemon);
stopErrorDaemon.dispose();
}
if (!disposeOnStop) {
stopcmd.stop();
} else {
stopcmd.dispose();
}
}
void dispose() {
if (stopcmd != null) {
stopcmd.dispose();
stopcmd = null;
}
if (stopErrorDaemon != null) {
stopErrorDaemon.dispose();
stopErrorDaemon = null;
}
if (stopConsoleDaemon != null) {
stopConsoleDaemon.dispose();
stopConsoleDaemon = null;
}
}
}
private final Runnable onCmdStop = () -> {
try {
synchronized (cmd) {
while (cmd.isRunning()) {
cmd.wait();
}
onCmdStopActions();
}
} catch (InterruptedException e) {
return;
}
};
private void onCmdStopActions() {
notifyConsoleObservers();
final String name = super.getName();
Display.getDefault().asyncExec(() -> setName(Localization.getString("ScriptConsole.Terminated") + name)); //$NON-NLS-1$
}
/**
* Stops the running command and the associated listeners.
*/
public synchronized void stop() {
stopCommand.start(cmd, errorDaemon, consoleDaemon, false);
}
/**
* Stops and disposes the running command.
*/
private synchronized void stopAndDispose() {
stopCommand.start(cmd, errorDaemon, consoleDaemon, true);
}
/**
* Stops the running command and immediately prepares the console for another command.
*/
private void reset() {
if (waitingToStart()) {
try {
onCmdStartThread.join();
} catch (InterruptedException e) {}
}
if (isThreadAlive(onCmdStopThread)) {
onCmdStopThread.interrupt();
try {
onCmdStopThread.join();
} catch (InterruptedException e) {}
}
if (isThreadAlive(stopCommandThread)) {
try {
stopCommandThread.join();
} catch (InterruptedException e) {}
disposeCommand();
} else if (isRunning()) {
stopAndDispose();
} else {
disposeCommand();
}
clearConsole();
setName(getBaseName());
}
private synchronized void notifyConsoleObservers() {
boolean started = hasStarted();
boolean running = isRunning();
for (ScriptConsoleObserver observer : inactiveConsoleObservers) {
activeConsoleObservers.remove(observer);
}
inactiveConsoleObservers.clear();
for (ScriptConsoleObserver observer : activeConsoleObservers) {
observer.runningStateChanged(started, !running);
}
}
/**
* Add observer to the script console.
*
* @param observer The observer to be added.
* @since 2.0
*/
public synchronized void addScriptConsoleObserver(ScriptConsoleObserver observer) {
activeConsoleObservers.add(observer);
observer.runningStateChanged(hasStarted(), !isRunning());
}
/**
* Removes observer from the script console.
* @param observer The observer to be removed.
* @since 3.0
*/
public synchronized void removeScriptConsoleObserver(ScriptConsoleObserver observer) {
inactiveConsoleObservers.add(observer);
}
/**
* Method to allow the user to save the Commands output to a file for use latter.
* Does not return a value indicating success of the operation; for that, use
* {@link #saveStreamAndReturnResult(File)} instead.
* @param file The new file to save the output to.
*/
public void saveStream(File file) {
saveStreamAndReturnResult(file);
}
/**
* Method to allow the user to save the Commands output to a file for use later.
* @param file The new file to save the output to.
* @return <code>true</code> if the save result was successful, <code>false</code> otherwise.
* Note that a failed save attempt will not interfere with an already-running log.
* @since 3.1
*/
public boolean saveStreamAndReturnResult(File file) {
if (!cmd.saveLog(file)) {
MessageDialog.openWarning(
PlatformUI.getWorkbench()
.getActiveWorkbenchWindow().getShell(),
Localization.getString("ScriptConsole.Problem"), Localization.getString("ScriptConsole.ErrorSavingLog")); //$NON-NLS-1$//$NON-NLS-2$
return false;
}
return true;
}
private boolean waitingToStart() {
return isThreadAlive(onCmdStartThread);
}
/**
* Check to see if the Command is still running.
* @return boolean representing if the command is running.
*/
public boolean isRunning() {
return cmd == null ? false : cmd.isRunning();
}
/**
* Check to see if the Command has been set up.
* @return boolean representing if the command has started.
*/
private boolean hasStarted() {
return cmd == null ? false : cmd.hasStarted();
}
/**
* Check to see if this class has already been disposed.
* @return boolean representing whether or not the class has been disposed.
*/
public boolean isDisposed() {
// If there is no command it can be considered disposed
if (cmd == null) {
return true;
}
return cmd.isDisposed();
}
private void disposeCommand() {
if (!isDisposed()) {
cmd.dispose();
}
}
/**
* Gets the command that is running in this console, or null if there is no running command.
* @return The <code>LoggedCommand</code> that is running in this console.
* @since 2.0
*/
public Command getCommand() {
return cmd;
}
/**
* @return The process associated with this console's script when it is run.
* A <code>null</code> process indicates that the script has not yet started
* (if {@link #isRunning} returns true) or failed to start (if {@link #isRunning} is false).
* @since 3.0
*/
public Process getProcess() {
return cmd != null ? cmd.getProcess() : null;
}
/**
* @return The name of this console with all status strings removed.
*/
private String getBaseName() {
return getName().replace(Localization.getString(
"ScriptConsole.Terminated"), ""); //$NON-NLS-1$ //$NON-NLS-2$
}
private boolean isThreadAlive(Thread thread) {
return thread != null && thread.isAlive();
}
/**
* Disposes of all internal references in the class. No method should be called after this.
*/
@Override
public synchronized void dispose() {
if (!isDisposed()) {
onCmdStartThread = null;
onCmdStopThread = null;
stopCommandThread = null;
if (cmd != null) {
cmd.dispose();
}
cmd = null;
if (errorDaemon != null) {
errorDaemon.dispose();
}
errorDaemon = null;
if (consoleDaemon != null) {
consoleDaemon.dispose();
}
consoleDaemon = null;
stopCommand.dispose();
if (activeConsoleObservers != null) {
activeConsoleObservers.clear();
}
if (inactiveConsoleObservers != null) {
inactiveConsoleObservers.clear();
}
}
}
}