Ignore:
Timestamp:
2017-05-18T20:38:56+12:00 (7 years ago)
Author:
ak19
Message:

Major changes to SafeProcess and classes of the download package, by bringing the final GLI (and final Greenstone) class DownloadJob over to use SafeProcess. Significant changes to SafeProcess: 1. Introduction of cancelRunningProcess as a new method, so that callers don't need to know how cancelling a process is implemented (as an interrupt) nor do they need to know what thread they ought to interrupt (which should be the thread that launched SafeProcess), nor do they need to know of the complexity surrounding the join() blocking call which should not be interrupted. 2. Introduction of the SafeProcess.MainHandler interface that provides methods that allow implementers to write hooks to various stages of the SafeProcess' internal process' life cycle. 3. moved process cleanUp() code into a reusable method within SafeProcess. Significant changes to DownloadJob and its associated DownloadProgressBar and DownloadScrollPane classes: 1. Unused member vars removed or commented out and those that can be declared final are now declared so, as a programming pricinple for thread safety, since DownloadJob and the other download classes will have to interact with multiple threads and could be called by different threads. 2. Replaced existing actionPerformed() and callDownload() of DownloadJob with SafeProcess and implemented the necessary Hooks in the SafeProcess.MainHandler() interface to ensure that perl is still told to terminate wget on a cancel action. 3. Replaced DownloadScrollPane's deleteDownloadJob() with a new version that now more responsively removes its DownloadProgressBar (with controls) for a DownloadJob. It's called by the SafeProcess.MainHandler interface hooks implemented by DownloadJob, so DownloadScrollPane no longer needs to wait() to be notify()ed when a process has cleaned up on premature termination by a cancel action. 4. Made the necessary methods in DownloadProgressBar synchronized for thread safety. 5. GShell now uses SafeProcess' new cancelRunningProcess() method in place of directly calling interrupt on the (GShell) thread that launched SafeProcess.

File:
1 edited

