PageRenderTime 42ms CodeModel.GetById 14ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/hudson-core/src/main/java/hudson/Proc.java

http://github.com/hudson/hudson
Java | 376 lines | 228 code | 34 blank | 114 comment | 16 complexity | e256bdec1bd4afb79ce6ad7a35dd0310 MD5 | raw file
  1/*
  2 * The MIT License
  3 * 
  4 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
  5 * 
  6 * Permission is hereby granted, free of charge, to any person obtaining a copy
  7 * of this software and associated documentation files (the "Software"), to deal
  8 * in the Software without restriction, including without limitation the rights
  9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10 * copies of the Software, and to permit persons to whom the Software is
 11 * furnished to do so, subject to the following conditions:
 12 * 
 13 * The above copyright notice and this permission notice shall be included in
 14 * all copies or substantial portions of the Software.
 15 * 
 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 22 * THE SOFTWARE.
 23 */
 24package hudson;
 25
 26import hudson.model.TaskListener;
 27import hudson.util.IOException2;
 28import hudson.util.StreamCopyThread;
 29import hudson.util.ProcessTree;
 30
 31import java.io.File;
 32import java.io.IOException;
 33import java.io.InputStream;
 34import java.io.OutputStream;
 35import java.util.Locale;
 36import java.util.Map;
 37import java.util.concurrent.CancellationException;
 38import java.util.concurrent.CountDownLatch;
 39import java.util.concurrent.ExecutionException;
 40import java.util.concurrent.ExecutorService;
 41import java.util.concurrent.Executors;
 42import java.util.concurrent.Future;
 43import java.util.concurrent.TimeUnit;
 44import java.util.logging.Level;
 45import java.util.logging.Logger;
 46
 47/**
 48 * External process wrapper.
 49 *
 50 * <p>
 51 * Used for launching, monitoring, waiting for a process.
 52 *
 53 * @author Kohsuke Kawaguchi
 54 */
 55public abstract class Proc {
 56    protected Proc() {}
 57
 58    /**
 59     * Checks if the process is still alive.
 60     */
 61    public abstract boolean isAlive() throws IOException, InterruptedException;
 62
 63    /**
 64     * Terminates the process.
 65     *
 66     * @throws IOException
 67     *      if there's an error killing a process
 68     *      and a stack trace could help the trouble-shooting.
 69     */
 70    public abstract void kill() throws IOException, InterruptedException;
 71
 72    /**
 73     * Waits for the completion of the process and until we finish reading everything that the process has produced
 74     * to stdout/stderr.
 75     *
 76     * <p>
 77     * If the thread is interrupted while waiting for the completion
 78     * of the process, this method terminates the process and
 79     * exits with a non-zero exit code.
 80     *
 81     * @throws IOException
 82     *      if there's an error launching/joining a process
 83     *      and a stack trace could help the trouble-shooting.
 84     */
 85    public abstract int join() throws IOException, InterruptedException;
 86
 87    private static final ExecutorService executor = Executors.newCachedThreadPool();
 88    /**
 89     * Like {@link #join} but can be given a maximum time to wait.
 90     * @param timeout number of time units
 91     * @param unit unit of time
 92     * @param listener place to send messages if there are problems, incl. timeout
 93     * @return exit code from the process
 94     * @throws IOException for the same reasons as {@link #join}
 95     * @throws InterruptedException for the same reasons as {@link #join}
 96     * @since 1.363
 97     */
 98    public final int joinWithTimeout(final long timeout, final TimeUnit unit,
 99            final TaskListener listener) throws IOException, InterruptedException {
100        final CountDownLatch latch = new CountDownLatch(1);
101        try {
102            executor.submit(new Runnable() {
103                public void run() {
104                    try {
105                        if (!latch.await(timeout, unit)) {
106                            listener.error("Timeout after " + timeout + " " +
107                                    unit.toString().toLowerCase(Locale.ENGLISH));
108                            kill();
109                        }
110                    } catch (InterruptedException x) {
111                        listener.error(x.toString());
112                    } catch (IOException x) {
113                        listener.error(x.toString());
114                    } catch (RuntimeException x) {
115                        listener.error(x.toString());
116                    }
117                }
118            });
119            return join();
120        } finally {
121            latch.countDown();
122        }
123    }
124    
125    /**
126     * Locally launched process.
127     */
128    public static final class LocalProc extends Proc {
129        private final Process proc;
130        private final Thread copier,copier2;
131        private final OutputStream out;
132        private final EnvVars cookie;
133        private final String name;
134
135        public LocalProc(String cmd, Map<String,String> env, OutputStream out, File workDir) throws IOException {
136            this(cmd,Util.mapToEnv(env),out,workDir);
137        }
138
139        public LocalProc(String[] cmd, Map<String,String> env,InputStream in, OutputStream out) throws IOException {
140            this(cmd,Util.mapToEnv(env),in,out);
141        }
142
143        public LocalProc(String cmd,String[] env,OutputStream out, File workDir) throws IOException {
144            this( Util.tokenize(cmd), env, out, workDir );
145        }
146
147        public LocalProc(String[] cmd,String[] env,OutputStream out, File workDir) throws IOException {
148            this(cmd,env,null,out,workDir);
149        }
150
151        public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out) throws IOException {
152            this(cmd,env,in,out,null);
153        }
154
155        public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out, File workDir) throws IOException {
156            this(cmd,env,in,out,null,workDir);
157        }
158
159        /**
160         * @param err
161         *      null to redirect stderr to stdout.
162         */
163        public LocalProc(String[] cmd,String[] env,InputStream in,OutputStream out,OutputStream err,File workDir) throws IOException {
164            this( calcName(cmd),
165                  stderr(environment(new ProcessBuilder(cmd),env).directory(workDir),err),
166                  in, out, err );
167        }
168
169        private static ProcessBuilder stderr(ProcessBuilder pb, OutputStream stderr) {
170            if(stderr==null)    pb.redirectErrorStream(true);
171            return pb;
172        }
173
174        private static ProcessBuilder environment(ProcessBuilder pb, String[] env) {
175            if(env!=null) {
176                Map<String, String> m = pb.environment();
177                m.clear();
178                for (String e : env) {
179                    int idx = e.indexOf('=');
180                    m.put(e.substring(0,idx),e.substring(idx+1,e.length()));
181                }
182            }
183            return pb;
184        }
185
186        private LocalProc( String name, ProcessBuilder procBuilder, InputStream in, OutputStream out, OutputStream err ) throws IOException {
187            Logger.getLogger(Proc.class.getName()).log(Level.FINE, "Running: {0}", name);
188            this.name = name;
189            this.out = out;
190            this.cookie = EnvVars.createCookie();
191            procBuilder.environment().putAll(cookie);
192            this.proc = procBuilder.start();
193            copier = new StreamCopyThread(name+": stdout copier", proc.getInputStream(), out);
194            copier.start();
195            if(in!=null)
196                new StdinCopyThread(name+": stdin copier",in,proc.getOutputStream()).start();
197            else
198                proc.getOutputStream().close();
199            if(err!=null) {
200                copier2 = new StreamCopyThread(name+": stderr copier", proc.getErrorStream(), err);
201                copier2.start();
202            } else {
203                // while this is not discussed in javadoc, even with ProcessBuilder.redirectErrorStream(true),
204                // Process.getErrorStream() still returns a distinct reader end of a pipe that needs to be closed.
205                // this is according to the source code of JVM
206                proc.getErrorStream().close();
207                copier2 = null;
208            }
209        }
210
211        /**
212         * Waits for the completion of the process.
213         */
214        @Override
215        public int join() throws InterruptedException, IOException {
216            // show what we are waiting for in the thread title
217            // since this involves some native work, let's have some soak period before enabling this by default 
218            Thread t = Thread.currentThread();
219            String oldName = t.getName();
220            if (SHOW_PID) {
221                ProcessTree.OSProcess p = ProcessTree.get().get(proc);
222                t.setName(oldName+" "+(p!=null?"waiting for pid="+p.getPid():"waiting for "+name));
223            }
224
225            try {
226                int r = proc.waitFor();
227                // see http://wiki.hudson-ci.org/display/HUDSON/Spawning+processes+from+build
228                // problems like that shows up as infinite wait in join(), which confuses great many users.
229                // So let's do a timed wait here and try to diagnose the problem
230                copier.join(10*1000);
231                if(copier2!=null)   copier2.join(10*1000);
232                if(copier.isAlive() || (copier2!=null && copier2.isAlive())) {
233                    // looks like handles are leaking.
234                    // closing these handles should terminate the threads.
235                    String msg = "Process leaked file descriptors. See http://wiki.hudson-ci.org/display/HUDSON/Spawning+processes+from+build for more information";
236                    Throwable e = new Exception().fillInStackTrace();
237                    LOGGER.log(Level.WARNING,msg,e);
238
239                    // doing proc.getInputStream().close() hangs in FileInputStream.close0()
240                    // it could be either because another thread is blocking on read, or
241                    // it could be a bug in Windows JVM. Who knows.
242                    // so I'm abandoning the idea of closing the stream
243//                    try {
244//                        proc.getInputStream().close();
245//                    } catch (IOException x) {
246//                        LOGGER.log(Level.FINE,"stdin termination failed",x);
247//                    }
248//                    try {
249//                        proc.getErrorStream().close();
250//                    } catch (IOException x) {
251//                        LOGGER.log(Level.FINE,"stderr termination failed",x);
252//                    }
253                    out.write(msg.getBytes());
254                    out.write('\n');
255                }
256                return r;
257            } catch (InterruptedException e) {
258                // aborting. kill the process
259                destroy();
260                throw e;
261            } finally {
262                t.setName(oldName);
263            }
264        }
265
266        @Override
267        public boolean isAlive() throws IOException, InterruptedException {
268            try {
269                proc.exitValue();
270                return false;
271            } catch (IllegalThreadStateException e) {
272                return true;
273            }
274        }
275
276        @Override
277        public void kill() throws InterruptedException, IOException {
278            destroy();
279            join();
280        }
281
282        /**
283         * Destroys the child process without join.
284         */
285        private void destroy() throws InterruptedException {
286            ProcessTree.get().killAll(proc,cookie);
287        }
288
289        /**
290         * {@link Process#getOutputStream()} is buffered, so we need to eagerly flash
291         * the stream to push bytes to the process.
292         */
293        private static class StdinCopyThread extends Thread {
294            private final InputStream in;
295            private final OutputStream out;
296
297            public StdinCopyThread(String threadName, InputStream in, OutputStream out) {
298                super(threadName);
299                this.in = in;
300                this.out = out;
301            }
302
303            @Override
304            public void run() {
305                try {
306                    try {
307                        byte[] buf = new byte[8192];
308                        int len;
309                        while ((len = in.read(buf)) > 0) {
310                            out.write(buf, 0, len);
311                            out.flush();
312                        }
313                    } finally {
314                        in.close();
315                        out.close();
316                    }
317                } catch (IOException e) {
318                    // TODO: what to do?
319                }
320            }
321        }
322
323        private static String calcName(String[] cmd) {
324            StringBuilder buf = new StringBuilder();
325            for (String token : cmd) {
326                if(buf.length()>0)  buf.append(' ');
327                buf.append(token);
328            }
329            return buf.toString();
330        }
331    }
332
333    /**
334     * Retemoly launched process via {@link Channel}.
335     */
336    public static final class RemoteProc extends Proc {
337        private final Future<Integer> process;
338
339        public RemoteProc(Future<Integer> process) {
340            this.process = process;
341        }
342
343        @Override
344        public void kill() throws IOException, InterruptedException {
345            process.cancel(true);
346        }
347
348        @Override
349        public int join() throws IOException, InterruptedException {
350            try {
351                return process.get();
352            } catch (InterruptedException e) {
353                // aborting. kill the process
354                process.cancel(true);
355                throw e;
356            } catch (ExecutionException e) {
357                if(e.getCause() instanceof IOException)
358                    throw (IOException)e.getCause();
359                throw new IOException2("Failed to join the process",e);
360            } catch (CancellationException x) {
361                return -1;
362            }
363        }
364
365        @Override
366        public boolean isAlive() throws IOException, InterruptedException {
367            return !process.isDone();
368        }
369    }
370
371    private static final Logger LOGGER = Logger.getLogger(Proc.class.getName());
372    /**
373     * Debug switch to have the thread display the process it's waiting for.
374     */
375    public static boolean SHOW_PID = false;
376}