| /******************************************************************************* |
| * Copyright (c) 2004, 2015 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.core.tests.session; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| |
| /** |
| * Executes an external process synchronously, allowing the client to define |
| * a maximum amount of time for the process to complete. |
| */ |
| public class ProcessController { |
| /** |
| * Thrown when a process being executed exceeds the maximum amount |
| * of time allowed for it to complete. |
| */ |
| public static class TimeOutException extends Exception { |
| /** |
| * All serializable objects should have a stable serialVersionUID |
| */ |
| private static final long serialVersionUID = 1L; |
| |
| public TimeOutException() { |
| super(); |
| } |
| |
| public TimeOutException(String message) { |
| super(message); |
| } |
| } |
| |
| private boolean finished; |
| private OutputStream forwardStdErr; |
| private InputStream forwardStdIn; |
| private OutputStream forwardStdOut; |
| private boolean killed; |
| private String[] params; |
| private Process process; |
| private long startupTime; |
| private long timeLimit; |
| |
| /** |
| * Constructs an instance of ProcessController. This does not creates an |
| * OS process. <code>run()</code> does that. |
| * |
| * @param timeout the maximum time the process should take to run |
| * @param params the parameters to be passed to the controlled process |
| */ |
| public ProcessController(long timeout, String[] params) { |
| this.timeLimit = timeout; |
| this.params = params; |
| } |
| |
| private void controlProcess() { |
| new Thread("Process controller") { |
| @Override |
| public void run() { |
| while (!isFinished() && !timedOut()) { |
| synchronized (this) { |
| try { |
| wait(100); |
| } catch (InterruptedException e) { |
| break; |
| } |
| } |
| } |
| kill(); |
| } |
| }.start(); |
| } |
| |
| /** |
| * Causes the process to start executing. This call will block until the |
| * process has completed. If <code>timeout</code> is specified, the |
| * process will be interrupted if it takes more than the specified amount |
| * of time to complete, causing a <code>TimedOutException</code> to be thrown. |
| * Specifying zero as <code>timeout</code> means |
| * the process is not time constrained. |
| * |
| * @return the process exit value |
| * @throws InterruptedException |
| * @throws IOException |
| * @throws TimeOutException if the process did not complete in time |
| */ |
| public int execute() throws InterruptedException, IOException, TimeOutException { |
| this.startupTime = System.currentTimeMillis(); |
| // starts the process |
| process = Runtime.getRuntime().exec(params); |
| if (forwardStdErr != null) { |
| forwardStream("stderr", process.getErrorStream(), forwardStdErr); |
| } |
| if (forwardStdOut != null) { |
| forwardStream("stdout", process.getInputStream(), forwardStdOut); |
| } |
| if (forwardStdIn != null) { |
| forwardStream("stdin", forwardStdIn, process.getOutputStream()); |
| } |
| if (timeLimit > 0) { |
| // ensures process execution time does not exceed the time limit |
| controlProcess(); |
| } |
| try { |
| return process.waitFor(); |
| } finally { |
| markFinished(); |
| if (wasKilled()) { |
| throw new TimeOutException(); |
| } |
| } |
| } |
| |
| /** |
| * Forwards the process standard error output to the given output stream. |
| * Must be called before execution has started. |
| * |
| * @param err an output stream where to forward the process |
| * standard error output to |
| */ |
| public void forwardErrorOutput(OutputStream err) { |
| this.forwardStdErr = err; |
| } |
| |
| /** |
| * Forwards the process standard output to the given output stream. |
| * Must be called before execution has started. |
| * |
| * @param out an output stream where to forward the process |
| * standard output to |
| */ |
| public void forwardOutput(OutputStream out) { |
| this.forwardStdOut = out; |
| } |
| |
| private void forwardStream(final String name, final InputStream in, final OutputStream out) { |
| new Thread("Stream forwarder [" + name + "]") { |
| @Override |
| public void run() { |
| try { |
| while (!isFinished()) { |
| while (in.available() > 0) { |
| out.write(in.read()); |
| } |
| synchronized (this) { |
| this.wait(100); |
| } |
| } |
| out.flush(); |
| } catch (IOException | InterruptedException e) { |
| //TODO only log/show if debug is on |
| e.printStackTrace(); |
| } |
| } |
| }.start(); |
| } |
| |
| /** |
| * Returns the controled process. Will return <code>null</code> before |
| * <code>execute</code> is called. |
| * |
| * @return the underlying process |
| */ |
| public Process getProcess() { |
| return process; |
| } |
| |
| protected synchronized boolean isFinished() { |
| return finished; |
| } |
| |
| /** |
| * Kills the process. Does nothing if it has been finished already. |
| */ |
| public void kill() { |
| synchronized (this) { |
| if (isFinished()) { |
| return; |
| } |
| killed = true; |
| } |
| process.destroy(); |
| } |
| |
| private synchronized void markFinished() { |
| finished = true; |
| notifyAll(); |
| } |
| |
| protected synchronized boolean timedOut() { |
| return System.currentTimeMillis() - startupTime > timeLimit; |
| } |
| |
| /** |
| * Returns whether the process was killed due to a time out. |
| * |
| * @return <code>true</code> if the process was killed, |
| * <code>false</code> if the completed normally |
| */ |
| public boolean wasKilled() { |
| return killed; |
| } |
| |
| /** |
| * Forwards the given input stream to the process standard input. |
| * Must be called before execution has started. |
| * |
| * @param in an input stream where the process |
| * standard input will be forwarded to |
| */ |
| public void forwardInput(InputStream in) { |
| forwardStdIn = in; |
| } |
| } |