/******************************************************************************* * Copyright (c) 2000, 2011 QNX Software Systems 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: * QNX Software Systems - Initial API and implementation * Wind River Systems - bug 248071, bug 286162 *******************************************************************************/ package org.eclipse.cdt.utils.spawner; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.StringTokenizer; import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.utils.pty.PTY; import org.eclipse.core.runtime.Platform; import org.eclipse.osgi.util.NLS; public class Spawner extends Process { public int NOOP = 0; public int HUP = 1; public int KILL = 9; public int TERM = 15; /** * On Windows, what this does is far from easy to explain. * Some of the logic is in the JNI code, some in the spawner.exe code. * * <ul> * <li>If the process this is being raised against was launched by us (the Spawner) * <ul> * <li>If the process is a cygwin program (has the cygwin1.dll loaded), then issue a 'kill -SIGINT'. If * the 'kill' utility isn't available, send the process a CTRL-C * <li>If the process is <i>not</i> a cygwin program, send the process a CTRL-C * </ul> * <li>If the process this is being raised against was <i>not</i> launched by us, use * DebugBreakProcess to interrupt it (sending a CTRL-C is easy only if we share a console * with the target process) * </ul> * * On non-Windows, raising this just raises a POSIX SIGINT * */ public int INT = 2; /** * A fabricated signal number for use on Windows only. Tells the starter program to send a CTRL-C * regardless of whether the process is a Cygwin one or not. * * @since 5.2 */ public int CTRLC = 1000; // arbitrary high number to avoid collision int pid = 0; int status; final int[] fChannels = new int[3]; boolean isDone; OutputStream out; InputStream in; InputStream err; private PTY fPty; public Spawner(String command, boolean bNoRedirect) throws IOException { StringTokenizer tokenizer = new StringTokenizer(command); String[] cmdarray = new String[tokenizer.countTokens()]; for (int n = 0; tokenizer.hasMoreTokens(); n++) cmdarray[n] = tokenizer.nextToken(); if (bNoRedirect) exec_detached(cmdarray, new String[0], "."); //$NON-NLS-1$ else exec(cmdarray, new String[0], "."); //$NON-NLS-1$ } /** * Executes the specified command and arguments in a separate process with the * specified environment and working directory. **/ protected Spawner(String[] cmdarray, String[] envp, File dir) throws IOException { String dirpath = "."; //$NON-NLS-1$ if (dir != null) dirpath = dir.getAbsolutePath(); exec(cmdarray, envp, dirpath); } protected Spawner(String[] cmdarray, String[] envp, File dir, PTY pty) throws IOException { String dirpath = "."; //$NON-NLS-1$ if (dir != null) dirpath = dir.getAbsolutePath(); fPty = pty; exec_pty(cmdarray, envp, dirpath, pty); } /** * Executes the specified string command in a separate process. **/ protected Spawner(String command) throws IOException { this(command, null); } /** * Executes the specified command and arguments in a separate process. **/ protected Spawner(String[] cmdarray) throws IOException { this(cmdarray, null); } /** * Executes the specified command and arguments in a separate process with the * specified environment. **/ protected Spawner(String[] cmdarray, String[] envp) throws IOException { this(cmdarray, envp, null); } /** * Executes the specified string command in a separate process with the specified * environment. **/ protected Spawner(String cmd, String[] envp) throws IOException { this(cmd, envp, null); } /** * Executes the specified string command in a separate process with the specified * environment and working directory. **/ protected Spawner(String command, String[] envp, File dir) throws IOException { StringTokenizer tokenizer = new StringTokenizer(command); String[] cmdarray = new String[tokenizer.countTokens()]; for (int n = 0; tokenizer.hasMoreTokens(); n++) cmdarray[n] = tokenizer.nextToken(); String dirpath = "."; //$NON-NLS-1$ if (dir != null) dirpath = dir.getAbsolutePath(); exec(cmdarray, envp, dirpath); } @Override protected void finalize() throws Throwable { closeUnusedStreams(); } /** * See java.lang.Process#getInputStream (); * The client is responsible for closing the stream explicitly. **/ @Override public synchronized InputStream getInputStream() { if(null == in) { if (fPty != null) { in = fPty.getInputStream(); } else { in = new SpawnerInputStream(fChannels[1]); } } return in; } /** * See java.lang.Process#getOutputStream (); * The client is responsible for closing the stream explicitly. **/ @Override public synchronized OutputStream getOutputStream() { if(null == out) { if (fPty != null) { out = fPty.getOutputStream(); } else { out = new SpawnerOutputStream(fChannels[0]); } } return out; } /** * See java.lang.Process#getErrorStream (); * The client is responsible for closing the stream explicitly. **/ @Override public synchronized InputStream getErrorStream() { if(null == err) { if (fPty != null && !fPty.isConsole()) { // If PTY is used and it's not in "Console" mode, then stderr is // redirected to the PTY's output stream. Therefore, return a // dummy stream for error stream. err = new InputStream() { @Override public int read() throws IOException { return -1; } }; } else { err = new SpawnerInputStream(fChannels[2]); } } return err; } /** * See java.lang.Process#waitFor (); **/ @Override public synchronized int waitFor() throws InterruptedException { while (!isDone) { wait(); } // For situations where the user does not call destroy(), // we try to kill the streams that were not used here. // We check for streams that were not created, we create // them to attach to the pipes, and then we close them // to release the pipes. // Streams that were created by the client need to be // closed by the client itself. // // But 345164 closeUnusedStreams(); return status; } /** * See java.lang.Process#exitValue (); **/ @Override public synchronized int exitValue() { if (!isDone) { throw new IllegalThreadStateException("Process not Terminated"); //$NON-NLS-1$ } return status; } /** * See java.lang.Process#destroy (); * * Clients are responsible for explicitly closing any streams * that they have requested through * getErrorStream(), getInputStream() or getOutputStream() **/ @Override public synchronized void destroy() { // Sends the TERM terminate(); // Close the streams on this side. // // We only close the streams that were // never used by any client. // So, if the stream was not created yet, // we create it ourselves and close it // right away, so as to release the pipe. // Note that even if the stream was never // created, the pipe has been allocated in // native code, so we need to create the // stream and explicitly close it. // // We don't close streams the clients have // created because we don't know when the // client will be finished using them. // It is up to the client to close those // streams. // // But 345164 closeUnusedStreams(); // Grace before using the heavy gone. if (!isDone) { try { wait(1000); } catch (InterruptedException e) { } } if (!isDone) { kill(); } } /** * On Windows, interrupt the spawned program by using Cygwin's utility 'kill -SIGINT' if it's a Cgywin * program, otherwise send it a CTRL-C. If Cygwin's 'kill' command is not available, send a CTRL-C. On * linux, interrupt it by raising a SIGINT. */ public int interrupt() { return raise(pid, INT); } /** * On Windows, interrupt the spawned program by send it a CTRL-C (even if it's a Cygwin program). On * linux, interrupt it by raising a SIGINT. * * @since 5.2 */ public int interruptCTRLC() { if (Platform.getOS().equals(Platform.OS_WIN32)) { return raise(pid, CTRLC); } else { return interrupt(); } } public int hangup() { return raise(pid, HUP); } public int kill() { return raise(pid, KILL); } public int terminate() { return raise(pid, TERM); } public boolean isRunning() { return (raise(pid, NOOP) == 0); } private void exec(String[] cmdarray, String[] envp, String dirpath) throws IOException { String command = cmdarray[0]; SecurityManager s = System.getSecurityManager(); if (s != null) s.checkExec(command); if (envp == null) envp = new String[0]; Reaper reaper = new Reaper(cmdarray, envp, dirpath); reaper.setDaemon(true); reaper.start(); // Wait until the subprocess is started or error. synchronized (this) { while (pid == 0) { try { wait(); } catch (InterruptedException e) { } } } // Check for errors. if (pid == -1) { throw new IOException(reaper.getErrorMessage()); } } private void exec_pty(String[] cmdarray, String[] envp, String dirpath, PTY pty) throws IOException { String command = cmdarray[0]; SecurityManager s = System.getSecurityManager(); if (s != null) s.checkExec(command); if (envp == null) envp = new String[0]; final String slaveName = pty.getSlaveName(); final int masterFD = pty.getMasterFD().getFD(); final boolean console = pty.isConsole(); //int fdm = pty.get Reaper reaper = new Reaper(cmdarray, envp, dirpath) { /* (non-Javadoc) * @see org.eclipse.cdt.utils.spawner.Spawner.Reaper#execute(java.lang.String[], java.lang.String[], java.lang.String, int[]) */ @Override int execute(String[] cmd, String[] env, String dir, int[] channels) throws IOException { return exec2(cmd, env, dir, channels, slaveName, masterFD, console); } }; reaper.setDaemon(true); reaper.start(); // Wait until the subprocess is started or error. synchronized (this) { while (pid == 0) { try { wait(); } catch (InterruptedException e) { } } } // Check for errors. if (pid == -1) { throw new IOException("Exec_tty error:" + reaper.getErrorMessage()); //$NON-NLS-1$ } } public void exec_detached(String[] cmdarray, String[] envp, String dirpath) throws IOException { String command = cmdarray[0]; SecurityManager s = System.getSecurityManager(); if (s != null) s.checkExec(command); if (envp == null) envp = new String[0]; pid = exec1(cmdarray, envp, dirpath); if (pid == -1) { throw new IOException("Exec error"); //$NON-NLS-1$ } fChannels[0] = -1; fChannels[1] = -1; fChannels[2] = -1; } /** * Close any streams not used by clients. */ private synchronized void closeUnusedStreams() { try { if(null == err) getErrorStream().close(); } catch (IOException e) {} try { if(null == in) getInputStream().close(); } catch (IOException e) {} try { if(null == out) getOutputStream().close(); } catch (IOException e) {} } /** * Native method use in normal exec() calls. */ native int exec0( String[] cmdarray, String[] envp, String dir, int[] chan) throws IOException; /** * Native method use in no redirect meaning to streams will created. */ native int exec1( String[] cmdarray, String[] envp, String dir) throws IOException; /** * Native method when executing with a terminal emulation. */ native int exec2( String[] cmdarray, String[] envp, String dir, int[] chan, String slaveName, int masterFD, boolean console) throws IOException; /** * Native method to drop a signal on the process with pid. */ public native int raise(int processID, int sig); /* * Native method to wait(3) for process to terminate. */ native int waitFor(int processID); static { try { System.loadLibrary("spawner"); //$NON-NLS-1$ } catch (SecurityException e) { CCorePlugin.log(e); } catch (UnsatisfiedLinkError e) { CCorePlugin.log(e); } } // Spawn a thread to handle the forking and waiting // We do it this way because on linux the SIGCHLD is // send to the one thread. So do the forking and // the wait in the same thread. class Reaper extends Thread { String[] fCmdarray; String[] fEnvp; String fDirpath; volatile Throwable fException; public Reaper(String[] array, String[] env, String dir) { super("Spawner Reaper"); //$NON-NLS-1$ fCmdarray = array; fEnvp = env; fDirpath = dir; fException = null; } int execute(String[] cmdarray, String[] envp, String dir, int[] channels) throws IOException { return exec0(cmdarray, envp, dir, channels); } @Override public void run() { try { pid = execute(fCmdarray, fEnvp, fDirpath, fChannels); } catch (Exception e) { pid = -1; fException= e; } // Tell spawner that the process started. synchronized (Spawner.this) { Spawner.this.notifyAll(); } if (pid != -1) { // Sync with spawner and notify when done. status = waitFor(pid); synchronized (Spawner.this) { isDone = true; Spawner.this.notifyAll(); } } } public String getErrorMessage() { final String reason= fException != null ? fException.getMessage() : "Unknown reason"; //$NON-NLS-1$ return NLS.bind(CCorePlugin.getResourceString("Util.error.cannotRun"), fCmdarray[0], reason); //$NON-NLS-1$ } } }