/******************************************************************************* * ALMA - Atacama Large Millimeter Array * Copyright (c) ESO - European Southern Observatory, 2011 * (in the framework of the ALMA collaboration). * All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *******************************************************************************/ package alma.acs.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * Continuously reads the stdout and stderr streams of {@link Process} in separate threads, * so that the OS will not block out the JVM, as it could otherwise happen if lines are read from these streams sequentially in one thread. * <p> * This class is only useful if the external process's output is either not interesting at all for the user, * or if the output should be read only after the process has terminated. * Otherwise the streams should be read directly without using this class. * <p> * The {@link #gobble(long, TimeUnit)} call returns either when both stdout and stderr streams deliver a null, * or if the timeout is reached. @TODO check if relying on the null from both streams is * as robust as {@link Process#waitFor()}. * * @author hsommer */ public class ProcessStreamGobbler { private final Process proc; private final List<String> stdout; private final List<String> stderr; private final ThreadFactory tf; private GobblerRunnable runErr; private GobblerRunnable runOut; private volatile boolean DEBUG = false; /** * @param proc The process whose streams we want to read. * @param tf ThreadFactory to be used to create the two threads that read the stdout and stderr streams * @param storeStreamContent If true, then the stdout and stderr content will be stored * and can be read using {@link #getStdout()} and {@link #getStderr()} * @TODO: set max sizes for the buffers */ public ProcessStreamGobbler(Process proc, ThreadFactory tf, boolean storeStreamContent) { this.proc = proc; this.tf = tf; if (storeStreamContent) { stdout = new ArrayList<String>(); stderr = new ArrayList<String>(); } else { stdout = null; stderr = null; } } /** * Starts fetching process output from stdout/stderr, storing it internally * so that it can later be read via {@link #getStdout()} and {@link #getStderr()}. * <p> * Use this method if you do not want to wait for the process to end. * The status can anyway be checked using {@link #hasTerminated()}. */ public void gobbleAsync() { try { gobble(-1, null); } catch (InterruptedException ex) { // this should never happen, since InterruptedException is only thrown while we would wait for the external process, which here we don't. ex.printStackTrace(); } } /** * Starts fetching process output from stdout/stderr, storing it internally * so that it can later be read via {@link #getStdout()} and {@link #getStderr()}. * <p> * Use this method if you want to wait for the process to end, limited by a timeout. * <p> * @TODO: set max sizes for the buffers * * @param timeout maximum time to wait for the process to finish * @param unit unit for the timeout * @return <code>true</code> if returning because the process ended, * otherwise <code>false</code> if the timeout applied (in which case the streams will continue to be read though). * @throws InterruptedException */ public boolean gobble(long timeout, TimeUnit unit) throws InterruptedException { ExecutorService exsrv = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), tf); runOut = new GobblerRunnable(proc.getInputStream(), stdout, "stdout", DEBUG); exsrv.submit(runOut); runErr = new GobblerRunnable(proc.getErrorStream(), stderr, "stderr", DEBUG); exsrv.submit(runErr); if (timeout > 0) { exsrv.shutdown(); return exsrv.awaitTermination(timeout, unit); } else { return false; } } /** * Returns the stdout data read, or null if storeStreamContent==false */ public List<String> getStdout() { return stdout; } /** * Returns the stderr data read, or null if storeStreamContent==false */ public List<String> getStderr() { return stderr; } /** * @return true if there was 1 or more errors reading the stdout or stderr stream. * @throws IllegalStateException if called before {@link #gobble(long, TimeUnit)} or {@link #gobbleAsync()}. */ public boolean hasStreamReadErrors() { if (runOut == null || runErr == null) { throw new IllegalStateException("Cannot call this method before gobbling."); } return (runOut.hasReadError | runErr.hasReadError); } public boolean hasTerminated() { if (runOut == null || runErr == null) { throw new IllegalStateException("Cannot call this method before gobbling."); } return (runOut.hasTerminated | runErr.hasTerminated); } public void setDebug(boolean DEBUG) { this.DEBUG = DEBUG; } private static class GobblerRunnable implements Runnable { private final BufferedReader br; private final List<String> buffer; private boolean hasReadError; private boolean hasTerminated; private String name; private final boolean DEBUG; GobblerRunnable(InputStream stream, List<String> buffer, String name, boolean DEBUG) { br = new BufferedReader(new InputStreamReader(stream)); this.buffer = buffer; hasReadError = false; hasTerminated = false; this.name = name; this.DEBUG = DEBUG; } public void run() { try { String line = null; while ((line = br.readLine()) != null) { if (DEBUG) { System.out.println(name + ": "+ line); } if (buffer != null) { buffer.add(line); } } if (DEBUG) { System.out.println(name + ": done reading"); } } catch (IOException ioe) { ioe.printStackTrace(); hasReadError = true; } finally { // close the BufferedReader and underlying stream. // It should only be closed in this thread, as otherwise // we can get a deadlock if this thread waits on br.readLine() and the other on // the related lock in BufferedReader#close try { br.close(); if (DEBUG) { System.out.println(name + ": streams closed."); } } catch (IOException ex) { if (DEBUG) { System.out.println(name + ": streams failed to close."); } } finally { hasTerminated = true; } } } } }