Ignore:
Timestamp:
2017-05-22T16:43:13+12:00 (7 years ago)
Author:
ak19
Message:

Bringing GS3 src code's SafeProcess up to speed with GLI version, with some addiitonal minor changes to be included in the GLI version hereafter. Tested on Linux.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/greenstone3/src/java/org/greenstone/util/SafeProcess.java

    r31665 r31695  
    1010import java.io.OutputStream;
    1111import java.io.OutputStreamWriter;
     12import java.net.Socket;
    1213import java.util.Arrays;
    1314import java.util.Scanner;
    1415import java.util.Stack;
     16import javax.swing.SwingUtilities;
     17
    1518
    1619import com.sun.jna.*;
     
    1922
    2023import java.lang.reflect.Field;
    21 //import java.lang.reflect.Method;
    2224
    2325import org.apache.log4j.*;
     
    3941    public static final int STDIN = 2;
    4042    // can't make this variable final and init in a static block, because it needs to use other SafeProcess static methods which rely on this in turn:
    41     public static String WIN_KILL_CMD;
    42        
     43    public static String WIN_KILL_CMD;
     44
     45    /**
     46     * Boolean interruptible is used to mark any sections of blocking code that should not be interrupted
     47     * with an InterruptedExceptions. At present only the cancelRunningProcess() attempts to do such a thing
     48     * and avoids doing so when interruptible is false.
     49     * Note that interruptible is also used as a lock, so remember to synchronize on it when using it!
     50    */
     51    public Boolean interruptible = Boolean.TRUE;
    4352   
    4453    // charset for reading process stderr and stdout streams
     
    5564    private Process process = null;
    5665    private boolean forciblyTerminateProcess = false;
     66
     67    /** a ref to the thread in which the Process is being executed (the thread wherein Runtime.exec() is called) */
     68    private Thread theProcessThread = null;
    5769   
    5870    // output from running SafeProcess.runProcess()
     
    6476    // allow callers to process exceptions of the main process thread if they want
    6577    private ExceptionHandler exceptionHandler = null;
     78    /** allow callers to implement hooks that get called during the main phases of the internal
     79     * process' life cycle, such as before and after process.destroy() gets called
     80    */
     81    private MainProcessHandler mainHandler = null;
    6682
    6783    // whether std/err output should be split at new lines
     
    110126    }
    111127
     128    /** to set a handler that will handle the main (SafeProcess) thread,
     129     * implementing the hooks that will get called during the internal process' life cycle,
     130     * such as before and after process.destroy() is called */
     131    public void setMainHandler(MainProcessHandler handler) {
     132    this.mainHandler = handler;
     133    }
     134
    112135    // set if you want the std output or err output to have \n at each newline read from the stream
    113136    public void setSplitStdOutputNewLines(boolean split) {
     
    118141    }
    119142
     143   
     144    /*
     145    public boolean canInterrupt() {
     146    boolean canInterrupt;
     147    synchronized(interruptible) {
     148        canInterrupt = interruptible.booleanValue();
     149    }
     150    return canInterrupt;
     151    }
     152    */
     153
     154    /**
     155     * Call this method when you want to prematurely and safely terminate any process
     156     * that SafeProcess may be running.
     157     * You may want to implement the SafeProcess.MainHandler interface to write code
     158     * for any hooks that will get called during the process' life cycle.
     159     * @return false if process has already terminated or if it was already terminating
     160     * when cancel was called. In such cases no interrupt is sent. Returns boolean sentInterrupt.
     161     */
     162    public synchronized boolean cancelRunningProcess() {
     163    // on interrupt:
     164    // - forciblyTerminate will be changed to true if the interrupt came in when the process was
     165    // still running (and so before the process' streams were being joined)
     166    // - and forciblyTerminate will still remain false if the interrupt happens when the process'
     167    // streams are being/about to be joined (hence after the process naturally terminated).
     168    // So we don't touch the value of this.forciblyTerminate here.
     169    // The value of forciblyTerminate determines whether Process.destroy() and its associated before
     170    // and after handlers are called or not: we don't bother destroying the process if it came to
     171    // a natural end.
     172
     173    // no process to interrupt, so we're done
     174    if(this.process == null) {
     175        log("@@@ No Java process to interrupt.");
     176        return false;
     177    }
     178   
     179    boolean sentInterrupt = false;
     180
     181    // can't interrupt when SafeProcess is joining (cleanly terminating) worker threads
     182    // have to wait until afterward     
     183    if (interruptible) {
     184        // either way, we can now interrupt the thread - if we have one (we should)
     185        if(this.theProcessThread != null) { // we're told which thread should be interrupted
     186        this.theProcessThread.interrupt();
     187        log("@@@ Successfully sent interrupt to process.");
     188        sentInterrupt = true;
     189        }
     190    }
     191    else { // wait for join()s to finish.
     192        // During and after joining(), there's no need to interrupt any more anyway: no calls
     193        // subsequent to joins() block, so everything thereafter is insensitive to InterruptedExceptions.
     194
     195        if(SwingUtilities.isEventDispatchThread()) {
     196        log("#### Event Dispatch thread, returning");
     197        return false;
     198        }
     199
     200        while(!interruptible) {
     201
     202        log("######### Waiting for process to become interruptible...");
     203       
     204        // https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
     205        // wait will release lock on this object, and regain it when loop condition interruptible is true
     206        try {
     207            this.wait(); // can't interrupt when SafeProcess is joining (cleanly terminating) worker threads, so wait
     208        } catch(Exception e) {
     209            log("@@@ Interrupted exception while waiting for SafeProcess' worker threads to finish joining on cancelling process");
     210        }
     211        }
     212
     213        // now the process is sure to have ended as the worker threads would have been joined
     214    }
     215
     216    return sentInterrupt;
     217    }
     218
     219   
    120220    // In future, think of changing the method doRuntimeExec() over to using ProcessBuilder
    121221    // instead of Runtime.exec(). ProcessBuilder seems to have been introduced from Java 5.
    122222    // https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html
    123     // https://zeroturnaround.com/rebellabs/how-to-deal-with-subprocesses-in-java/
     223    // See also https://zeroturnaround.com/rebellabs/how-to-deal-with-subprocesses-in-java/
    124224    // which suggests using Apache Common Exec to launch processes and says what will be forthcoming in Java 9
    125225   
     
    159259    }
    160260
     261    this.theProcessThread = Thread.currentThread(); // store a ref to the thread wherein the Process is being run
    161262    return prcs;
    162263    }
     
    173274    outputGobbler.start();
    174275   
    175     // any error???
    176276    try {
    177         this.exitValue = process.waitFor(); // can throw an InterruptedException if process did not terminate
     277        this.exitValue = process.waitFor(); // can throw an InterruptedException if process was cancelled/prematurely terminated
    178278    } catch(InterruptedException ie) {
    179279        log("*** Process interrupted (InterruptedException). Expected to be a Cancel operation.");
     
    185285        // propagate interrupts to worker threads here
    186286        // unless the interrupt emanated from any of them in any join(),
    187         // which will be caught by caller's catch on InterruptedException.
     287        // which will be caught by the calling method's own catch on InterruptedException.
    188288        // Only if the thread that SafeProcess runs in was interrupted
    189289        // should we propagate the interrupt to the worker threads.
     
    208308        // to die off. Don't set process=null until after we've forcibly terminated it if needs be.
    209309        this.forciblyTerminateProcess = true;
    210        
     310       
    211311        // even after the interrupts, we want to proceed to calling join() on all the worker threads
    212312        // in order to wait for each of them to die before attempting to destroy the process if it
     
    215315       
    216316        //log("Process exitValue: " + exitValue);
     317        ///log("@@@@ Before join phase. Forcibly terminating: " + this.forciblyTerminateProcess);
    217318       
    218319        // From the comments of
     
    225326        // Any of these can throw InterruptedExceptions too
    226327        // and will be processed by the calling function's catch on InterruptedException.
    227         // However, no one besides us will interrupting these threads I think...
    228         // and we won't be throwing the InterruptedException from within the threads...
    229         // So if any streamgobbler.join() call throws an InterruptedException, that would be unexpected
    230 
    231         outputGobbler.join();
     328
     329
     330        // Thread.joins() below are blocking calls, same as Process.waitFor(), and a cancel action could
     331        // send an interrupt during any Join: the InterruptedException ensuing will then break out of the
     332        // joins() section. We don't want that to happen: by the time the joins() start happening, the
     333        // actual process has finished in some way (naturally terminated or interrupted), and nothing
     334        // should interrupt the joins() (nor ideally any potential p.destroy after that).
     335        // So we mark the join() section as an un-interruptible section, and make anyone who's seeking
     336        // to interrupt just then first wait for this Thread (in which SafeProcess runs) to become
     337        // interruptible again. Thos actually assumes anything interruptible can still happen thereafter
     338        // when in reality, none of the subsequent actions after the joins() block. So they nothing
     339        // thereafter, which is the cleanup phase, will actually respond to an InterruptedException.
     340
     341       
     342        if(this.mainHandler != null) {
     343        // this method can unset forcible termination flag
     344        // if the process had already naturally terminated by this stage:
     345        this.forciblyTerminateProcess = mainHandler.beforeWaitingForStreamsToEnd(this.forciblyTerminateProcess);
     346        }
     347
     348        ///log("@@@@ After beforeJoin Handler. Forcibly terminating: " + this.forciblyTerminateProcess);
     349
     350        // Anyone could interrupt/cancel during waitFor() above,
     351        // but no one should interrupt while the worker threads come to a clean close,
     352        // so make anyone wanting to cancel the process at this stage wait()
     353        // until we're done with the join()s:
     354        synchronized(interruptible) {
     355        interruptible = Boolean.FALSE;
     356        }
     357        //Thread.sleep(5000); // Uncomment to test this uninterruptible section, also comment out block checking for
     358            // EventDispatchThread in cancelRunningProcess() and 2 calls to progress.enableCancelJob() in DownloadJob.java
     359        outputGobbler.join();
    232360        errorGobbler.join();
    233         inputGobbler.join();
    234        
    235        
     361        inputGobbler.join();
     362
     363        synchronized(interruptible) {
     364        interruptible = Boolean.TRUE;
     365        }
     366
     367        ///log("@@@@ Join phase done...");
     368
     369        // notify any of those waiting to interrupt this thread, that they may feel free to do so again
     370        // https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
     371        synchronized(this) {
     372        this.notify();
     373        }
     374
    236375        // set the variables that the code which created a SafeProcess object may want to inspect
    237376        this.outputStr = outputGobbler.getOutput();
    238         this.errorStr = errorGobbler.getOutput();       
     377        this.errorStr = errorGobbler.getOutput();
     378       
     379        // call the after join()s hook
     380        if(this.mainHandler != null) {
     381        this.forciblyTerminateProcess = mainHandler.afterStreamsEnded(this.forciblyTerminateProcess);
     382        }
    239383    }
    240384
     
    260404    try {
    261405        this.forciblyTerminateProcess = true;
    262        
     406       
    263407        // 1. create the process
    264408        process = doRuntimeExec();
     
    283427       
    284428        Thread.currentThread().interrupt();
    285     } finally {
    286 
    287         if( this.forciblyTerminateProcess ) {
    288         destroyProcess(process); // see runProcess() below     
    289         }
    290         process = null;
    291         this.forciblyTerminateProcess = false; // reset
     429    } finally {
     430       
     431        cleanUp("SafeProcess.runBasicProcess");
    292432    }
    293433    return this.exitValue;
     
    347487
    348488
    349             // 3. kick off the stream gobblers
     489        // 3. kick off the stream gobblers
    350490        this.exitValue = waitForWithStreams(inputGobbler, outputGobbler, errorGobbler);
    351491       
     
    365505        log("@@@@ Unexpected InterruptedException when waiting for process stream gobblers to die");
    366506        } else {
    367         log("*** Unexpected InterruptException when waiting for process stream gobblers to die:" + ie.getMessage(), ie);
     507        log("*** Unexpected InterruptException when waiting for process stream gobblers to die: " + ie.getMessage(), ie);
    368508        }
    369509
     
    371511        Thread.currentThread().interrupt();
    372512   
    373     } finally {
    374         //String cmd = (this.command == null) ? Arrays.toString(this.command_args) : this.command;
    375         //log("*** In finally of SafeProcess.runProcess(3 params): " + cmd);
    376 
    377         if( this.forciblyTerminateProcess ) {
    378         log("*** Going to call process.destroy 2");
    379         destroyProcess(process);               
    380         log("*** Have called process.destroy 2");
    381         }
    382         process = null;
    383         this.forciblyTerminateProcess = false; // reset
     513    } finally {
     514       
     515        cleanUp("SafeProcess.runProcess(3 params)");
    384516    }
    385517   
     
    424556
    425557
    426         // 4. kick off the stream gobblers
     558        // 3. kick off the stream gobblers
    427559        this.exitValue = waitForWithStreams(inputGobbler, outputGobbler, errorGobbler);
    428560       
     
    452584        Thread.currentThread().interrupt(); // re-interrupt the thread - which thread? Infinite loop?
    453585   
    454     } finally {
    455 
    456         // Moved into here from GS2PerlConstructor and GShell.runLocal() which said
    457         // "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 (no it doesn't!)"
    458         // http://steveliles.github.io/invoking_processes_from_java.html
    459         // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
    460         // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec       
    461        
    462         //String cmd = (this.command == null) ? Arrays.toString(this.command_args) : this.command;
    463         //log("*** In finally of SafeProcess.runProcess(2 params): " + cmd);
    464 
    465         if( this.forciblyTerminateProcess ) {
    466         log("*** Going to call process.destroy 1");
    467         destroyProcess(process);   
    468         log("*** Have called process.destroy 1");
    469         }
    470         process = null;
    471         this.forciblyTerminateProcess = false; //reset     
     586    } finally {
     587       
     588        cleanUp("SafeProcess.runProcess(2 params)");   
    472589    }
    473590   
    474591    return this.exitValue;
     592    }
     593
     594    private void cleanUp(String callingMethod) {
     595   
     596    // Moved into here from GS2PerlConstructor and GShell.runLocal() which said
     597    // "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 (no it doesn't!)"
     598    // http://steveliles.github.io/invoking_processes_from_java.html
     599    // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
     600    // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec       
     601
     602    //String cmd = (this.command == null) ? Arrays.toString(this.command_args) : this.command;
     603    //log("*** In finally of " + callingMethod + ": " + cmd);
     604
     605    // if we're forcibly terminating the process, call the before- and afterDestroy hooks
     606    // besides actually destroying the process
     607    if( this.forciblyTerminateProcess ) {
     608        log("*** Going to call process.destroy from " + callingMethod);
     609
     610        if(mainHandler != null) mainHandler.beforeProcessDestroy();
     611        SafeProcess.destroyProcess(process); // see runProcess(2 args/3 args)
     612        if(mainHandler != null) mainHandler.afterProcessDestroy();
     613
     614        log("*** Have called process.destroy from " + callingMethod);
     615    }   
     616
     617    process = null;
     618    this.theProcessThread = null; // let the process thread ref go too
     619    boolean wasForciblyTerminated = this.forciblyTerminateProcess;
     620    this.forciblyTerminateProcess = false; // reset
     621   
     622    if(mainHandler != null) mainHandler.doneCleanup(wasForciblyTerminated);
    475623    }
    476624
     
    608756// e.g. full-import.pl may be terminated with p.destroy(), but it launches import.pl which is left running until it naturally terminates.
    609757static void destroyProcess(Process p) {
     758    log("### in SafeProcess.destroyProcess(Process p)");
     759
    610760    // If it isn't windows, process.destroy() terminates any child processes too
    611761    if(!Misc.isWindows()) {
     
    626776    // so we can use it to find the pids of any subprocesses it launched in order to terminate those too.
    627777   
    628     long processID = SafeProcess.getProcessID(p);       
    629     log("Attempting to terminate sub processes of Windows process with pid " + processID);
    630     terminateSubProcessesRecursively(processID, p);
     778    long processID = SafeProcess.getProcessID(p);
     779    if(processID == -1) { // the process doesn't exist or no longer exists (terminated naturally?)
     780    p.destroy(); // minimum step, do this anyway, at worst there's no process and this won't have any effect
     781    } else {
     782    log("Attempting to terminate sub processes of Windows process with pid " + processID);
     783    terminateSubProcessesRecursively(processID, p);
     784    }
    631785   
    632786}
     
    706860private static String getWinProcessKillCmd(Long processID) {
    707861    // check if we first need to init WIN_KILL_CMD. We do this only once, but can't do it in a static codeblock
    708    
     862    // because of a cyclical dependency regarding this during static initialization
     863
    709864    if(WIN_KILL_CMD == null) {     
    710865    if(SafeProcess.isAvailable("wmic")) {
     
    734889// where is not part of winbin. where is a system command on windows, but only since 2003, https://ss64.com/nt/where.html
    735890// There is no `where` on Linux/Mac, must use which for them.
    736 // On windows, "which tskill" fails but "which" succeeds on taskkill|wmic|browser names.
     891// On windows, "which tskill" fails (and "where tskill" works), but "which" succeeds on taskkill|wmic|browser names.
    737892public static boolean isAvailable(String program) {     
    738893    try {
     
    741896    SafeProcess prcs = new SafeProcess("which " + program);     
    742897    prcs.runProcess();
    743     String output = prcs.getStdOutput().trim(); 
     898    String output = prcs.getStdOutput().trim();
    744899    ///System.err.println("*** 'which " + program + "' returned: |" + output + "|");
    745900    if(output.equals("")) {
     
    749904        return false;
    750905    }
    751     //System.err.println("*** 'which " + program + "' returned: " + output);
    752906    return true;
    753907    } catch (Exception exc) {
     
    768922public static interface ExceptionHandler {
    769923
    770     // when implementing ExceptionHandler.gotException(), if it manipulates anything that's
    771     // not threadsafe, declare gotException() as a synchronized method to ensure thread safety
    772     public void gotException(Exception e); // can't declare as synchronized in interface method declaration
     924    /**
     925     * Called whenever an exception occurs during the execution of the main thread of SafeProcess
     926     * (the thread in which the Process is run).
     927     * Since this method can't be declared as synchronized in this interface method declaration,
     928     * when implementing ExceptionHandler.gotException(), if it manipulates anything that's
     929     * not threadsafe, declare gotException() as a synchronized method to ensure thread safety
     930     */
     931    public void gotException(Exception e);
     932}
     933
     934/** On interrupting (cancelling) a process,
     935 * if the class that uses SafeProcess wants to do special handling
     936 * either before and after join() is called on all the worker threads,
     937 * or, only on forcible termination, before and after process.destroy() is to be called,
     938 * then that class can implement this MainProcessHandler interface
     939 */
     940public static interface MainProcessHandler {
     941    /**
     942     * Called before the streamgobbler join()s.
     943     * If not overriding, the default implementation should be:
     944     * public boolean beforeWaitingForStreamsToEnd(boolean forciblyTerminating) { return forciblyTerminating; }
     945     * When overriding:
     946     * @param forciblyTerminating is true if currently it's been decided that the process needs to be
     947     * forcibly terminated. Return false if you don't want it to be. For a basic implementation,
     948     * return the parameter.
     949     * @return true if the process is still running and therefore still needs to be destroyed, or if
     950     * you can't determine whether it's still running or not. Process.destroy() will then be called.
     951     * @return false if the process has already naturally terminated by this stage. Process.destroy()
     952     * won't be called, and neither will the before- and after- processDestroy methods of this class.
     953    */
     954    public boolean beforeWaitingForStreamsToEnd(boolean forciblyTerminating);
     955    /**
     956     * Called after the streamgobbler join()s have finished.
     957     * If not overriding, the default implementation should be:
     958     * public boolean afterStreamsEnded(boolean forciblyTerminating) { return forciblyTerminating; }
     959     * When overriding:
     960     * @param forciblyTerminating is true if currently it's been decided that the process needs to be
     961     * forcibly terminated. Return false if you don't want it to be. For a basic implementation,
     962     * return the parameter (usual case).
     963     * @return true if the process is still running and therefore still needs to be destroyed, or if
     964     * can't determine whether it's still running or not. Process.destroy() will then be called.
     965     * @return false if the process has already naturally terminated by this stage. Process.destroy()
     966     * won't be called, and neither will the before- and after- processDestroy methods of this class.
     967    */
     968    public boolean afterStreamsEnded(boolean forciblyTerminating);
     969    /**
     970     * called after join()s and before process.destroy()/destroyProcess(Process), iff forciblyTerminating
     971     */
     972    public void beforeProcessDestroy();
     973    /**
     974     * Called after process.destroy()/destroyProcess(Process), iff forciblyTerminating
     975     */
     976    public void afterProcessDestroy();
     977
     978    /**
     979     * Always called after process ended: whether it got destroyed or not
     980     */
     981    public void doneCleanup(boolean wasForciblyTerminated);
    773982}
    774983
     
    10141223    logger.info(msg);
    10151224
    1016     System.err.println(msg);
     1225    //System.err.println(msg);
    10171226
    10181227    //DebugStream.println(msg);
     
    10231232    logger.error(msg, e);
    10241233
    1025     System.err.println(msg);
    1026     e.printStackTrace();
     1234    //System.err.println(msg);
     1235    //e.printStackTrace();
    10271236
    10281237    //DebugStream.println(msg);
     
    10341243    logger.error(e);
    10351244
    1036     e.printStackTrace();
     1245    //e.printStackTrace();
    10371246
    10381247    //DebugStream.printStackTrace(e);
     
    10841293    }
    10851294
     1295    // in Java 6, Sockets don't yet implement Closeable
     1296    public static boolean closeSocket(Socket resourceHandle) {
     1297    boolean success = false;
     1298    try {
     1299        if(resourceHandle != null) {
     1300        resourceHandle.close();
     1301        resourceHandle = null;
     1302        success = true;
     1303        }
     1304    } catch(Exception e) {
     1305        log("Exception closing resource: " + e.getMessage(), e);
     1306        resourceHandle = null;
     1307        success = false;
     1308    } finally {
     1309        return success;
     1310    }
     1311    }
     1312
    10861313    public static boolean closeProcess(Process prcs) {
    10871314    boolean success = true;
Note: See TracChangeset for help on using the changeset viewer.