Changeset 31692 for main/trunk/gli

Show
Ignore:
Timestamp:
18.05.2017 20:38:56 (3 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?.

Location:
main/trunk/gli/src/org/greenstone/gatherer
Files:
6 modified

Legend:

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

    r12651 r31692  
    5858 
    5959 
    60     static public boolean isDebuggingEnabled() 
     60    static synchronized public boolean isDebuggingEnabled() 
    6161    { 
    6262    return debugging_enabled; 
  • main/trunk/gli/src/org/greenstone/gatherer/download/DownloadJob.java

    r22103 r31692  
    4242import java.util.*; 
    4343import javax.swing.tree.*; 
     44import javax.swing.SwingUtilities; 
    4445import org.greenstone.gatherer.Configuration; 
    4546import org.greenstone.gatherer.DebugStream; 
     
    5051import org.greenstone.gatherer.file.WorkspaceTree; 
    5152import org.greenstone.gatherer.util.AppendLineOnlyFileDocument; 
     53import org.greenstone.gatherer.util.SafeProcess; 
    5254import org.greenstone.gatherer.util.Utility; 
    5355import org.greenstone.gatherer.cdm.Argument; 
    5456import org.greenstone.gatherer.collection.*; 
     57 
    5558/** 
    5659 * @author John Thompson, Greenstone Digital Library, University of Waikato 
    5760 * @version 2.0 
     61 * When modifying this class, bear in mind concurrency issues that could arise with  
     62 * SafeProcess's worker threads and where synchronization may be needed to prevent such issues. 
    5863 */ 
    5964public class DownloadJob 
    60     implements ActionListener { 
    61       
    62     private boolean debug; 
    63     private boolean higher_directories; 
    64     private boolean no_parents; 
    65     private boolean other_hosts;  
    66     private boolean page_requisites;  
    67     private boolean quiet; 
     65    implements ActionListener, SafeProcess.MainProcessHandler { 
    6866 
    6967    private AppendLineOnlyFileDocument download_log; 
     
    7169    private DownloadProgressBar progress; 
    7270 
    73     private int depth; 
    7471    private int previous_state; 
    7572    private int state; 
    7673 
    77     private String download_url = ""; 
     74    private SafeProcess prcs = null; 
     75 
     76    private final String download_url; 
    7877     
    7978    //    private String current_url; 
    8079    //    private String destination; 
    81     private String proxy_pass; 
    82     private String proxy_user; 
    83  
    84     private Vector encountered_urls; 
    85     private Vector failed_urls; 
     80    private final String proxy_pass; 
     81    private final String proxy_user; 
     82 
     83    //private final Vector encountered_urls; 
     84    //private Vector failed_urls; 
    8685    private Download download; 
    8786    private DownloadScrollPane mummy; 
    8887    private HashMap download_option; 
    8988 
    90     public static int COMPLETE = 0; 
    91     public static int PAUSED   = 1; 
    92     public static int RUNNING  = 2; 
    93     public static int STOPPED  = 3; 
    94  
    95     public static int UNKNOWN_MAX  = 0; 
    96     public static int DEFINED_MAX  = 1; 
    97     public static int UNDEFINED_MAX  = 2; 
     89    public static final int COMPLETE = 0; 
     90    public static final int PAUSED   = 1; 
     91    public static final int RUNNING  = 2; 
     92    public static final int STOPPED  = 3; 
     93 
     94    public static final int UNKNOWN_MAX  = 0; 
     95    public static final int DEFINED_MAX  = 1; 
     96    public static final int UNDEFINED_MAX  = 2; 
    9897     
    9998    // To prematurely terminate wget, we will need to use sockets and find a free port. 
     
    103102    private static int nextFreePort = PORT_BASE;  // Keep track what port numbers we have checked for availability 
    104103    int port;                                     // package access. The socket port number this instance of DownloadJob will use 
     104                                             // only the main thread (where DownloadJob runs) modifies port, so no synching needed 
    105105     
    106     private String mode = null; 
     106    private final String mode; 
    107107    
    108     private String proxy_url; 
     108    private String proxy_url; // only the main thread (where DownloadJob runs) modifies this, so no synching needed 
    109109     
    110110    /** 
     
    149149 
    150150    progress = new DownloadProgressBar(this,download_url, true); 
    151     encountered_urls = new Vector(); 
    152     failed_urls = new Vector(); 
     151    //encountered_urls = new Vector(); 
     152    //failed_urls = new Vector(); 
    153153 
    154154    previous_state = STOPPED; 
     
    167167    } 
    168168 
    169     /** Depending on which button on the progress bar was pushed, 
     169    /** Depending on which button on the progress bar was pushed, 
    170170     * this method will affect the state of the DownloadJob and perhaps make 
    171171     * calls to wget.class if necessary. 
     
    173173     * which we must respond to. 
    174174     */ 
    175     public void actionPerformed(ActionEvent event) { 
     175    public void old_actionPerformed(ActionEvent event) { 
    176176    // The stop_start_button is used to alternately start or stop the 
    177177    // job. If the current state of the job is paused then this 
     
    195195    } 
    196196    } 
     197     
     198    /** Depending on which button on the progress bar was pushed, 
     199     * this method will affect the state of the DownloadJob and perhaps make 
     200     * calls to wget.class if necessary. 
     201     * @param event The ActionEvent fired from within the DownloadProgressBar 
     202     * which we must respond to. 
     203     * Now using synchronized methods like previous_state = getState(); instead of 
     204     * previous_state = state; and setState(STOPPED); instead of state = STOPPED; 
     205     */ 
     206    public void actionPerformed(ActionEvent event) { 
     207    // The stop_start_button is used to alternately start or stop the 
     208    // job. If the current state of the job is paused then this 
     209    // restart is logically equivalent to a resume. 
     210    if(event.getSource() == progress.stop_start_button) { 
     211        previous_state = getState(); 
     212        if (getState() == RUNNING) { 
     213        //setState(STOPPED); 
     214        stopDownload(); // cancels any running SafeProcess       
     215        } else { 
     216        //previous_state = getState(); 
     217        setState(RUNNING); 
     218        mummy.resumeThread(); 
     219        } 
     220    } 
     221    else if (event.getSource() == progress.close_button) { 
     222        SafeProcess.log("@@@ Progress bar close button pressed"); 
     223        if(getState() == RUNNING) { 
     224        previous_state = getState(); 
     225        //setState(STOPPED); // do we need to do anything else to stop this? YES, we do: 
     226        stopDownload(); // cancels any running SafeProcess       
     227        }  
     228        mummy.deleteDownloadJob(this); 
     229    } 
     230    } 
    197231 
    198232    /** Given a portnumber to check, returns true if it is available 
     
    229263    }  
    230264 
    231     public void callDownload() { 
     265    // If eschewing the use of SafeProcess, reactivate (by renaming) old_callDownload() 
     266    // and old_actionPerformed(), and DownloadScrollPane.java's old_deleteDownloadJob(). 
     267    public void old_callDownload() { 
    232268 
    233269    ArrayList command_list = new ArrayList(); 
     
    537573    } 
    538574 
     575    public void callDownload() { 
     576 
     577    ArrayList command_list= new ArrayList(); 
     578     
     579    // the following also works for client-gli if downloading is enabled (when there's a gs2build directory inside gli) 
     580    command_list.add(Configuration.perl_path); 
     581    command_list.add("-S"); 
     582    command_list.add(LocalGreenstone.getBinScriptDirectoryPath()+"downloadfrom.pl"); 
     583    command_list.add("-download_mode"); 
     584    command_list.add(mode); 
     585    command_list.add("-cache_dir"); 
     586    command_list.add(Gatherer.getGLIUserCacheDirectoryPath()); 
     587    // For the purposes of prematurely terminating wget from GLI (which creates a socket 
     588    // as a communication channel between GLI and Perl), it is important to tell the script 
     589    // that we're running as GLI. Because when running from the command prompt, it should 
     590    // not create this socket and do the related processing. 
     591    command_list.add("-gli"); 
     592     
     593    ArrayList all_arg = download.getArguments(true,false); 
     594    for(int i = 0; i < all_arg.size(); i++) { 
     595        Argument argument = (Argument) all_arg.get(i); 
     596        if(argument.isAssigned()) { 
     597        command_list.add("-" + argument.getName()); 
     598        if(argument.getType() != Argument.FLAG) { 
     599            command_list.add(argument.getValue()); 
     600        } 
     601        }    
     602    } 
     603 
     604    String [] cmd = (String []) command_list.toArray(new String[0]); 
     605    DebugStream.println("Download job, "+command_list); 
     606 
     607    if (previous_state == DownloadJob.COMPLETE) { 
     608        progress.mirrorBegun(true, true); 
     609    } 
     610    else { 
     611        progress.mirrorBegun(false, true); 
     612    } 
     613 
     614    try { 
     615        Runtime rt = Runtime.getRuntime(); 
     616 
     617        String [] env = null; 
     618          
     619        if (Utility.isWindows()) { 
     620                prcs = new SafeProcess(cmd); 
     621        } 
     622        else { 
     623        if (proxy_url != null && !proxy_url.equals("")) {            
     624            // Specify proxies as environment variables 
     625            // Need to manually specify GSDLHOME and GSDLOS also 
     626            env = new String[4]; 
     627                    proxy_url = proxy_url.replaceAll("http://",""); 
     628            env[0] = "http_proxy=http://"+proxy_url; 
     629            env[1] = "ftp_proxy=ftp://"+proxy_url; 
     630            env[2] = "GSDLHOME=" + Configuration.gsdl_path; 
     631            env[3] = "GSDLOS=" + Gatherer.client_operating_system; 
     632 
     633            prcs = new SafeProcess(cmd, env, null); 
     634        }  
     635        else if(Gatherer.isGsdlRemote && Gatherer.isDownloadEnabled) { 
     636            // Not Windows, but running client with download panel 
     637            // Need to manually specify GSDLHOME and GSDLOS 
     638            env = new String[2]; 
     639            env[0] = "GSDLHOME=" + Configuration.gsdl_path; 
     640            env[1] = "GSDLOS=" + Gatherer.client_operating_system; 
     641 
     642            prcs = new SafeProcess(cmd, env, null); 
     643        } 
     644        else { 
     645            // Will inherit the GLI's environment, with GSDLHOME and GSDLOS set 
     646            prcs = new SafeProcess(cmd); 
     647        } 
     648        } 
     649        //System.out.println(newcmd); 
     650        prcs.setMainHandler(this); // attach handler to clean up before and after process.destroy() 
     651                                   // for which DownloadJob implements SafeProcess.MainProcessHandler  
     652 
     653        // To be able to stop Wget, we use sockets to communicate with the perl process that launched wget 
     654        if (mode.equals("Web") || mode.equals("MediaWiki")) { // wget download modes other than OAI 
     655         
     656        // Need to find an available (unused) port within the range we're looking for to pass it 
     657        // the Perl child process, so that it may set up a listening ServerSocket at that port number 
     658        try { 
     659            boolean foundFreePort = false; 
     660            for(int i = 0; i < PORT_BLOCK_SIZE; i++) { 
     661 
     662            if(isPortAvailable(nextFreePort)) { 
     663                foundFreePort = true; 
     664                break; 
     665                 
     666            } else { 
     667                incrementNextFreePort(); 
     668            } 
     669            } 
     670             
     671            if(foundFreePort) { 
     672            // Free port number currently found becomes the port number of the socket that this 
     673            // DownloadJob instance will be connecting to when the user wants to prematurely stop Wget. 
     674            this.port = nextFreePort; 
     675            incrementNextFreePort(); //// Necessary? 
     676             
     677            } else { 
     678            throw new Exception("Cannot find an available port in the range "  
     679                        + PORT_BASE + "-" + (PORT_BASE+PORT_BLOCK_SIZE) 
     680                        + "\nwhich is necessary for forcibly terminating wget."); 
     681            } 
     682 
     683            // Communicate the chosen port for this DownloadJob instance to the perl process, so 
     684            // that it can set up a ServerSocket at that port to listen for any signal to terminate wget 
     685            //OutputStream os = prcs.getOutputStream(); 
     686            String p = ""+this.port+"\n"; 
     687            System.err.println("Portnumber found: " + p); 
     688 
     689            prcs.setInputString(p); 
     690           
     691        } catch(Exception ex) { 
     692            System.err.println("Sent available portnumber " + this.port + " to process' outputstream.\nBut got exception: " + ex); 
     693        } 
     694        } 
     695 
     696        ProcessErrHandler errHandler = new ProcessErrHandler(); // meaningful output comes from prcs stderr 
     697        ProcessOutHandler outHandler = new ProcessOutHandler(); // debugging output comes from prcs' stdout 
     698 
     699        int exitVal = prcs.runProcess(null, outHandler, errHandler); 
     700         
     701        // if prcs is interrupted (cancelled) during the blocking runProcess() call, 
     702        // as happens on state == STOPPED, then 
     703        // beforeWaitingForStreamsToEnd() is called before the process' worker threads come to a halt 
     704        // and afterStreamsEnded() is called when the process' worker threads have halted, 
     705        // beforeProcessDestroy() is called before the process is destroyed, 
     706        // and afterProcessDestroy() is called after the proc has been destroyed. 
     707        // If when beforeWaitingForStreamsEnd() stage the perl was still running but had been  
     708        // told to stop, then the beforeWaitingForStreamsEnd() method will make sure to communicate 
     709        // with the perl process over a socket and send it the termination message,  
     710        // which will also kill any runnning wget that perl launched. 
     711        // In that case, destroy() is actually called on the process at last. 
     712 
     713    } 
     714    catch (Exception ioe) { 
     715        SafeProcess.log(ioe); 
     716        DebugStream.printStackTrace(ioe); 
     717    } 
     718 
     719    // now the process is done, we can at last null it 
     720    prcs = null; 
     721 
     722    // If we've got to here and the state isn't STOPPED then the 
     723    // job is complete. 
     724    if(getState() == DownloadJob.RUNNING) { 
     725        progress.mirrorComplete(); 
     726        previous_state = getState(); 
     727        setState(DownloadJob.COMPLETE);      
     728    }    
     729 
     730    SafeProcess.log("@@@@ DONE callDownload()"); 
     731     
     732    /* 
     733    // Regardless of whether state==STOPPED or ends up being COMPLETE, the process is at an end now. 
     734    // Notify the DownloadScrollPane which is waiting on this job to complete that we are ready  
     735    synchronized(this) { 
     736        System.err.println("**************** Notifying download scrollpane"); 
     737        this.notify(); 
     738    } 
     739    */   
     740 
     741    // refresh the workspace tree 
     742    Gatherer.g_man.refreshWorkspaceTree(WorkspaceTree.DOWNLOADED_FILES_CHANGED); 
     743    } 
     744 
     745    private synchronized boolean isStopped() { return state == STOPPED; } 
     746 
     747    // called when the user cancelled the download and we're told to stop both our external perl process 
     748    // and the wget process that it in turn launched 
     749    public void stopDownload() {     
     750    if(prcs != null) {       
     751        SafeProcess.log("@@@ Going to interrupt the SafeProcess...");        
     752 
     753        // Whether a process ends naturally or is prematurely ended, beforeWaitingForStreamsToEnd() 
     754        // will be called. We've hooked this in to calling tellPerlToTerminateWget() only if the 
     755        // process is still running when cancel is pressed, but not when it's naturally terminated. 
     756        boolean hadToSendInterrupt = prcs.cancelRunningProcess(); // returns false if it was already terminating/terminated, true if interrupt sent 
     757 
     758 
     759        /* 
     760          // if process terminating naturally but waiting for process' worker threads to join(), 
     761          // shall we just remove the progress bar display for this download? 
     762          // If so, do this section in place of the 2 calls to progress.enableCancelJob(boolean) below 
     763        if(!hadToSendInterrupt && SwingUtilities.isEventDispatchThread()) { 
     764        if(getState() == DownloadJob.RUNNING) { 
     765            progress.mirrorComplete(); 
     766            previous_state = getState(); 
     767            setState(DownloadJob.COMPLETE);      
     768        } 
     769        mummy.deleteCurrentDownloadJob(this); // why wait for the cleanup which can't be interrupted anyway? 
     770        } 
     771        */ 
     772    } else { 
     773        System.err.println("@@@@ No process to interrupt"); 
     774    } 
     775 
     776    //setState(STOPPED); // would set it to stop on cancel, even if it already naturally terminated 
     777     
     778    } 
     779     
     780//*********** START of implementing interface Safeprocess.MainProcessHandler 
     781    // before and after processDestroy only happen when interrupted AND terminatePerlScript=true 
     782    public void beforeProcessDestroy() {}     
     783    public void afterProcessDestroy() {} 
     784 
     785    // after blocking call on closing up streamgobbler worker threads that happens 
     786    // upon natural termination or interruption of process' main body/thread. 
     787    // if not overriding, then return the parameter forciblyTerminating as-is 
     788    public boolean afterStreamsEnded(boolean forciblyTerminating) { return forciblyTerminating; } 
     789 
     790    // called after the SafeProcess has fully terminated (naturally or via process.destroy()) 
     791    // and has been cleaned up 
     792    public void doneCleanup(boolean wasForciblyTerminated) { 
     793    // let the user know they can cancel again now cleanup phase is done 
     794    progress.enableCancelJob(true); 
     795     
     796    if(wasForciblyTerminated) { 
     797        setState(STOPPED); // sets it to stop only if process truly was prematurely terminated, not merely 
     798        // if the cancel button was clicked when it had already naturally terminated 
     799 
     800        // we're now ready to remove the display of the until now running job 
     801        // from the download progress bar interface 
     802        mummy.deleteCurrentDownloadJob(this);  
     803    } /*else { 
     804        // If we've got to here and the state isn't STOPPED then the 
     805        // job is complete. 
     806        System.err.println("**************** NOT Notifying download scrollpane"); 
     807        if(getState() == DownloadJob.RUNNING) { 
     808        progress.mirrorComplete(); 
     809        previous_state = getState(); 
     810        setState(DownloadJob.COMPLETE);      
     811        } 
     812    } 
     813     
     814    // Regardless of whether state==STOPPED or ends up being COMPLETE, the process is at an end now. 
     815    // Notify the DownloadScrollPane which is waiting on this job to complete that we are ready  
     816    synchronized(this) { 
     817        System.err.println("**************** Notifying download scrollpane"); 
     818        this.notify(); 
     819        }*/ 
     820 
     821    } 
     822 
     823    // before blocking call of ending streamgobbler worker threads that happens 
     824    // after process' main body/thread has naturally terminated or been interrupted 
     825    public boolean beforeWaitingForStreamsToEnd(boolean forciblyTerminating) { 
     826    // let the user know they can't cancel during cleanup phase 
     827    progress.enableCancelJob(false); 
     828 
     829    SafeProcess.log("**** in beforeWaitingForStreamsToEnd()"); 
     830 
     831    // state would not be STOPPED if cancel was pressed after the process naturally terminated anyway 
     832    // in that case we don't need to send perl the signal to terminate WGET 
     833    if(!forciblyTerminating) { //if(!isStopped()) { 
     834        SafeProcess.log("*** Process not (yet) cancelled/state not (yet) stopped"); 
     835        SafeProcess.log("*** But process has naturally terminated (process streams are being closed before any interruption signal can be received), so won't be destroying process even on interrupt"); 
     836        return false; // for us to be in this method at all with forciblyTerminating being false  
     837            // means the process is already naturally terminating, so don't unnaturally destroy it 
     838    } 
     839     
     840    // else the process is still running and we've been told to stop, so tell perl to stop wget first 
     841    // (so that process destroy can then be called thereafter) 
     842    return tellPerlToTerminateWget(); 
     843    } 
     844//*********** END of implementing interface Safeprocess.MainProcessHandler 
     845     
     846    public boolean tellPerlToTerminateWget() { 
     847    SafeProcess.log("**** in tellPerlToTerminateWget()"); 
     848     
     849    boolean terminatePerlScript = true; 
     850     
     851    // When GLI is working with wget-based download modes other than OAI (MediaWiki and Web  
     852    // download) and the STOP button has been pressed, wget needs to be prematurely terminated. 
     853    // Only wget download modes Web and MediaWiki require the use of sockets to communicate 
     854    // with the perl script in order to get wget to terminate. Other download modes, including 
     855    // wgetdownload mode OAI, can terminate in the traditional manner: close process inputstream  
     856    // and kill perl process. OAI launches many wgets. So that when the perl process is terminated, 
     857    // the currently running wget will finish off but other wgets are no longer launched.  
     858    if((mode.equals("Web") || mode.equals("MediaWiki"))) {  
     859        SafeProcess.log("@@@ Socket communication to end wget"); 
     860        // create a socket to the perl child process and communicate the STOP message 
     861        Socket clientSocket = null; 
     862        BufferedReader clientReader = null; 
     863        OutputStream os = null; 
     864         
     865        if(clientSocket == null) { 
     866        try { 
     867            clientSocket = new Socket("localhost", this.port); // connect to the port chosen for this DownloadJob instance 
     868             
     869            clientReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); 
     870            String response = clientReader.readLine(); // see if we've been connected 
     871            System.err.println("Communicating with perl download script on port " + this.port  
     872                       + "\nGot response from perl: " + response);  
     873             
     874            // Send the STOP signal 
     875            os = clientSocket.getOutputStream(); 
     876            String message = "<<STOP>>\n"; 
     877            os.write(message.getBytes()); 
     878            response = clientReader.readLine(); // see whether the stop signal has been received 
     879            System.err.println("GLI sent STOP signal to perl to terminate wget."  
     880                       + "\nGot response from perl: " + response); 
     881             
     882            response = clientReader.readLine(); // see whether the perl script is ready to be terminated 
     883            System.err.println("Got another response from perl: " + response);       
     884             
     885            if(response == null) { // why? Is it because the process has already terminated naturally if response is null? 
     886            terminatePerlScript = false; 
     887            } 
     888        } catch(IOException ex) { 
     889            if(ex instanceof IOException && ex.getMessage().indexOf("Connection refused") != -1) { 
     890            terminatePerlScript = false; // no socket listening on other end because process ended 
     891            System.err.println("Tried to communicate through client socket - port " + this.port + ", but the process seems to have already ended naturally"); 
     892            } else { 
     893            System.err.println("Tried to communicate through client socket - port " + this.port + ", but got exception: " + ex); 
     894            } 
     895 
     896        } catch(Exception ex) { 
     897            System.err.println("Tried to open client socket, but got exception: " + ex); 
     898        } finally { 
     899            SafeProcess.closeResource(os); 
     900            SafeProcess.closeResource(clientReader); 
     901            SafeProcess.closeSocket(clientSocket); // close the clientSocket (the Perl end will close the server socket that Perl opened) 
     902            os = null; 
     903            clientReader = null; 
     904            clientSocket = null; 
     905        } 
     906        } 
     907    } 
     908     
     909    return terminatePerlScript; // if true, it will call destroy() on the SafeProcess' process 
     910    } 
     911 
    539912 
    540913    /** Called by the WGet native code when the current download is  
     
    542915     */ 
    543916    public void downloadComplete() { 
    544     progress.downloadComplete(); 
     917    progress.downloadComplete(); // now this is synchronized 
    545918    } 
    546919 
     
    548921    public void downloadComplete(String current_file_downloading) 
    549922    { 
    550     progress.downloadComplete(); 
     923    progress.downloadComplete(); // now this is synchronized 
    551924    DebugStream.println("Download complete: " + current_file_downloading); 
    552925    } 
     
    558931    public void downloadFailed() { 
    559932    // TODO!! 
     933    //synchronized(failed_urls) { 
    560934    //failed_urls.add(current_url); // It is the current url that failed 
    561     progress.downloadFailed(); 
     935    //} 
     936    progress.downloadFailed(); // now this is synchronized 
    562937    //DebugStream.println("Download failed: " + current_url); 
    563938    }  
     
    566941     */ 
    567942    public void downloadWarning() { 
    568     progress.downloadWarning(); 
     943    progress.downloadWarning(); // now this is synchronized 
    569944    } 
    570945 
     
    584959     * @return An int representing the current DownloadJob state. 
    585960     */ 
    586     public int getState() { 
     961    public synchronized int getState() { 
    587962    return state; 
    588963    } 
     
    592967     * stop. 
    593968     */ 
    594     public boolean hasSignalledStop() { 
     969    public synchronized boolean hasSignalledStop() { 
    595970    if(state == DownloadJob.STOPPED || state == DownloadJob.PAUSED ||  
    596971       state == DownloadJob.COMPLETE) { 
     
    600975    } 
    601976 
    602     public void setState(int state) { 
     977    public synchronized void setState(int state) { 
    603978    previous_state = this.state; 
    604979    this.state = state; 
     
    624999 
    6251000     
    626     // Inner thread class that reads from process downloadfrom.pl's errorstream 
    627     private class PerlReaderThread extends Thread { 
    628     Process prcs = null; 
    629  
    630     public PerlReaderThread(Process proc) { 
    631         this.prcs = proc; 
    632     } 
    633  
    634     public void run() { 
     1001    /* 
     1002      Go through https://docs.oracle.com/javase/tutorial/essential/concurrency/atomicvars.html series of 
     1003      Java articles on concurrency again. 
     1004      Go through http://docs.oracle.com/javase/tutorial/uiswing/concurrency/ 
     1005 
     1006      http://stackoverflow.com/questions/574240/is-there-an-advantage-to-use-a-synchronized-method-instead-of-a-synchronized-blo 
     1007 
     1008       "Not only do synchronized methods not lock the whole class, but they don't lock the whole instance either. Unsynchronized methods in the class may still proceed on the instance." 
     1009       "Only the syncronized methods are locked. If there are fields you use within synced methods that are accessed by unsynced methods, you can run into race conditions." 
     1010        
     1011       "synchronizing on "this" is considered in some circles to be an anti-pattern. The unintended consequence is that outside of the class someone can lock on an object reference that is equal to "this" and prevent other threads from passing the barriers within the class potentially creating a deadlock situation. Creating a "private final Object = new Object();" variable purely for locking purposes is the often used solution. Here's another question relating directly to this issue. http://stackoverflow.com/questions/442564/avoid-synchronizedthis-in-java?lq=1" 
     1012 
     1013       "A private lock is a defensive mechanism, which is never a bad idea. 
     1014 
     1015       Also, as you alluded to, private locks can control granularity. One set of operations on an object might be totally unrelated to another but synchronized(this) will mutually exclude access to all of them." 
     1016 
     1017     http://stackoverflow.com/questions/8393883/is-synchronized-keyword-exception-safe 
     1018     "In any scoped thread-safe block, the moment you get out of it, the thread-safety is gone." 
     1019     "In case of an exception the lock will be released." 
     1020 
     1021     http://stackoverflow.com/questions/8259479/should-i-synchronize-listener-notifications-or-not 
     1022     "Use a CopyOnWriteArrayList for your listener arrays." 
     1023     "If you use the CopyOnWriteArrayList, then you don't have to synchronize when iterating." 
     1024     "CopyOnWriteArrayList is thread-safe, so there is no need to synchronize." 
     1025 
     1026     "Use a ConcurrentLinkedQueue<Listener> ... for this kind of problems: adding, removing and iterating simultaneously on a collection. 
     1027     A precision : this solution prevents a listener from being called from the very moment it is deregistered." 
     1028     "It means that you start iterating, an element is added, it will be called, another is removed, it won't, all this in the same iteration cycle. 
     1029     It's the best of both world: ensuring synchronization, while being fine grained on who gets called and who's not." 
     1030 
     1031     http://stackoverflow.com/questions/8260205/when-a-listener-is-removed-is-it-okay-that-the-event-be-called-on-that-listener 
     1032 
     1033     http://stackoverflow.com/questions/2282166/java-synchronizing-on-primitives 
     1034      
     1035     1. You can't lock on a primitive and 
     1036     2. Don't lock on a Long unless you're careful how you construct them. Long values created by autoboxing or Long.valueOf() in a certain range are guaranteed to be the same across the JVM which means other threads could be locking on the same exact Long object and giving you cross-talk. This can be a subtle concurrency bug (similar to locking on intern'ed strings). 
     1037 
     1038     Cross-talk: 
     1039     "In electronics, crosstalk is any phenomenon by which a signal transmitted on one circuit or channel of a transmission system creates an undesired effect in another circuit or channel. Crosstalk is usually caused by undesired capacitive, inductive, or conductive coupling from one circuit, part of a circuit, or channel, to another." 
     1040    */ 
     1041 
     1042 
     1043    // Inner thread class that reads from process downloadfrom.pl's std output stream 
     1044    private class ProcessOutHandler extends SafeProcess.CustomProcessHandler { 
     1045     
     1046    public ProcessOutHandler() {  
     1047        super(SafeProcess.STDOUT); 
     1048    } 
     1049 
     1050    public void run(Closeable stream) { 
     1051        InputStream is = (InputStream) stream; 
     1052        BufferedReader eReader = null; 
    6351053        try { 
    636         if(prcs != null) { 
    637             String message = null; 
    638             BufferedReader eReader = new BufferedReader(new InputStreamReader(prcs.getInputStream())); 
    639             while(prcs != null && (message = eReader.readLine()) != null) { 
    640             if(!message.equals("\n")) { 
    641                 System.err.println("**** Perl STDOUT: " + message); 
    642             } 
    643             } 
    644              
    645             if(prcs != null && eReader != null) { 
    646             eReader.close(); 
    647             eReader = null; 
    648             System.err.println("**** Perl ENDed."); 
    649             } 
    650         } 
     1054         
     1055        String message = null; 
     1056        eReader = new BufferedReader(new InputStreamReader(is)); 
     1057        while(!Thread.currentThread().isInterrupted() && (message = eReader.readLine()) != null) { 
     1058            if(!message.equals("\n")) { 
     1059            System.err.println("**** Perl STDOUT: " + message); 
     1060            } 
     1061        } 
     1062        if(Thread.currentThread().isInterrupted()) { 
     1063            System.err.println("**** Perl INTERRUPTed."); 
     1064        } else { 
     1065            System.err.println("**** Perl ENDed."); 
     1066        } 
     1067         
    6511068        } catch(Exception e) { 
    6521069        System.err.println("Thread - caught exception: " + e); 
    653         } 
     1070        } finally { 
     1071        if(Thread.currentThread().isInterrupted()) { 
     1072            SafeProcess.log("@@@ Successfully interrupted " + Thread.currentThread().getName() + "."); 
     1073        } 
     1074        SafeProcess.closeResource(eReader); 
     1075        eReader = null; 
     1076        } 
     1077    } 
     1078    } 
     1079 
     1080 
     1081    private class ProcessErrHandler extends SafeProcess.CustomProcessHandler { 
     1082     
     1083    public ProcessErrHandler() {  
     1084        super(SafeProcess.STDERR); 
     1085    } 
     1086 
     1087    public void run(Closeable stream) { 
     1088        InputStream eis = (InputStream) stream; 
     1089         
     1090        BufferedReader br = null; 
     1091        try { 
     1092        br = new BufferedReader(new InputStreamReader(eis)); 
     1093         
     1094        // Capture the standard error stream and search for two particular occurrences. 
     1095        String line=""; 
     1096        boolean ignore_for_robots = false; 
     1097        int max_download = DownloadJob.UNKNOWN_MAX; 
     1098         
     1099        // handle to outer class objects that need synchronization (on either objects or their methods) 
     1100        DownloadProgressBar progress = DownloadJob.this.progress; 
     1101        AppendLineOnlyFileDocument download_log = DownloadJob.this.download_log; 
     1102 
     1103        while (!Thread.currentThread().isInterrupted() && (line = br.readLine()) != null 
     1104               && !line.trim().equals("<<Finished>>") /*&& !isStopped()*/) { 
     1105            if (max_download == DownloadJob.UNKNOWN_MAX) { 
     1106            if(line.lastIndexOf("<<Defined Maximum>>") != -1) { 
     1107                max_download = DownloadJob.DEFINED_MAX; 
     1108            } 
     1109            else if (line.lastIndexOf("<<Undefined Maximum>>") != -1) { 
     1110                max_download = DownloadJob.UNDEFINED_MAX; 
     1111            } 
     1112            } 
     1113            else if(max_download == DownloadJob.UNDEFINED_MAX) { 
     1114            DebugStream.println(line);           
     1115            download_log.appendLine(line); // now synchronized 
     1116            // The first magic special test is to see if we've just 
     1117            // asked for the robots.txt file. If so we ignore 
     1118            // the next add and then the next complete/error. 
     1119            if(line.lastIndexOf("robots.txt;") != -1) { 
     1120                DebugStream.println("***** Requesting robot.txt"); 
     1121                ignore_for_robots = true; 
     1122            } 
     1123            // If line contains "=> `" display text as the  
     1124            // currently downloading url. Unique to add download. 
     1125            else if(line.lastIndexOf("=> `") != -1) { 
     1126                if(!ignore_for_robots) { 
     1127                // Add download 
     1128                String new_url = line.substring(line.indexOf("`") + 1, line.lastIndexOf("'")); 
     1129                 
     1130                // now synchronized 
     1131                progress.addDownload("file"); //addDownload("http:/" + new_url.substring(cachedir_prefix_length()-1)); 
     1132                } 
     1133            } 
     1134            // If line contains "/s) - `" set currently  
     1135            // downloading url to "Download Complete". 
     1136            else if(line.lastIndexOf("/s) - `") != -1) { 
     1137                String current_file_downloading = line.substring(line.indexOf("`") + 1, line.lastIndexOf("'")); 
     1138                if(!ignore_for_robots) { 
     1139                DebugStream.println("Not ignore for robots"); 
     1140                // Download complete 
     1141                downloadComplete(current_file_downloading); // synchronized 
     1142                } 
     1143                else { 
     1144                DebugStream.println("Ignore for robots"); 
     1145                ignore_for_robots = false; 
     1146                } 
     1147            } 
     1148            // The already there line begins "File `..." However this 
     1149            // is only true in english, so instead I looked and there 
     1150            // are few (if any at all) other messages than those above 
     1151            // and not overwriting messages that use " `" so we'll  
     1152            // look for that. Note this method is not guarenteed to be 
     1153            // unique like the previous two. 
     1154            else if(line.lastIndexOf(" `") != -1) { 
     1155                // Not Overwriting 
     1156                DebugStream.println("Already there."); 
     1157                String new_url = line.substring(line.indexOf("`") + 1, line.lastIndexOf("'")); 
     1158                 
     1159                progress.addDownload("file"); //addDownload("http:/" + new_url.substring(cachedir_prefix_length()-1)); 
     1160                downloadWarning(); 
     1161            } 
     1162            // Any other important message starts with the time in the form hh:mm:ss 
     1163            else if(line.length() > 7) { 
     1164                if(line.charAt(2) == ':' && line.charAt(5) == ':') { 
     1165                if(!ignore_for_robots) { 
     1166                    DebugStream.println("Error."); 
     1167                    downloadFailed(); 
     1168                } 
     1169                else { 
     1170                    ignore_for_robots = false; 
     1171                } 
     1172                } 
     1173            } 
     1174            } 
     1175            else if (max_download == DownloadJob.DEFINED_MAX) { 
     1176            if (line.lastIndexOf("<<Total number of record(s):") != -1) { 
     1177                String total_ID = line.substring(line.indexOf(":") + 1, line.indexOf(">")); 
     1178 
     1179                progress.setTotalDownload((Integer.valueOf(total_ID)).intValue()); 
     1180                progress.resetFileCount();   
     1181                progress.addDownload("files"); // for display: "Downloading files" 
     1182 
     1183            } 
     1184            else if (line.lastIndexOf("<<Done>>") != -1) { 
     1185                progress.increaseFileCount(); 
     1186            } 
     1187            else if(line.lastIndexOf("<<Done:") != -1) { 
     1188                String completed_amount = line.substring(line.indexOf(":") + 1, line.indexOf(">")); 
     1189                progress.increaseFileCount((Integer.valueOf(completed_amount)).intValue()); 
     1190            } 
     1191             
     1192            DebugStream.println(line); 
     1193            download_log.appendLine(line); 
     1194            } 
     1195            else { 
     1196            System.out.println("Error!!"); 
     1197            System.exit(-1); 
     1198            } 
     1199        } 
     1200         
     1201        } catch (IOException ioe) { 
     1202        //message(Utility.ERROR, ioe.toString()); 
     1203        //JTest 
     1204        DebugStream.printStackTrace(ioe); 
     1205 
     1206        } finally { 
     1207        if(Thread.currentThread().isInterrupted()) { // if the thread this class is running in is interrupted 
     1208            SafeProcess.log("@@@ Successfully interrupted " + Thread.currentThread().getName() + ".");           
     1209        } 
     1210 
     1211        SafeProcess.closeResource(br); 
     1212        br = null; 
     1213        } 
     1214         
    6541215    } 
    6551216    } 
  • main/trunk/gli/src/org/greenstone/gatherer/download/DownloadProgressBar.java

    r22103 r31692  
    128128    JPanel button_pane = new JPanel(); 
    129129 
     130    // our "pause" button never paused before, it always did a process.destroy() on being pressed. 
     131    // See http://trac.greenstone.org/browser/trunk/gli/src/org/greenstone/gatherer/download/DownloadJob.java?rev=13594 
     132    // However, now we additionally ensure that the wget launched by the perl is stopped before  
     133    // process.destroy(), so at least it cleans up better. I'm therefore changing it to a "Stop" button. 
    130134    stop_start_button = new GLIButton(Dictionary.get("Mirroring.DownloadJob.Pause"),Dictionary.get("Mirroring.DownloadJob.Pause_Tooltip")); 
     135    //stop_start_button = new GLIButton(Dictionary.get("Mirroring.DownloadJob.Stop"),Dictionary.get("Mirroring.DownloadJob.Stop_Tooltip")); 
    131136    stop_start_button.addActionListener(this); 
    132137    stop_start_button.addActionListener(owner); 
     
    166171    // Make the labels, etc update. 
    167172    refresh(); 
     173    } 
     174 
     175    // internally thread-safe 
     176    public void enableCancelJob(boolean isEnabled) { 
     177    synchronized(stop_start_button) { 
     178        stop_start_button.setEnabled(isEnabled); 
     179    } 
     180    synchronized(stop_start_button) { 
     181        close_button.setEnabled(isEnabled); 
     182    } 
    168183    } 
    169184 
     
    212227     * @param url The url String of the file that is being downloaded. 
    213228     */ 
    214     public void addDownload(String url) { 
     229    public synchronized void addDownload(String url) { 
    215230    current_url = url; 
    216231    file_size = 0; 
     
    221236     * is called to enlighten the DownloadProgressBar of this fact. 
    222237     */ 
    223     public void downloadComplete() { 
     238    public synchronized void downloadComplete() { 
    224239    current_url = null; 
    225240    file_count++; 
     
    232247    } 
    233248 
    234     public void downloadFailed() { 
     249    public synchronized void downloadFailed() { 
    235250    err_count++; 
    236251    if(total_count < (file_count + err_count + warning_count)) { 
     
    240255    } 
    241256 
    242     public void downloadWarning() { 
     257    public synchronized void downloadWarning() { 
    243258    warning_count++; 
    244259    if(total_count < (file_count + err_count + warning_count)) { 
     
    248263    } 
    249264 
    250     public void setTotalDownload(int total_download) { 
     265    // need to make these methods synchronized too, as they modify variables 
     266    // that other synchronized methods work with. And if any methods that modify 
     267    // such variables were to remain unsynchronized, can end up with race conditions 
     268    // http://stackoverflow.com/questions/574240/is-there-an-advantage-to-use-a-synchronized-method-instead-of-a-synchronized-blo 
     269    // "Not only do synchronized methods not lock the whole class, but they don't lock the whole instance either. Unsynchronized methods in the class may still proceed on the instance." 
     270    // "Only the syncronized methods are locked. If there are fields you use within synced methods that are accessed by unsynced methods, you can run into race conditions." 
     271    public synchronized void setTotalDownload(int total_download) { 
    251272    total_count = total_download; 
    252273    refresh(); 
     
    257278    } 
    258279 
    259     public void increaseFileCount() { 
     280    public synchronized void increaseFileCount() { 
    260281    file_count++; 
    261282    refresh(); 
    262283    } 
    263284 
    264     public void increaseFileCount(int amount) { 
     285    public synchronized void increaseFileCount(int amount) { 
    265286    file_count += amount; 
    266287    refresh(); 
    267288    } 
    268289 
    269     public void resetFileCount() { 
     290    public synchronized void resetFileCount() { 
    270291    file_count = 0; 
    271292    refresh(); 
     
    278299     * reset to zero. 
    279300     */ 
    280     public void mirrorBegun(boolean reset, boolean simple) { 
     301    public synchronized void mirrorBegun(boolean reset, boolean simple) { 
    281302    if(reset) { 
    282303        this.file_count = 0; 
     
    301322     * components. 
    302323     */ 
    303     public void mirrorComplete() { 
     324    public synchronized void mirrorComplete() { 
    304325    current_action = DownloadJob.COMPLETE; 
    305326    current_url = null; 
     
    319340     * the amount of the current file downloaded. 
    320341     */ 
    321     public void updateProgress(long current, long expected) { 
     342    public synchronized void updateProgress(long current, long expected) { 
    322343    file_size = file_size + current; 
    323344    if(!progress.isIndeterminate()) { 
  • main/trunk/gli/src/org/greenstone/gatherer/download/DownloadScrollPane.java

    r18221 r31692  
    4444import javax.swing.tree.*; 
    4545import org.greenstone.gatherer.*; 
     46import org.greenstone.gatherer.util.SafeProcess; 
    4647 
    4748/** This class provides access to the functionality of the WGet program, either by calling it via a shell script or by the JNI. It maintains a queue of pending jobs, and the component for showing these tasks to the user. 
     
    7879    } 
    7980 
    80     public void deleteDownloadJob(DownloadJob delete_me) { 
     81    /**  
     82     * To be used with DownloadJob.java's old_callDownload() and old_actionPerformed() 
     83     * OR by uncommenting the "synchronized(this)" section in Download.java at the end of 
     84     * its new callDownload() along with commenting out "mummy.deleteCurrentDownloadJob(this);" 
     85     * in Download.java's doneCleanup(). 
     86    */ 
     87    public void old_deleteDownloadJob(DownloadJob delete_me) { 
    8188    if (delete_me == job) { 
    8289        try { 
     
    8693        synchronized(delete_me) { 
    8794            if (!delete_me.hasSignalledStop()) { // don't wait if DownloadJob.COMPLETED  
     95            ///SafeProcess.log("**************** download scrollpane waiting for downloadjob to stop"); 
    8896            delete_me.wait(); 
    8997            } 
     
    93101        }        
    94102    } 
     103 
     104    ///System.err.println("**************** Deleting job from download scroll pane"); 
    95105    // Close button pressed, get rid of this download's dedicated pane 
    96106    finishedDownloadJob(delete_me, true); 
    97107    } 
     108 
     109    /** 
     110     * If called to delete the current download job, this method won't do anything. 
     111     * But if called on any inactive download job, its display is removed. 
     112    */ 
     113    public void deleteDownloadJob(DownloadJob delete_me) { 
     114    if (delete_me != job) { 
     115 
     116        SafeProcess.log("**************** Deleting job from download scroll pane"); 
     117        // Close button pressed, get rid of this download's dedicated pane 
     118        finishedDownloadJob(delete_me, true); 
     119    } // else don't do anything, we'll be contacted again when the current job can be deleted 
     120 
     121    } 
     122 
     123    /** 
     124     * To be called when we're ready to delete the current download job, 
     125     * else this method won't do anything 
     126    */ 
     127    public void deleteCurrentDownloadJob(DownloadJob delete_me) { 
     128    if (delete_me == job) { 
     129        SafeProcess.log("**************** Deleting current job from download scroll pane"); 
     130        // Close button pressed, get rid of this download's dedicated pane 
     131        finishedDownloadJob(delete_me, true); 
     132    } 
     133    } 
     134 
    98135 
    99136    /** To be called when a download job has terminated naturally or was prematurely stopped 
  • main/trunk/gli/src/org/greenstone/gatherer/shell/GShell.java

    r31670 r31692  
    6363 
    6464/** The <strong>GShell</strong> is reponsible for running a separately threaded process in the command shell. This is necessary for executing the Perl Scripts and also for other system related funcitonality. 
     65 * When modifying this class, bear in mind concurrency issues that could arise with SafeProcess' 
     66 * worker threads and where synchronization may be needed to prevent such issues. 
    6567 */ 
    6668public class GShell 
     
    390392        SafeProcess.log("********** HAS SIGNALLED STOP. INTERRUPTING THE GSHELL/SAFEPROCESS THREAD"); 
    391393 
    392         this.interrupt(); // interrupt this thread which is running the SafeProcess prcs 
    393         // this will propagate the CANCEL status to any worker threads launched by the SafeProcess 
     394        // The following is implemented to interrupt the SafeProcess thread (which is *this* GShell thread), 
     395        // but only if any process is being run by SafeProcess. 
     396        // This will propagate the CANCEL status to any worker threads launched by the SafeProcess. 
     397        // Everything will clean up nicely after itself (even terminating subprocesses that prcs launched on Windows!) 
     398        prcs.cancelRunningProcess();  
     399         
    394400        prcs = null;         
    395401    } 
  • 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;