001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.io;
018
019import java.io.IOException;
020import java.io.OutputStreamWriter;
021import java.io.Writer;
022import java.nio.charset.Charset;
023import java.nio.charset.StandardCharsets;
024import java.util.Arrays;
025import java.util.Timer;
026import java.util.function.BiFunction;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import org.conqat.lib.commons.concurrent.InterruptTimerTask;
031import org.conqat.lib.commons.string.StringUtils;
032import org.conqat.lib.commons.system.SystemUtils;
033
034/**
035 * Executes a system process. Takes care of reading stdout and stderr of the
036 * process in separate threads to avoid blocking.
037 */
038public class ProcessUtils {
039
040        /**
041         * Command (path or name) to mono, to execute .NET programs on non-windows
042         * machines.
043         */
044        public static final String MONO_COMMAND = "mono";
045
046        /** Character set used for process I/O. Important on Windows. */
047        public static Charset CONSOLE_CHARSET = StandardCharsets.UTF_8;
048
049        static {
050                if (SystemUtils.isWindows()) {
051                        try {
052                                ExecutionResult result = execute(new String[] { "chcp.com" });
053                                Matcher matcher = Pattern.compile("\\d+").matcher(result.getStdout());
054                                if (matcher.find()) {
055                                        CONSOLE_CHARSET = Charset.forName("Cp" + matcher.group());
056                                }
057                        } catch (IOException | IllegalArgumentException e) {
058                                // Keep default.
059                        }
060                }
061        }
062
063        /**
064         * Creates a {@link ProcessBuilder} for executing a .net assembly. If the
065         * current OS is not Windows mono will be used for executing the assembly.
066         */
067        public static ProcessBuilder createDotNetProcessBuilder(String... arguments) {
068                ProcessBuilder builder = new ProcessBuilder();
069
070                if (!SystemUtils.isWindows()) {
071                        builder.command().add(MONO_COMMAND);
072                }
073
074                builder.command().addAll(Arrays.asList(arguments));
075
076                return builder;
077        }
078
079        /**
080         * Executes a .net program in a thread-safe way.
081         */
082        public static ExecutionResult executeDotNet(String[] completeArguments) throws IOException {
083                return execute(createDotNetProcessBuilder(completeArguments));
084        }
085
086        /**
087         * Executes a process in a thread-safe way.
088         *
089         * @param completeArguments
090         *            Array of command line arguments to start the process
091         *
092         * @return result of the execution
093         */
094        public static ExecutionResult execute(String[] completeArguments) throws IOException {
095                return execute(completeArguments, null);
096        }
097
098        /**
099         * Executes a process in a thread-safe way.
100         *
101         * @param completeArguments
102         *            Array of command line arguments to start the process
103         * @param input
104         *            String that gets written to stdin
105         *
106         * @return result of the execution
107         */
108        public static ExecutionResult execute(String[] completeArguments, String input) throws IOException {
109                ProcessBuilder builder = new ProcessBuilder(completeArguments);
110                return execute(builder, input);
111        }
112
113        /**
114         * Executes a process in a thread-safe way.
115         *
116         * @param builder
117         *            builder that gets executed
118         * @return result of the execution
119         */
120        public static ExecutionResult execute(ProcessBuilder builder) throws IOException {
121                return execute(builder, null);
122        }
123
124        /**
125         * Executes a process in a thread-safe way.
126         *
127         * @param builder
128         *            builder that gets executed
129         * @param input
130         *            String that gets written to stdin
131         * @return result of the execution
132         */
133        public static ExecutionResult execute(ProcessBuilder builder, String input) throws IOException {
134                return execute(builder, input, -1);
135        }
136
137        /**
138         * Executes a process in a thread-safe way.
139         *
140         * @param builder
141         *            builder that gets executed
142         * @param input
143         *            String that gets written to stdin (may be null).
144         * @param timeOut
145         *            the number to seconds to wait for the process. If this runs
146         *            longer, the process is killed. Passing a value of 0 or less makes
147         *            the method wait forever (until the process finishes normally). To
148         *            find out whether the process was killed, query
149         *            {@link ExecutionResult#isNormalTermination()}.
150         *
151         * @return result of the execution
152         */
153        public static ExecutionResult execute(ProcessBuilder builder, String input, int timeOut) throws IOException {
154                return execute(builder, input, timeOut, true);
155        }
156
157        /**
158         * Executes a process in a thread-safe way.
159         *
160         * @param builder
161         *            builder that gets executed
162         * @param input
163         *            String that gets written to stdin (may be null).
164         * @param timeOut
165         *            the number to seconds to wait for the process. If this runs
166         *            longer, the process is killed. Passing a value of 0 or less makes
167         *            the method wait forever (until the process finishes normally). To
168         *            find out whether the process was killed, query
169         *            {@link ExecutionResult#isNormalTermination()}.
170         * @param collectOutputStreamContent
171         *            boolean that indicates if the content from the stderr and stdout
172         *            shall be collected. False is useful if the content is not needed
173         *            and / or if the content would cause an OutOfMemoryException.
174         *
175         * @return result of the execution
176         */
177        public static ExecutionResult execute(ProcessBuilder builder, String input, int timeOut,
178                        boolean collectOutputStreamContent) throws IOException {
179                // start process
180                Process process = builder.start();
181
182                // read error for later use
183                StreamReaderThread stderrReader = new StreamReaderThread(process.getErrorStream(), CONSOLE_CHARSET,
184                                collectOutputStreamContent);
185                StreamReaderThread stdoutReader = new StreamReaderThread(process.getInputStream(), CONSOLE_CHARSET,
186                                collectOutputStreamContent);
187
188                // write input to process
189                if (input != null) {
190                        Writer stdIn = new OutputStreamWriter(process.getOutputStream());
191                        stdIn.write(input);
192                        stdIn.close();
193                }
194
195                // wait for process
196                boolean normalTermination = waitForProcess(process, timeOut);
197                int exitValue = -1;
198                if (normalTermination) {
199                        exitValue = process.exitValue();
200                }
201
202                try {
203                        // It is important to wait for the threads, so the output is
204                        // completely stored.
205                        stderrReader.join();
206                        stdoutReader.join();
207                } catch (InterruptedException e) {
208                        // ignore this one
209                }
210
211                return new ExecutionResult(stdoutReader.getContent(), stderrReader.getContent(), exitValue, normalTermination);
212        }
213
214        /**
215         * Waits for the process to end or terminates it if it hits the timeout. The
216         * return value indicated whether the process terminated (true) or was killed by
217         * the timeout (false).
218         *
219         * @param maxRuntimeSeconds
220         *            is this is non-positive, this method waits until the process
221         *            terminates (without timeout).
222         */
223        private static boolean waitForProcess(Process process, int maxRuntimeSeconds) {
224                // Stopping a running process after a given time is not well supported
225                // by the Java API. See the following links for the gory details:
226                // * http://kylecartmell.com/?p=9
227                // * http://www.kylecartmell.com/public_files/ProcessTimeoutExample.java
228                Timer timer = new Timer(true);
229
230                if (maxRuntimeSeconds > 0) {
231                        timer.schedule(new InterruptTimerTask(Thread.currentThread()), maxRuntimeSeconds * 1000);
232                }
233                try {
234                        process.waitFor();
235                        return true;
236                } catch (InterruptedException e) {
237                        process.destroy();
238                        return false;
239                } finally {
240                        // stop timer if still running (relevant if process terminates "in
241                        // time")
242                        timer.cancel();
243                        // clear the interrupt flag (see the links above for details)
244                        Thread.interrupted();
245                }
246        }
247
248        /**
249         * Parameter object that encapsulates the result of a process execution. This
250         * object is immutable.
251         */
252        public static class ExecutionResult {
253
254                /** Output on stdout of the process */
255                private final String stdout;
256
257                /** Output on stderr of the process */
258                private final String stderr;
259
260                /** Return code of the process */
261                private final int returnCode;
262
263                /** Whether termination was normal (not timeout). */
264                private final boolean normalTermination;
265
266                /** Constructor */
267                private ExecutionResult(String stdout, String stderr, int returnCode, boolean normalTermination) {
268                        this.stdout = stdout;
269                        this.stderr = stderr;
270                        this.returnCode = returnCode;
271                        this.normalTermination = normalTermination;
272                }
273
274                /** Returns stdout. */
275                public String getStdout() {
276                        return stdout;
277                }
278
279                /** Returns stderr. */
280                public String getStderr() {
281                        return stderr;
282                }
283
284                /** Returns returnCode. */
285                public int getReturnCode() {
286                        return returnCode;
287                }
288
289                /** Returns whether this was a normal termination (not a timeout). */
290                public boolean isNormalTermination() {
291                        return normalTermination;
292                }
293        }
294
295        /**
296         * Runs the given process builder with the given input. If creating the process
297         * fails, the process times out or the process exits with a non-zero exit code,
298         * an exception is thrown
299         *
300         * @param builder
301         *            The process builder to execute.
302         * @param input
303         *            The input to the program.
304         * @param timeout
305         *            How many seconds to wait before terminating the process. If this
306         *            is less than 1, no timeout is applied.
307         * @param exceptionConstructor
308         *            Constructs the exception that is thrown in case something goes
309         *            wrong. The first argument is a descriptive message, the second is
310         *            either <code>null</code> (in case of a timeout or non-zero exit
311         *            code) or the exception that caused the failure. This parameter is
312         *            compatible with most exception constructors, e.g.
313         *            <code>ConQATException::new</code>.
314         * @return the execution result of the successfully run process.
315         * @throws T
316         *             An exception constructed with the given constructor. Thrown if
317         *             the process fails to run and return a zero exit code.
318         */
319        public static <T extends Exception> ExecutionResult executeOrThrow(ProcessBuilder builder, String input,
320                        int timeout, BiFunction<String, Throwable, T> exceptionConstructor) throws T {
321                String commandString = StringUtils.concat(builder.command(), StringUtils.SPACE);
322                try {
323                        ExecutionResult result = execute(builder, input, timeout);
324                        if (!result.isNormalTermination()) {
325                                throw exceptionConstructor.apply("Process " + commandString + " timed out.", null);
326                        }
327                        if (result.getReturnCode() != 0) {
328                                throw exceptionConstructor.apply("Process " + commandString + " failed with non-zero exit code "
329                                                + result.getReturnCode() + ". Standard output: '" + result.getStdout() + "', Error output: '"
330                                                + result.getStderr() + "'", null);
331                        }
332                        return result;
333                } catch (IOException e) {
334                        throw exceptionConstructor.apply("Failed to execute " + commandString, e);
335                }
336        }
337}