hudson.Proc.java Source code

Java tutorial

Introduction

Here is the source code for hudson.Proc.java

Source

/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, CloudBees, Inc.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson;

import hudson.Launcher.ProcStarter;
import hudson.model.TaskListener;
import hudson.remoting.Channel;
import hudson.util.DaemonThreadFactory;
import hudson.util.ExceptionCatchingThreadFactory;
import hudson.util.NamingThreadFactory;
import hudson.util.NullStream;
import hudson.util.StreamCopyThread;
import hudson.util.ProcessTree;
import org.apache.commons.io.input.NullInputStream;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * External process wrapper.
 *
 * <p>
 * Used for launching, monitoring, waiting for a process.
 *
 * @author Kohsuke Kawaguchi
 */
public abstract class Proc {
    protected Proc() {
    }

    /**
     * Checks if the process is still alive.
     */
    public abstract boolean isAlive() throws IOException, InterruptedException;

    /**
     * Terminates the process.
     *
     * @throws IOException
     *      if there's an error killing a process
     *      and a stack trace could help the trouble-shooting.
     */
    public abstract void kill() throws IOException, InterruptedException;

    /**
     * Waits for the completion of the process.
     *
     * Unless the caller opts to pump the streams via {@link #getStdout()} etc.,
     * this method also blocks until we finish reading everything that the process has produced
     * to stdout/stderr.
     *
     * <p>
     * If the thread is interrupted while waiting for the completion
     * of the process, this method terminates the process and
     * exits with a non-zero exit code.
     *
     * @throws IOException
     *      if there's an error launching/joining a process
     *      and a stack trace could help the trouble-shooting.
     */
    public abstract int join() throws IOException, InterruptedException;

    /**
     * Returns an {@link InputStream} to read from {@code stdout} of the child process.
     * <p>
     * When this method returns null, {@link Proc} will internally pump the output from
     * the child process to your {@link OutputStream} of choosing.
     *
     * @return
     *      null unless {@link ProcStarter#readStdout()} is used to indicate
     *      that the caller intends to pump the stream by itself.
     * @since 1.399
     */
    public abstract InputStream getStdout();

    /**
     * Returns an {@link InputStream} to read from {@code stderr} of the child process.
     * <p>
     * When this method returns null, {@link Proc} will internally pump the output from
     * the child process to your {@link OutputStream} of choosing.
     *
     * @return
     *      null unless {@link ProcStarter#readStderr()} is used to indicate
     *      that the caller intends to pump the stream by itself.
     * @since 1.399
     */
    public abstract InputStream getStderr();

    /**
     * Returns an {@link OutputStream} to write to {@code stdin} of the child process.
     * <p>
     * When this method returns null, {@link Proc} will internally pump the {@link InputStream}
     * of your choosing to the child process.
     *
     * @return
     *      null unless {@link ProcStarter#writeStdin()} is used to indicate
     *      that the caller intends to pump the stream by itself.
     * @since 1.399
     */
    public abstract OutputStream getStdin();

    private static final ExecutorService executor = Executors
            .newCachedThreadPool(new ExceptionCatchingThreadFactory(
                    new NamingThreadFactory(new DaemonThreadFactory(), "Proc.executor")));

