001package com.randomnoun.common;
002
003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
005 */
006
007import java.io.ByteArrayOutputStream;
008import java.io.File;
009import java.io.IOException;
010import java.io.InputStream;
011import java.io.OutputStream;
012import java.util.Map;
013
014/** Utility class for running processes, including timeouts, and slightly better exceptions that include more information
015 * about process failure. 
016 * 
017 */
018public class ProcessUtil {
019    
020        /// maximum output; if >0, removes passwords & limits to this amount
021        private int maxOutputChars = 8000;
022        
023        public static int NO_MAX_OUTPUT_CHARS = 0;
024        
025        public void setMaxOutputChars(int maxOutputChars) {
026                this.maxOutputChars = maxOutputChars;
027        }
028        
029        /** Encapsulates an error from executing a command through System.exec()
030         * 
031         */
032        public class ProcessException extends Exception {
033                
034                /** Generated serialVersionUID */
035                private static final long serialVersionUID = -6301630237335589674L;
036                
037                private String command;
038                private int exitCode;
039                private String stdout;
040                private String stderr;
041                private String exitCause;
042
043                /** Create a new executable exception
044                 * 
045                 * @param command Command being executed
046                 * @param hostname The host the command was being executed on
047                 * @param exitCode The exit code of the program
048                 * @param stdout The standard output of the program
049                 * @param stderr The error output of the program
050                 */
051                public ProcessException(String command, String exitCause, int exitCode, String stdout, String stderr) {
052                        super("Error executing '" + command + "'" + ", cause='" + exitCause + "'");
053                        
054                        this.command = command;
055                        this.exitCode = exitCode;
056                        this.exitCause = exitCause;
057                        this.stdout = stdout;
058                        this.stderr = stderr;
059                }
060                public String getStdout() { return stdout; }
061                public String getStderr() { return stderr; }
062                public String getCommand() { return command; }
063                public int getExitCode() { return exitCode; }
064                public String getExitCause() { return exitCause; }
065                public String getMessage() {
066                        // trim stdout/stderr
067                        if (maxOutputChars>0) { stdout = Text.getDisplayString("stdout", stdout, 8000); }
068                        if (maxOutputChars>0) { stderr = Text.getDisplayString("stderr", stderr, 8000); }
069                        return super.getMessage() +
070                          "; " + 
071                          (stdout==null ? "" : ", stdout='" + stdout + "'\n") +
072                      (stderr==null ? "" : ", stderr='" + stderr + "'\n") +
073                      "exitCode=" + exitCode;
074                }
075        }
076        
077        
078
079        public String exec(String[] command) throws ProcessException {
080                return exec(command, -1, null, null, null);
081        }
082        
083        public String exec(String[] command, Map<String, String> env) throws ProcessException {
084                return exec(command, -1, null, env, null);
085        }
086
087        public String exec(String[] command, long timeout, InputStream stdin, Map<String, String> envMap, /*String[] env, */ File dir) throws ProcessException {
088                Process process;
089                try {
090                        ProcessBuilder pb = new ProcessBuilder(command);
091                        if (dir != null) {
092                                pb.directory(dir);
093                        }
094                        if (envMap != null) {
095                                pb.environment().clear();
096                                pb.environment().putAll(envMap);
097                        }
098                        process = pb.start();
099                } catch (IOException ioe) {
100                        throw (ProcessException) new ProcessException(Text.join(command, " "), "IOException", 0, "", "").initCause(ioe);
101                }
102                InputStream stdout = process.getInputStream();
103                InputStream stderr = process.getErrorStream();
104                OutputStream processStdin = process.getOutputStream();
105                ByteArrayOutputStream stdoutByteArrayStream = new ByteArrayOutputStream();
106                ByteArrayOutputStream stderrByteArrayStream = new ByteArrayOutputStream();
107                OutputStream stdoutStream = stdoutByteArrayStream;
108                OutputStream stderrStream = stderrByteArrayStream;
109                // OutputStream stdoutStream = new TeeOutputStream(stdoutByteArrayStream, new LoggingOutputStream(stdoutLogger));
110                // OutputStream stderrStream = new TeeOutputStream(stderrByteArrayStream, new LoggingOutputStream(stderrLogger));
111                
112                // could probably use nio these days
113                Thread copyStdoutThread = StreamUtil.copyThread(stdout, stdoutStream, 1024);
114                Thread copyStderrThread = StreamUtil.copyThread(stderr, stderrStream, 1024);
115                Thread copyStdinThread = null;
116                if (stdin!=null) { 
117                        copyStdinThread = StreamUtil.copyAndCloseThread(stdin, processStdin, 1024);
118                        copyStdinThread.start();
119                }
120                
121                copyStdoutThread.start();
122                copyStderrThread.start();
123                int exitCode = -1;
124                boolean throwException = false;
125                long interval = 100;
126                String cause = "";
127                
128                try {
129                        // timeouts
130                        long    timeWaiting = 0;
131                        boolean processFinished = false;
132                        while ((timeout == -1 || timeWaiting < timeout) && !processFinished) {
133                                processFinished = true;
134                                Thread.sleep(interval);
135                                try {
136                                        exitCode = process.exitValue();
137                                } catch (IllegalThreadStateException e) {
138                                        // process hasn't finished yet
139                                        processFinished = false;
140                                }
141                                timeWaiting += interval;
142                        }
143                        
144                        if (processFinished) {
145                                // wait for copy threads to complete
146                                copyStdoutThread.join();
147                                copyStderrThread.join();  
148                                if (copyStdinThread != null) {
149                                        copyStdinThread.interrupt();
150                                }
151                                if (exitCode != 0) {
152                                        cause = "non-0 exitCode";
153                                        throwException = true;
154                                }
155                        } else {
156                                cause = "timeout";
157                                throwException = true;
158                                process.destroy(); 
159                                if (copyStdinThread != null) {
160                                        copyStdinThread.interrupt();
161                                }
162                                copyStderrThread.interrupt();
163                                copyStdoutThread.interrupt();
164                        }                       
165                        
166                } catch (InterruptedException ie) {
167                        cause = "InterruptedException";
168                        throwException = true;
169                        process.destroy();
170                        if (copyStdinThread!=null) {
171                                copyStdinThread.interrupt();
172                        }
173                        copyStderrThread.interrupt();
174                        copyStdoutThread.interrupt();                   
175                }
176                
177                if (throwException) {
178                        throw new ProcessException(
179                                Text.join(command, " "), cause, exitCode,  
180                                stdoutByteArrayStream.toString(), stderrByteArrayStream.toString());
181                }
182                return stdoutByteArrayStream.toString();
183        }       
184        
185        
186        
187        
188}