source: main/trunk/greenstone3/src/java/org/greenstone/util/SafeProcess.java@ 31594

Last change on this file since 31594 was 31594, checked in by ak19, 7 years ago

More equalising of debugging between the gli and gs3 src code versions of SafeProcess.java

File size: 17.0 KB
Line 
1package org.greenstone.util;
2
3import java.io.BufferedReader;
4import java.io.BufferedWriter;
5import java.io.Closeable;
6import java.io.File;
7import java.io.InputStream;
8import java.io.InputStreamReader;
9import java.io.IOException;
10import java.io.OutputStream;
11import java.io.OutputStreamWriter;
12
13import java.util.Arrays;
14
15import org.apache.log4j.*;
16
17// Use this class to run a Java Process. It follows the good and safe practices at
18// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
19// to avoid blocking problems that can arise from a Process' input and output streams.
20
21public class SafeProcess {
22
23 static Logger logger = Logger.getLogger(org.greenstone.util.SafeProcess.class.getName());
24
25 // input to SafeProcess and initialising it
26 private String command = null;
27 private String[] command_args = null;
28 private String[] envp = null;
29 private File dir = null;
30 private String inputStr = null;
31
32 // output from running SafeProcess.runProcess()
33 private String outputStr = "";
34 private String errorStr = "";
35 private int exitValue = -1;
36
37 // allow callers to process exceptions of the main process thread if they want
38 private ExceptionHandler exceptionHandler = null;
39
40 // whether std/err output should be split at new lines
41 private boolean splitStdOutputNewLines = false;
42 private boolean splitStdErrorNewLines = false;
43
44 // call one of these constructors
45
46 // cmd args version
47 public SafeProcess(String[] cmd_args)
48 {
49 command_args = cmd_args;
50 }
51
52 // cmd string version
53 public SafeProcess(String cmdStr)
54 {
55 command = cmdStr;
56 }
57
58 // cmd args with env version, launchDir can be null.
59 public SafeProcess(String[] cmd_args, String[] envparams, File launchDir)
60 {
61 command_args = cmd_args;
62 envp = envparams;
63 dir = launchDir;
64 }
65
66 // The important methods:
67 // to get the output from std err and std out streams after the process has been run
68 public String getStdOutput() { return outputStr; }
69 public String getStdError() { return errorStr; }
70 public int getExitValue() { return exitValue; }
71
72
73 // set any string to send as input to the process spawned by SafeProcess
74 public void setInputString(String sendStr) {
75 inputStr = sendStr;
76 }
77
78 // register a SafeProcess ExceptionHandler whose gotException() method will
79 // get called for each exception encountered
80 public void setExceptionHandler(ExceptionHandler exception_handler) {
81 exceptionHandler = exception_handler;
82 }
83
84 // set if you want the std output or err output to have \n at each newline read from the stream
85 public void setSplitStdOutputNewLines(boolean split) {
86 splitStdOutputNewLines = split;
87 }
88 public void setSplitStdErrorNewLines(boolean split) {
89 splitStdErrorNewLines = split;
90 }
91
92//***************** Copied from gli's gui/FormatConversionDialog.java *************//
93
94 public int runProcess() {
95 return runProcess(null, null, null); // use default processing of all 3 of the process' iostreams
96 }
97
98 public int runProcess(CustomProcessHandler procInHandler,
99 CustomProcessHandler procOutHandler,
100 CustomProcessHandler procErrHandler)
101 {
102 Process prcs = null;
103 SafeProcess.OutputStreamGobbler inputGobbler = null;
104 SafeProcess.InputStreamGobbler errorGobbler = null;
105 SafeProcess.InputStreamGobbler outputGobbler = null;
106
107 try {
108
109 Runtime rt = Runtime.getRuntime();
110
111
112 if(this.command != null) {
113 prcs = rt.exec(this.command);
114 }
115 else { // at least command_args must be set now
116
117 // http://stackoverflow.com/questions/5283444/convert-array-of-strings-into-a-string-in-java
118 ///System.err.println("SafeProcess running: " + Arrays.toString(command_args));
119 logger.info("SafeProcess running: " + Arrays.toString(command_args));
120
121 if(this.envp == null) {
122 prcs = rt.exec(this.command_args);
123 } else { // launch process using cmd str with env params
124
125 if(this.dir == null) {
126 ///logger.info("\twith: " + Arrays.toString(this.envp));
127 ///System.err.println("\twith: " + Arrays.toString(this.envp));
128 prcs = rt.exec(this.command_args, this.envp);
129 } else {
130 ///logger.info("\tfrom directory: " + this.dir);
131 ///logger.info("\twith: " + Arrays.toString(this.envp));
132 ///System.err.println("\tfrom directory: " + this.dir);
133 ///System.err.println("\twith: " + Arrays.toString(this.envp));
134 prcs = rt.exec(this.command_args, this.envp, this.dir);
135 }
136 }
137 }
138
139 // Create the streamgobblers and set any specified handlers on them
140
141 // PROC INPUT STREAM
142 if(procInHandler == null) {
143 // send inputStr to process. The following constructor can handle inputStr being null
144 inputGobbler = // WriterToProcessInputStream
145 new SafeProcess.OutputStreamGobbler(prcs.getOutputStream(), this.inputStr);
146 } else { // user will do custom handling of process' InputStream
147 inputGobbler = new SafeProcess.OutputStreamGobbler(prcs.getOutputStream(), procInHandler);
148 }
149
150 // PROC ERR STREAM to monitor for any error messages or expected output in the process' stderr
151 if(procErrHandler == null) {
152 errorGobbler // ReaderFromProcessOutputStream
153 = new SafeProcess.InputStreamGobbler(prcs.getErrorStream(), splitStdErrorNewLines);
154 } else {
155 errorGobbler
156 = new SafeProcess.InputStreamGobbler(prcs.getErrorStream(), procErrHandler);
157 }
158
159 // PROC OUT STREAM to monitor for the expected std output line(s)
160 if(procOutHandler == null) {
161 outputGobbler
162 = new SafeProcess.InputStreamGobbler(prcs.getInputStream(), splitStdOutputNewLines);
163 } else {
164 outputGobbler
165 = new SafeProcess.InputStreamGobbler(prcs.getInputStream(), procOutHandler);
166 }
167
168
169 // kick off the stream gobblers
170 inputGobbler.start();
171 errorGobbler.start();
172 outputGobbler.start();
173
174 // any error???
175 this.exitValue = prcs.waitFor(); // can throw an InterruptedException if process did not terminate
176
177 ///logger.info("Process exitValue: " + exitValue);
178
179 // From the comments of
180 // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
181 // To avoid running into nondeterministic failures to get the process output
182 // if there's no waiting for the threads, call join() on each Thread (StreamGobbler) object.
183 // From Thread API: join() "Waits for this thread (the thread join() is invoked on) to die."
184 outputGobbler.join();
185 errorGobbler.join();
186 inputGobbler.join();
187
188
189 // set the variables the code that created a SafeProcess object may want to inspect
190 this.outputStr = outputGobbler.getOutput();
191 this.errorStr = errorGobbler.getOutput();
192
193 // Since we didn't have an exception, process should have terminated now (waitFor blocks until then)
194 // Set process to null so we don't forcibly terminate it below with process.destroy()
195 prcs = null;
196
197 } catch(IOException ioe) {
198 if(exceptionHandler != null) {
199 exceptionHandler.gotException(ioe);
200 } else {
201 logger.error("IOexception: " + ioe.getMessage(), ioe);
202 //System.err.println("IOexception " + ioe.getMessage());
203 //ioe.printStackTrace();
204 }
205 } catch(InterruptedException ie) {
206
207 if(exceptionHandler != null) {
208 exceptionHandler.gotException(ie);
209 } else {
210 logger.error("Process InterruptedException: " + ie.getMessage(), ie);
211 //System.err.println("Process InterruptedException " + ie.getMessage());
212 ///ie.printStackTrace(); // an interrupt here is not an error, it can be a cancel action
213 }
214
215 // propagate interrupts to worker threads here?
216 // unless the interrupt emanated from any of them in any join()...
217 // Only if the thread SafeProcess runs in was interrupted
218 // do we propagate the interrupt to the worker threads.
219 // http://stackoverflow.com/questions/2126997/who-is-calling-the-java-thread-interrupt-method-if-im-not
220 // "I know that in JCiP it is mentioned that you should never interrupt threads you do not own"
221 // But SafeProcess owns the worker threads, so it have every right to interrupt them
222 // Also read http://stackoverflow.com/questions/13623445/future-cancel-method-is-not-working?noredirect=1&lq=1
223 if(Thread.currentThread().isInterrupted()) {
224 inputGobbler.interrupt();
225 errorGobbler.interrupt();
226 outputGobbler.interrupt();
227 }
228
229 // On catchingInterruptedException, re-interrupt the thread.
230 // This is just how InterruptedExceptions tend to be handled
231 // See also http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception
232 // and https://praveer09.github.io/technology/2015/12/06/understanding-thread-interruption-in-java/
233
234 // http://stackoverflow.com/questions/3976344/handling-interruptedexception-in-java
235 // http://stackoverflow.com/questions/4906799/why-invoke-thread-currentthread-interrupt-when-catch-any-interruptexception
236 // "Only code that implements a thread's interruption policy may swallow an interruption request. General-purpose task and library code should never swallow interruption requests."
237 // Does that mean that since this code implements this thread's interruption policy, it's ok
238 // to swallow the interrupt this time and not let it propagate by commenting out the next line?
239 Thread.currentThread().interrupt(); // re-interrupt the thread - which thread? Infinite loop?
240 } finally {
241
242 // Moved into here from GS2PerlConstructor which said
243 // "I need to somehow kill the child process. Unfortunately Thread.stop() and Process.destroy() both fail to do this. But now, thankx to the magic of Michaels 'close the stream suggestion', it works fine."
244 // http://steveliles.github.io/invoking_processes_from_java.html
245 // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
246 // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec
247 if( prcs != null ) {
248 prcs.destroy();
249 }
250 }
251
252 return this.exitValue;
253 }
254
255
256//**************** Inner class definitions (stream gobblers copied from GLI) **********//
257// Static inner classes can be instantiated without having to instantiate an object of the outer class first
258
259// Can have public static interfaces too,
260// see http://stackoverflow.com/questions/71625/why-would-a-static-nested-interface-be-used-in-java
261// Implementors need to take care that the implementations are thread safe
262// http://stackoverflow.com/questions/14520814/why-synchronized-method-is-not-included-in-interface
263public static interface ExceptionHandler {
264
265 // SHOULD I DECLARE THIS SYNCHRONIZED?
266 // It ends up being thread safe for the particular instance I'm using it for, but that doesn't
267 // make it future proof...
268 public void gotException(Exception e);
269}
270
271// Write your own run() body for any StreamGobbler. You need to create an instance of a class
272// implementing CustomProcessHandler for EACH IOSTREAM of the process that you want to handle.
273// Do not create a single CustomProcessHandler instance and reuse it for all three streams,
274// i.e. don't call SafeProcess' runProcess(x, x, x); It should be runProcess(x, y, z).
275// Make sure your implementation is threadsafe if you're sharing immutable objects between the threaded streams
276// example implementation is in the GS2PerlConstructor.SynchronizedProcessHandler class.
277public static interface CustomProcessHandler {
278 public void run(Closeable stream); //InputStream or OutputStream
279}
280
281
282// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
283// This class is used in FormatConversionDialog to properly read from the stdout and stderr
284// streams of a Process, Process.getInputStream() and Process.getErrorSream()
285public static class InputStreamGobbler extends Thread
286{
287 private InputStream is = null;
288 private StringBuffer outputstr = new StringBuffer();
289 private boolean split_newlines = false;
290 private CustomProcessHandler customHandler = null;
291
292 public InputStreamGobbler(InputStream is)
293 {
294 this.is = is;
295 this.split_newlines = false;
296 }
297
298 public InputStreamGobbler(InputStream is, boolean split_newlines)
299 {
300 this.is = is;
301 this.split_newlines = split_newlines;
302 }
303
304 public InputStreamGobbler(InputStream is, CustomProcessHandler customHandler)
305 {
306 this.is = is;
307 this.customHandler = customHandler;
308 }
309
310 // default run() behaviour
311 public void runDefault()
312 {
313 BufferedReader br = null;
314 try {
315 br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
316 String line=null;
317 while ( (line = br.readLine()) != null ) {
318
319 if(this.isInterrupted()) { // should we not instead check if SafeProcess thread was interrupted?
320 logger.info("Got interrupted when reading lines from process err/out stream.");
321 break; // will go to finally block
322 }
323
324 //System.out.println("@@@ GOT LINE: " + line);
325 outputstr.append(line);
326 if(split_newlines) {
327 outputstr.append(Misc.NEWLINE); // "\n" is system dependent (Win must be "\r\n")
328 }
329 }
330 } catch (IOException ioe) {
331 logger.error("Exception when reading from a process' stdout/stderr stream: ", ioe);
332 //System.err.println("Exception when reading from a process' stdout/stderr stream: ");
333 //ioe.printStackTrace();
334
335 } finally {
336 SafeProcess.closeResource(br);
337 }
338 }
339
340 public void runCustom() {
341 this.customHandler.run(is);
342 }
343
344 public void run() {
345 if(this.customHandler == null) {
346 runDefault();
347 } else {
348 runCustom();
349 }
350 }
351
352 public String getOutput() {
353 return outputstr.toString(); // implicit toString() call anyway. //return outputstr;
354 }
355} // end static inner class InnerStreamGobbler
356
357
358// http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
359// This class is used in FormatConversionDialog to properly write to the inputstream of a Process
360// Process.getOutputStream()
361public static class OutputStreamGobbler extends Thread
362{
363 private OutputStream os = null;
364 private String inputstr = "";
365 private CustomProcessHandler customHandler = null;
366
367 public OutputStreamGobbler(OutputStream os) {
368 this.os = os;
369 }
370
371 public OutputStreamGobbler(OutputStream os, String inputstr)
372 {
373 this.os = os;
374 this.inputstr = inputstr;
375 }
376
377 public OutputStreamGobbler(OutputStream os, CustomProcessHandler customHandler) {
378 this.os = os;
379 this.customHandler = customHandler;
380 }
381
382 // default run() behaviour
383 public void runDefault() {
384
385 if (inputstr == null) {
386 return;
387 }
388
389 BufferedWriter osw = null;
390 try {
391 osw = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
392 //System.out.println("@@@ SENDING LINE: " + inputstr);
393 osw.write(inputstr, 0, inputstr.length());
394 osw.newLine();//osw.write("\n");
395 osw.flush();
396
397 // Don't explicitly send EOF when using StreamGobblers as below,
398 // as the EOF char is echoed to output.
399 // Flushing the write handle and/or closing the resource seems
400 // to already send EOF silently.
401
402 /*if(Utility.isWindows()) {
403 osw.write("\032"); // octal for Ctrl-Z, EOF on Windows
404 } else { // EOF on Linux/Mac is Ctrl-D
405 osw.write("\004"); // octal for Ctrl-D, see http://www.unix-manuals.com/refs/misc/ascii-table.html
406 }
407 osw.flush();
408 */
409 } catch (IOException ioe) {
410 logger.error("Exception writing to SafeProcess' inputstream: ", ioe);
411 //System.err.println("Exception writing to SafeProcess' inputstream: ");
412 //ioe.printStackTrace();
413 } finally {
414 SafeProcess.closeResource(osw);
415 }
416 }
417
418 // call the user's custom handler for the run() method
419 public void runCustom() {
420 this.customHandler.run(os);
421 }
422
423 public void run()
424 {
425 if(this.customHandler == null) {
426 runDefault();
427 } else {
428 runCustom();
429 }
430
431 }
432} // end static inner class OutputStreamGobbler
433
434
435//**************** Copied from GLI's Utility.java ******************
436 // For safely closing streams/handles/resources.
437 // For examples of use look in the Input- and OutputStreamGobbler classes.
438 // http://docs.oracle.com/javase/tutorial/essential/exceptions/finally.html
439 // http://stackoverflow.com/questions/481446/throws-exception-in-finally-blocks
440 public static boolean closeResource(Closeable resourceHandle) {
441 boolean success = false;
442 try {
443 if(resourceHandle != null) {
444 resourceHandle.close();
445 resourceHandle = null;
446 success = true;
447 }
448 } catch(Exception e) {
449 logger.error("Exception closing resource: ", e);
450 //System.err.println("Exception closing resource: " + e.getMessage());
451 //e.printStackTrace();
452 resourceHandle = null;
453 success = false;
454 } finally {
455 return success;
456 }
457 }
458
459 public static boolean closeProcess(Process prcs) {
460 boolean success = true;
461 if( prcs != null ) {
462 success = success && closeResource(prcs.getErrorStream());
463 success = success && closeResource(prcs.getOutputStream());
464 success = success && closeResource(prcs.getInputStream());
465 prcs.destroy();
466 }
467 return success;
468 }
469
470} // end class SafeProcess
Note: See TracBrowser for help on using the repository browser.