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

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

Trying to correct the GS2PerlConstructor.runPerlCommand() function which has always been brittle, and causing a deadlocked Process when things go wrong. The current solution is with the new class SafeProcess, which tries to properly create, run and end a process and manage its streams, and close them off. Have tried to ensure there will be no concurrency issues. Maybe it would be better to use Executor to instantiate threads (and to use its cancel abilities if we want this in future), but have stuck to existing code that worked. Changed GS2PerlConstructor to use this new SafeProcess class in its new runPerlCommand() method. Tested document editing and adding user comments on Linux to find these things still work. However, the deadlock was happening on windows, so that's where the solution should be tested. Leaving in debug statements for windows testing.

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