Legend:

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

    r31664 r31692  
    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.*;
     
    3942    public static final int STDIN = 2;
    4043    // 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        
     44    public static String WIN_KILL_CMD;
     45
     46    /**
     47     * Boolean interruptible is used to mark any sections of blocking code that should not be interrupted
     48     * with an InterruptedExceptions. At present only the cancelRunningProcess() attempts to do such a thing
     49     * and avoids doing so when interruptible is false.
     50     * Note that interruptible is also used as a lock, so remember to synchronize on it when using it!
     51    */
     52    public Boolean interruptible = Boolean.TRUE;
    4353   
    4454    // charset for reading process stderr and stdout streams
     
    5565    private Process process = null;
    5666    private boolean forciblyTerminateProcess = false;
     67
     68    /** a ref to the thread in which the Process is being executed (the thread wherein Runtime.exec() is called) */
     69    private Thread theProcessThread = null;
    5770   
    5871    // output from running SafeProcess.runProcess()
     
    6477    // allow callers to process exceptions of the main process thread if they want
    6578    private ExceptionHandler exceptionHandler = null;
     79    /** allow callers to implement hooks that get called during the main phases of the internal
     80     * process' life cycle, such as before and after process.destroy() gets called
     81    */
     82    private MainProcessHandler mainHandler = null;
    6683
    6784    // whether std/err output should be split at new lines
     
    110127    }
    111128
     129    /** to set a handler that will handle the main (SafeProcess) thread,
     130     * implementing the hooks that will get called during the internal process' life cycle,
     131     * such as before and after process.destroy() is called */
     132    public void setMainHandler(MainProcessHandler handler) {
     133    this.mainHandler = handler;
     134    }
     135
    112136    // set if you want the std output or err output to have \n at each newline read from the stream
    113137    public void setSplitStdOutputNewLines(boolean split) {
     
    118142    }
    119143
     144   
     145    /*
     146    public boolean canInterrupt() {
     147    boolean canInterrupt;
     148    synchronized(interruptible) {
     149        canInterrupt = interruptible.booleanValue();
     150    }
     151    return canInterrupt;
     152    }
     153    */
     154
     155    /**
     156     * Call this method when you want to prematurely and safely terminate any process
     157     * that SafeProcess may be running.
     158     * You may want to implement the SafeProcess.MainHandler interface to write code
     159     * for any hooks that will get called during the process' life cycle.
     160     * @return false if process has already terminated or if it was already terminating
     161     * when cancel was called. In such cases no interrupt is sent. Returns boolean sentInterrupt.
     162     */
     163    public synchronized boolean cancelRunningProcess() {
     164    // on interrupt:
     165    // - forciblyTerminate will be changed to true if the interrupt came in when the process was
     166    // still running (and so before the process' streams were being joined)
     167    // - and forciblyTerminate will still remain false if the interrupt happens when the process'
     168    // streams are being/about to be joined (hence after the process naturally terminated).
     169    // So we don't touch the value of this.forciblyTerminate here.
     170    // The value of forciblyTerminate determines whether Process.destroy() and its associated before
     171    // and after handlers are called or not: we don't bother destroying the process if it came to
     172    // a natural end.
     173
     174    // no process to interrupt, so we're done
     175    if(this.process == null) {
     176        log("@@@ No Java process to interrupt.");
     177        return false;
     178    }
     179   
     180    boolean sentInterrupt = false;
     181
     182    // can't interrupt when SafeProcess is joining (cleanly terminating) worker threads
     183    // have to wait until afterward     
     184    if (interruptible) {
     185        // either way, we can now interrupt the thread - if we have one (we should)
     186        if(this.theProcessThread != null) { // we're told which thread should be interrupted
     187        this.theProcessThread.interrupt();
     188        log("@@@ Successfully sent interrupt to process.");
     189        sentInterrupt = true;
     190        }
     191    }
     192    else { // wait for join()s to finish.
     193        // During and after joining(), there's no need to interrupt any more anyway: no calls
     194        // subsequent to joins() block, so everything thereafter is insensitive to InterruptedExceptions.
     195
     196        if(SwingUtilities.isEventDispatchThread()) {
     197        log("#### Event Dispatch thread, returning");
     198        return false;
     199        }
     200
     201        while(!interruptible) {
     202
     203        log("######### Waiting for process to become interruptible...");
     204       
     205        // https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
     206        // wait will release lock on this object, and regain it when loop condition interruptible is true
     207        try {
     208            this.wait(); // can't interrupt when SafeProcess is joining (cleanly terminating) worker threads, so wait
     209        } catch(Exception e) {
     210            log("@@@ Interrupted exception while waiting for SafeProcess' worker threads to finish joining on cancelling process");
     211        }
     212        }
     213
     214        // now the process is sure to have ended as the worker threads would have been joined
     215    }
     216
     217    return sentInterrupt;
     218    }
     219
     220   
    120221    // In future, think of changing the method doRuntimeExec() over to using ProcessBuilder
    121222    // instead of Runtime.exec(). ProcessBuilder seems to have been introduced from Java 5.
    122223    // https://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html
    123     // https://zeroturnaround.com/rebellabs/how-to-deal-with-subprocesses-in-java/
     224    // See also https://zeroturnaround.com/rebellabs/how-to-deal-with-subprocesses-in-java/
    124225    // which suggests using Apache Common Exec to launch processes and says what will be forthcoming in Java 9
    125226   
     
    159260    }
    160261
     262    this.theProcessThread = Thread.currentThread(); // store a ref to the thread wherein the Process is being run
    161263    return prcs;
    162264    }
     
    173275    outputGobbler.start();
    174276   
    175     // any error???
    176277    try {
    177         this.exitValue = process.waitFor(); // can throw an InterruptedException if process did not terminate
     278        this.exitValue = process.waitFor(); // can throw an InterruptedException if process was cancelled/prematurely terminated
    178279    } catch(InterruptedException ie) {
    179280        log("*** Process interrupted (InterruptedException). Expected to be a Cancel operation.");
     
    185286        // propagate interrupts to worker threads here
    186287        // unless the interrupt emanated from any of them in any join(),
    187         // which will be caught by caller's catch on InterruptedException.
     288        // which will be caught by the calling method's own catch on InterruptedException.
    188289        // Only if the thread that SafeProcess runs in was interrupted
    189290        // should we propagate the interrupt to the worker threads.
     
    215316       
    216317        //log("Process exitValue: " + exitValue);
     318        ///log("@@@@ Before join phase. Forcibly terminating: " + this.forciblyTerminateProcess);
    217319       
    218320        // From the comments of
     
    225327        // Any of these can throw InterruptedExceptions too
    226328        // 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();
     329
     330
     331        // Thread.joins() below are blocking calls, same as Process.waitFor(), and a cancel action could
     332        // send an interrupt during any Join: the InterruptedException ensuing will then break out of the
     333        // joins() section. We don't want that to happen: by the time the joins() start happening, the
     334        // actual process has finished in some way (naturally terminated or interrupted), and nothing
     335        // should interrupt the joins() (nor ideally any potential p.destroy after that).
     336        // So we mark the join() section as an un-interruptible section, and make anyone who's seeking
     337        // to interrupt just then first wait for this Thread (in which SafeProcess runs) to become
     338        // interruptible again. Thos actually assumes anything interruptible can still happen thereafter
     339        // when in reality, none of the subsequent actions after the joins() block. So they nothing
     340        // thereafter, which is the cleanup phase, will actually respond to an InterruptedException.
     341
     342       
     343        if(this.mainHandler != null) {
     344        // this method can unset forcible termination flag
     345        // if the process had already naturally terminated by this stage:
     346        this.forciblyTerminateProcess = mainHandler.beforeWaitingForStreamsToEnd(this.forciblyTerminateProcess);
     347        }
     348
     349        ///log("@@@@ After beforeJoin Handler. Forcibly terminating: " + this.forciblyTerminateProcess);
     350
     351        // Anyone could interrupt/cancel during waitFor() above,
     352        // but no one should interrupt while the worker threads come to a clean close,
     353        // so make anyone wanting to cancel the process at this stage wait()
     354        // until we're done with the join()s:
     355        synchronized(interruptible) {
     356        interruptible = Boolean.FALSE;
     357        }
     358        //Thread.sleep(5000); // Uncomment to test this uninterruptible section, also comment out block checking for
     359            // EventDispatchThread in cancelRunningProcess() and 2 calls to progress.enableCancelJob() in DownloadJob.java
     360        outputGobbler.join();
    232361        errorGobbler.join();
    233         inputGobbler.join();
    234        
    235        
     362        inputGobbler.join();
     363
     364        synchronized(interruptible) {
     365        interruptible = Boolean.TRUE;
     366        }
     367
     368        ///log("@@@@ Join phase done...");
     369
     370        // notify any of those waiting to interrupt this thread, that they may feel free to do so again
     371        // https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
     372        synchronized(this) {
     373        this.notify();
     374        }
     375
    236376        // set the variables that the code which created a SafeProcess object may want to inspect
    237377        this.outputStr = outputGobbler.getOutput();
    238         this.errorStr = errorGobbler.getOutput();       
     378        this.errorStr = errorGobbler.getOutput();
     379       
     380        // call the after join()s hook
     381        if(this.mainHandler != null) {
     382        this.forciblyTerminateProcess = mainHandler.afterStreamsEnded(this.forciblyTerminateProcess);
     383        }
    239384    }
    240385
     
    283428       
    284429        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
     430    } finally {
     431       
     432        cleanUp("SafeProcess.runBasicProcess");
    292433    }
    293434    return this.exitValue;
     
    347488
    348489
    349             // 3. kick off the stream gobblers
     490        // 3. kick off the stream gobblers
    350491        this.exitValue = waitForWithStreams(inputGobbler, outputGobbler, errorGobbler);
    351492       
     
    365506        log("@@@@ Unexpected InterruptedException when waiting for process stream gobblers to die");
    366507        } else {
    367         log("*** Unexpected InterruptException when waiting for process stream gobblers to die:" + ie.getMessage(), ie);
     508        log("*** Unexpected InterruptException when waiting for process stream gobblers to die: " + ie.getMessage(), ie);
    368509        }
    369510
     
    371512        Thread.currentThread().interrupt();
    372513   
    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
     514    } finally {
     515       
     516        cleanUp("SafeProcess.runProcess(3 params)");
    384517    }
    385518   
     
    424557
    425558
    426         // 4. kick off the stream gobblers
     559        // 3. kick off the stream gobblers
    427560        this.exitValue = waitForWithStreams(inputGobbler, outputGobbler, errorGobbler);
    428561       
     
    452585        Thread.currentThread().interrupt(); // re-interrupt the thread - which thread? Infinite loop?
    453586   
    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     
     587    } finally {
     588       
     589        cleanUp("SafeProcess.runProcess(2 params)");   
    472590    }
    473591   
    474592    return this.exitValue;
     593    }
     594
     595    private void cleanUp(String callingMethod) {
     596   
     597    // Moved into here from GS2PerlConstructor and GShell.runLocal() which said
     598    // "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!)"
     599    // http://steveliles.github.io/invoking_processes_from_java.html
     600    // http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html?page=2
     601    // http://mark.koli.ch/leaky-pipes-remember-to-close-your-streams-when-using-javas-runtimegetruntimeexec       
     602
     603    //String cmd = (this.command == null) ? Arrays.toString(this.command_args) : this.command;
     604    //log("*** In finally of " + callingMethod + ": " + cmd);
     605
     606    // if we're forcibly terminating the process, call the before- and afterDestroy hooks
     607    // besides actually destroying the process
     608    if( this.forciblyTerminateProcess ) {
     609        log("*** Going to call process.destroy from " + callingMethod);
     610
     611        if(mainHandler != null) mainHandler.beforeProcessDestroy();
     612        SafeProcess.destroyProcess(process); // see runProcess(2 args/3 args)
     613        if(mainHandler != null) mainHandler.afterProcessDestroy();
     614
     615        log("*** Have called process.destroy from " + callingMethod);
     616    }   
     617
     618    process = null;
     619    this.theProcessThread = null; // let the process thread ref go too
     620    boolean wasForciblyTerminated = this.forciblyTerminateProcess;
     621    this.forciblyTerminateProcess = false; // reset
     622   
     623    if(mainHandler != null) mainHandler.doneCleanup(wasForciblyTerminated);
    475624    }
    476625
     
    608757// e.g. full-import.pl may be terminated with p.destroy(), but it launches import.pl which is left running until it naturally terminates.
    609758static void destroyProcess(Process p) {
     759    log("### in SafeProcess.destroyProcess(Process p)");
     760
    610761    // If it isn't windows, process.destroy() terminates any child processes too
    611762    if(!Utility.isWindows()) {
     
    626777    // so we can use it to find the pids of any subprocesses it launched in order to terminate those too.
    627778   
    628     long processID = SafeProcess.getProcessID(p);       
    629     log("Attempting to terminate sub processes of Windows process with pid " + processID);
    630     terminateSubProcessesRecursively(processID, p);
     779    long processID = SafeProcess.getProcessID(p);
     780    if(processID == -1) { // the process doesn't exist or no longer exists (terminated naturally?)
     781    p.destroy(); // minimum step, do this anyway, at worst there's no process and this won't have any effect
     782    } else {
     783    log("Attempting to terminate sub processes of Windows process with pid " + processID);
     784    terminateSubProcessesRecursively(processID, p);
     785    }
    631786   
    632787}
     
    706861private static String getWinProcessKillCmd(Long processID) {
    707862    // 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    
     863    // because of a cyclical dependency regarding this during static initialization
     864
    709865    if(WIN_KILL_CMD == null) {     
    710866    if(SafeProcess.isAvailable("wmic")) {
     
    734890// where is not part of winbin. where is a system command on windows, but only since 2003, https://ss64.com/nt/where.html
    735891// 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.
     892// On windows, "which tskill" fails (and "where tskill" works), but "which" succeeds on taskkill|wmic|browser names.
    737893public static boolean isAvailable(String program) {     
    738894    try {
     
    764920public static interface ExceptionHandler {
    765921
    766     // when implementing ExceptionHandler.gotException(), if it manipulates anything that's
    767     // not threadsafe, declare gotException() as a synchronized method to ensure thread safety
    768     public void gotException(Exception e); // can't declare as synchronized in interface method declaration
     922    /**
     923     * Called whenever an exception occurs during the execution of the main thread of SafeProcess
     924     * (the thread in which the Process is run).
     925     * Since this method can't be declared as synchronized in this interface method declaration,
     926     * when implementing ExceptionHandler.gotException(), if it manipulates anything that's
     927     * not threadsafe, declare gotException() as a synchronized method to ensure thread safety
     928     */
     929    public void gotException(Exception e);
     930}
     931
     932/** On interrupting (cancelling) a process,
     933 * if the class that uses SafeProcess wants to do special handling
     934 * either before and after join() is called on all the worker threads,
     935 * or, only on forcible termination, before and after process.destroy() is to be called,
     936 * then that class can implement this MainProcessHandler interface
     937 */
     938public static interface MainProcessHandler {
     939    /**
     940     * Called before the streamgobbler join()s.
     941     * If not overriding, the default implementation should be:
     942     * public boolean beforeWaitingForStreamsToEnd(boolean forciblyTerminating) { return forciblyTerminating; }
     943     * When overriding:
     944     * @param forciblyTerminating is true if currently it's been decided that the process needs to be
     945     * forcibly terminated. Return false if you don't want it to be. For a basic implementation,
     946     * return the parameter.
     947     * @return true if the process is still running and therefore still needs to be destroyed, or if
     948     * you can't determine whether it's still running or not. Process.destroy() will then be called.
     949     * @return false if the process has already naturally terminated by this stage. Process.destroy()
     950     * won't be called, and neither will the before- and after- processDestroy methods of this class.
     951    */
     952    public boolean beforeWaitingForStreamsToEnd(boolean forciblyTerminating);
     953    /**
     954     * Called after the streamgobbler join()s have finished.
     955     * If not overriding, the default implementation should be:
     956     * public boolean afterStreamsEnded(boolean forciblyTerminating) { return forciblyTerminating; }
     957     * When overriding:
     958     * @param forciblyTerminating is true if currently it's been decided that the process needs to be
     959     * forcibly terminated. Return false if you don't want it to be. For a basic implementation,
     960     * return the parameter (usual case).
     961     * @return true if the process is still running and therefore still needs to be destroyed, or if
     962     * can't determine whether it's still running or not. Process.destroy() will then be called.
     963     * @return false if the process has already naturally terminated by this stage. Process.destroy()
     964     * won't be called, and neither will the before- and after- processDestroy methods of this class.
     965    */
     966    public boolean afterStreamsEnded(boolean forciblyTerminating);
     967    /**
     968     * called after join()s and before process.destroy()/destroyProcess(Process), iff forciblyTerminating
     969     */
     970    public void beforeProcessDestroy();
     971    /**
     972     * Called after process.destroy()/destroyProcess(Process), iff forciblyTerminating
     973     */
     974    public void afterProcessDestroy();
     975
     976    /**
     977     * Always called after process ended: whether it got destroyed or not
     978     */
     979    public void doneCleanup(boolean wasForciblyTerminated);
    769980}
    770981
     
    10801291    }
    10811292
     1293    // in Java 6, Sockets don't yet implement Closeable
     1294    public static boolean closeSocket(Socket resourceHandle) {
     1295    boolean success = false;
     1296    try {
     1297        if(resourceHandle != null) {
     1298        resourceHandle.close();
     1299        resourceHandle = null;
     1300        success = true;
     1301        }
     1302    } catch(Exception e) {
     1303        log("Exception closing resource: " + e.getMessage(), e);
     1304        resourceHandle = null;
     1305        success = false;
     1306    } finally {
     1307        return success;
     1308    }
     1309    }
     1310
    10821311    public static boolean closeProcess(Process prcs) {
    10831312    boolean success = true;
Note: See TracChangeset for help on using the changeset viewer.