    /**
     * Like {@link #join} but can be given a maximum time to wait.
     * @param timeout number of time units
     * @param unit unit of time
     * @param listener place to send messages if there are problems, incl. timeout
     * @return exit code from the process
     * @throws IOException for the same reasons as {@link #join}
     * @throws InterruptedException for the same reasons as {@link #join}
     * @since 1.363
     */
    public final int joinWithTimeout(final long timeout, final TimeUnit unit, final TaskListener listener)
            throws IOException, InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        try {
            executor.submit(new Runnable() {
                public void run() {
                    try {
                        if (!latch.await(timeout, unit)) {
                            listener.error(
                                    "Timeout after " + timeout + " " + unit.toString().toLowerCase(Locale.ENGLISH));
                            kill();
                        }
                    } catch (InterruptedException x) {
                        x.printStackTrace(listener.error("Failed to join a process"));
                    } catch (IOException x) {
                        x.printStackTrace(listener.error("Failed to join a process"));
                    } catch (RuntimeException x) {
                        x.printStackTrace(listener.error("Failed to join a process"));
                    }
                }
            });
            return join();
        } finally {
            latch.countDown();
        }
    }

    /**
     * Locally launched process.
     */
    public static final class LocalProc extends Proc {
        private final Process proc;
        private final Thread copier, copier2;
        private final OutputStream out;
        private final EnvVars cookie;
        private final String name;

        private final InputStream stdout, stderr;
        private final OutputStream stdin;

        public LocalProc(String cmd, Map<String, String> env, OutputStream out, File workDir) throws IOException {
            this(cmd, Util.mapToEnv(env), out, workDir);
        }

        public LocalProc(String[] cmd, Map<String, String> env, InputStream in, OutputStream out)
                throws IOException {
            this(cmd, Util.mapToEnv(env), in, out);
        }

        public LocalProc(String cmd, String[] env, OutputStream out, File workDir) throws IOException {
            this(Util.tokenize(cmd), env, out, workDir);
        }

        public LocalProc(String[] cmd, String[] env, OutputStream out, File workDir) throws IOException {
            this(cmd, env, null, out, workDir);
        }

        public LocalProc(String[] cmd, String[] env, InputStream in, OutputStream out) throws IOException {
            this(cmd, env, in, out, null);
        }

        public LocalProc(String[] cmd, String[] env, InputStream in, OutputStream out, File workDir)
                throws IOException {
            this(cmd, env, in, out, null, workDir);
        }

        /**
         * @param err
         *      null to redirect stderr to stdout.
         */
        public LocalProc(String[] cmd, String[] env, InputStream in, OutputStream out, OutputStream err,
                File workDir) throws IOException {
            this(calcName(cmd), stderr(environment(new ProcessBuilder(cmd), env).directory(workDir),
                    err == null || err == SELFPUMP_OUTPUT), in, out, err);
        }

        private static ProcessBuilder stderr(ProcessBuilder pb, boolean redirectError) {
            if (redirectError)
                pb.redirectErrorStream(true);
            return pb;
        }

        private static ProcessBuilder environment(ProcessBuilder pb, String[] env) {
            if (env != null) {
                Map<String, String> m = pb.environment();
                m.clear();
                for (String e : env) {
                    int idx = e.indexOf('=');
                    m.put(e.substring(0, idx), e.substring(idx + 1, e.length()));
                }
            }
            return pb;
        }

        private LocalProc(String name, ProcessBuilder procBuilder, InputStream in, OutputStream out,
                OutputStream err) throws IOException {
            Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name);
            this.name = name;
            this.out = out;
            this.cookie = EnvVars.createCookie();
            procBuilder.environment().putAll(cookie);
            this.proc = procBuilder.start();

            InputStream procInputStream = proc.getInputStream();
            if (out == SELFPUMP_OUTPUT) {
                stdout = procInputStream;
                copier = null;
            } else {
                copier = new StreamCopyThread(name + ": stdout copier", procInputStream, out);
                copier.start();
                stdout = null;
            }

            if (in == null) {
                // nothing to feed to stdin
                stdin = null;
                proc.getOutputStream().close();
            } else if (in == SELFPUMP_INPUT) {
                stdin = proc.getOutputStream();
            } else {
                new StdinCopyThread(name + ": stdin copier", in, proc.getOutputStream()).start();
                stdin = null;
            }

            InputStream procErrorStream = proc.getErrorStream();
            if (err != null) {
                if (err == SELFPUMP_OUTPUT) {
                    stderr = procErrorStream;
                    copier2 = null;
                } else {
                    stderr = null;
                    copier2 = new StreamCopyThread(name + ": stderr copier", procErrorStream, err);
                    copier2.start();
                }
            } else {
                // the javadoc is unclear about what getErrorStream() returns when ProcessBuilder.redirectErrorStream(true),
                //
                // according to the source code, Sun JREs still still returns a distinct reader end of a pipe that needs to be closed.
                // but apparently at least on some IBM JDK5, returned input and error streams are the same.
                // so try to close them smartly
                if (procErrorStream != procInputStream) {
                    procErrorStream.close();
                }
                copier2 = null;
                stderr = null;
            }
        }

        public InputStream getStdout() {
            return stdout;
        }

        public InputStream getStderr() {
            return stderr;
        }

        public OutputStream getStdin() {
            return stdin;
        }

        /**
         * Waits for the completion of the process.
         */
        @Override
        public int join() throws InterruptedException, IOException {
            // show what we are waiting for in the thread title
            // since this involves some native work, let's have some soak period before enabling this by default 
            Thread t = Thread.currentThread();
            String oldName = t.getName();
            if (SHOW_PID) {
                ProcessTree.OSProcess p = ProcessTree.get().get(proc);
                t.setName(oldName + " " + (p != null ? "waiting for pid=" + p.getPid() : "waiting for " + name));
            }

            try {
                int r = proc.waitFor();
                // see http://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build
                // problems like that shows up as infinite wait in join(), which confuses great many users.
                // So let's do a timed wait here and try to diagnose the problem
                if (copier != null)
                    copier.join(10 * 1000);
                if (copier2 != null)
                    copier2.join(10 * 1000);
                if ((copier != null && copier.isAlive()) || (copier2 != null && copier2.isAlive())) {
                    // looks like handles are leaking.
                    // closing these handles should terminate the threads.
                    String msg = "Process leaked file descriptors. See http://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build for more information";
                    Throwable e = new Exception().fillInStackTrace();
                    LOGGER.log(Level.WARNING, msg, e);

                    // doing proc.getInputStream().close() hangs in FileInputStream.close0()
                    // it could be either because another thread is blocking on read, or
                    // it could be a bug in Windows JVM. Who knows.
                    // so I'm abandoning the idea of closing the stream
                    //                    try {
                    //                        proc.getInputStream().close();
                    //                    } catch (IOException x) {
                    //                        LOGGER.log(Level.FINE,"stdin termination failed",x);
                    //                    }
                    //                    try {
                    //                        proc.getErrorStream().close();
                    //                    } catch (IOException x) {
                    //                        LOGGER.log(Level.FINE,"stderr termination failed",x);
                    //                    }
                    out.write(msg.getBytes());
                    out.write('\n');
                }
                return r;
            } catch (InterruptedException e) {
                // aborting. kill the process
                destroy();
                throw e;
            } finally {
                t.setName(oldName);
            }
        }

        @Override
        public boolean isAlive() throws IOException, InterruptedException {
            try {
                proc.exitValue();
                return false;
            } catch (IllegalThreadStateException e) {
                return true;
            }
        }

        @Override
        public void kill() throws InterruptedException, IOException {
            destroy();
            join();
        }

        /**
         * Destroys the child process without join.
         */
        private void destroy() throws InterruptedException {
            ProcessTree.get().killAll(proc, cookie);
        }

        /**
         * {@link Process#getOutputStream()} is buffered, so we need to eagerly flash
         * the stream to push bytes to the process.
         */
        private static class StdinCopyThread extends Thread {
            private final InputStream in;
            private final OutputStream out;

            public StdinCopyThread(String threadName, InputStream in, OutputStream out) {
                super(threadName);
                this.in = in;
                this.out = out;
            }

            @Override
            public void run() {
                try {
                    try {
                        byte[] buf = new byte[8192];
                        int len;
                        while ((len = in.read(buf)) >= 0) {
                            out.write(buf, 0, len);
                            out.flush();
                        }
                    } finally {
                        in.close();
                        out.close();
                    }
                } catch (IOException e) {
                    // TODO: what to do?
                }
            }
        }

        private static String calcName(String[] cmd) {
            StringBuilder buf = new StringBuilder();
            for (String token : cmd) {
                if (buf.length() > 0)
                    buf.append(' ');
                buf.append(token);
            }
            return buf.toString();
        }

        public static final InputStream SELFPUMP_INPUT = new NullInputStream(0);
        public static final OutputStream SELFPUMP_OUTPUT = new NullStream();
    }

    /**
     * Remotely launched process via {@link Channel}.
     *
     * @deprecated as of 1.399. Replaced by {@link Launcher.RemoteLauncher.ProcImpl}
     */
    @Deprecated
    public static final class RemoteProc extends Proc {
        private final Future<Integer> process;

        public RemoteProc(Future<Integer> process) {
            this.process = process;
        }

        @Override
        public void kill() throws IOException, InterruptedException {
            process.cancel(true);
        }

        @Override
        public int join() throws IOException, InterruptedException {
            try {
                return process.get();
            } catch (InterruptedException e) {
                // aborting. kill the process
                process.cancel(true);
                throw e;
            } catch (ExecutionException e) {
                if (e.getCause() instanceof IOException)
                    throw (IOException) e.getCause();
                throw new IOException("Failed to join the process", e);
            } catch (CancellationException x) {
                return -1;
            }
        }

        @Override
        public boolean isAlive() throws IOException, InterruptedException {
            return !process.isDone();
        }

        @Override
        public InputStream getStdout() {
            return null;
        }

        @Override
        public InputStream getStderr() {
            return null;
        }

        @Override
        public OutputStream getStdin() {
            return null;
        }
    }

    private static final Logger LOGGER = Logger.getLogger(Proc.class.getName());
    /**
     * Debug switch to have the thread display the process it's waiting for.
     */
    public static boolean SHOW_PID = false;
}