Ignore:
Timestamp:
2017-04-05T20:22:15+12:00 (7 years ago)
Author:
ak19
Message:

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/greenstone3/src/java/org/greenstone/gsdl3/build/GS2PerlConstructor.java

    r30617 r31574  
    55import org.greenstone.util.Misc;
    66import org.greenstone.util.GlobalProperties;
     7import org.greenstone.util.SafeProcess;
    78
    89// xml classes
     
    2728 * perl scripts to do the building stuff
    2829 */
    29 public class GS2PerlConstructor extends CollectionConstructor
     30public class GS2PerlConstructor extends CollectionConstructor implements SafeProcess.ExceptionHandler
    3031{
    3132    static Logger logger = Logger.getLogger(org.greenstone.gsdl3.build.GS2PerlConstructor.class.getName());
     
    374375    return runPerlCommand(command, null, null);
    375376    }
    376    
    377     protected boolean runPerlCommand(String[] command, String[] envvars, File dir)
     377
     378       
     379    protected boolean runPerlCommand(String[] command, String[] envvars, File dir)
     380    {
     381    boolean success = true;
     382   
     383    int sepIndex = this.gsdl3home.lastIndexOf(File.separator);
     384    String srcHome = this.gsdl3home.substring(0, sepIndex);
     385   
     386    ArrayList<String> args = new ArrayList<String>();
     387    args.add("GSDLHOME=" + this.gsdl2home);
     388    args.add("GSDL3HOME=" + this.gsdl3home);
     389    args.add("GSDL3SRCHOME=" + srcHome);
     390    args.add("GSDLOS=" + this.gsdlos);
     391    args.add("GSDL-RUN-SETUP=true");
     392    args.add("PERL_PERTURB_KEYS=0");
     393   
     394    if(envvars != null) {
     395        for(int i = 0; i < envvars.length; i++) {
     396        args.add(envvars[i]);
     397        }
     398    }
     399   
     400    for (String a : System.getenv().keySet()) {
     401        args.add(a + "=" + System.getenv(a));
     402    }
     403   
     404    String command_str = "";
     405    for (int i = 0; i < command.length; i++) {
     406        command_str = command_str + command[i] + " ";
     407    }
     408   
     409    sendMessage(new ConstructionEvent(this, GSStatus.INFO, "command = " + command_str));
     410   
     411   
     412    logger.info("### Running command = " + command_str);
     413
     414    // This is where we create and run our perl process safely
     415    SafeProcess perlProcess
     416        = new SafeProcess(command, args.toArray(new String[args.size()]), dir); //  dir can be null
     417   
     418   
     419    sendProcessBegun(new ConstructionEvent(this, GSStatus.ACCEPTED, "starting"));
     420   
     421    File logDir = new File(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator + "log");
     422    if (!logDir.exists()) {
     423        logDir.mkdir();
     424    }
     425   
     426    // Only from Java 7+: Try-with-Resources block will safely close the BufferedWriter
     427    // https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
     428    BufferedWriter bw = null;
     429    try {
     430        bw = new BufferedWriter(new FileWriter(GSFile.collectDir(this.site_home) + File.separator + this.collection_name + File.separator + "log" + File.separator + "build_log." + (System.currentTimeMillis()) + ".txt"));       
     431   
     432        bw.write("Document Editor Build \n");       
     433        bw.write("Command = " + command_str + "\n");
     434       
     435        // handle each incoming line from stdout and stderr streams, and any exceptions that occur then
     436        SynchronizedProcessLineByLineHandler outLineByLineHandler
     437        = new SynchronizedProcessLineByLineHandler(bw, SynchronizedProcessLineByLineHandler.STDOUT);
     438        SynchronizedProcessLineByLineHandler errLineByLineHandler
     439        = new SynchronizedProcessLineByLineHandler(bw, SynchronizedProcessLineByLineHandler.STDERR);
     440        perlProcess.setStdOutLineByLineHandler(outLineByLineHandler);
     441        perlProcess.setStdErrLineByLineHandler(errLineByLineHandler);
     442       
     443        // GS2PerlConstructor will do further handling of exceptions that may occur during the perl
     444        // process (including if writing something to the process' inputstream, not that we're doing that for this perlProcess)
     445        perlProcess.setExceptionHandler(this);
     446       
     447        // finally, execute the process
     448       
     449        // Captures the std err of a program and pipes it into
     450        // std in of java, as before.
     451
     452        logger.info("**** BEFORE runProcess.");
     453        perlProcess.runProcess();
     454        logger.info("**** AFTER runProcess:");
     455       
     456    // The original runPerlCommand() code had an ineffective check for whether the cmd had been cancelled
     457    // midway through executing the perl, as condition of a while loop reading from stderr and stdout.
     458    // We don't include the cancel check here, as superclass CollectionConstructor.stopAction(), which set
     459    // this.cancel to true, never got called anywhere.
     460    // But I think a proper cancel of our perl process launched by this GS2PerlConstructor Thread object
     461    // and of the worker threads it launches, could be implemented with interrupts. See:
     462    // http://stackoverflow.com/questions/6859681/better-way-to-signal-other-thread-to-stop
     463    // https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html
     464    // https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#interrupted()
     465    // https://praveer09.github.io/technology/2015/12/06/understanding-thread-interruption-in-java/
     466    // The code that calls GS2PerlConstructor.stopAction() should also call GSPerlConstructor.interrupt()
     467    // Then in SafeProcess.runProcess(), I think the waitFor() will throw an InterruptedException()
     468    // This can be caught and interrupt() called on SafeProcess' workerthreads,
     469    // Any workerthreads' run() methods that block (IO, loops) can test this.isInterrupted()
     470    // and can break out of any loops and release resources in finally.
     471    // Back in SafeProcess.runProcess, the InterruptedException catch block will be followed by finally
     472    // that will clear up any further resources and destroy the process forcibly if it hadn't been ended.
     473
     474    } catch(IOException e) {
     475        e.printStackTrace();
     476        sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Exception occurred " + e.toString()));
     477    } finally {
     478        SafeProcess.closeResource(bw);
     479    }
     480
     481    if (!this.cancel) {
     482        // Now display final message based on exit value           
     483       
     484        if (perlProcess.getExitValue() == 0) { 
     485        //status = OK;
     486        sendProcessStatus(new ConstructionEvent(this, GSStatus.CONTINUING, "Success"));
     487       
     488        success = true;
     489        } else {
     490        //status = ERROR;
     491        sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Failure"));
     492       
     493        //return false;
     494        success = false;
     495       
     496        }
     497    } else { // cancelled
     498       
     499        // 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.
     500        sendProcessStatus(new ConstructionEvent(this, GSStatus.HALTED, "killing the process"));
     501        //prcs.getOutputStream().close();
     502        //prcs.destroy();
     503        ////status = ERROR;
     504       
     505        //return false;
     506        success = false;
     507    }
     508    // we're done, but we don't send a process complete message here cos there might be stuff to do after this has finished.
     509    //return true;     
     510    return success;
     511    }
     512   
     513
     514    // this method is blocking again, on Windows when adding user comments
     515    protected boolean old_runPerlCommand(String[] command, String[] envvars, File dir)
    378516    {
    379517        boolean success = true;
     
    406544            command_str = command_str + command[i] + " ";
    407545        }
     546
     547logger.info("### old runPerlCmd, command = " + command_str);
    408548
    409549        sendMessage(new ConstructionEvent(this, GSStatus.INFO, "command = " + command_str));
     
    454594                }
    455595            }
    456             closeResource(bw);
     596            SafeProcess.closeResource(bw);
    457597           
    458598            if (!this.cancel)
     
    500640       
    501641             if( prcs != null ) {
    502                 closeResource(prcs.getErrorStream());
    503                 closeResource(prcs.getOutputStream());
    504                 closeResource(prcs.getInputStream());
     642                SafeProcess.closeResource(prcs.getErrorStream());
     643                SafeProcess.closeResource(prcs.getOutputStream());
     644                SafeProcess.closeResource(prcs.getInputStream());
    505645                prcs.destroy();
    506646            }
    507647       
    508             closeResource(ebr);
    509             closeResource(stdinbr);
     648            SafeProcess.closeResource(ebr);
     649            SafeProcess.closeResource(stdinbr);
    510650        }
    511651
     
    514654        return success;
    515655    }
    516    
    517     public static void closeResource(Closeable resourceHandle) {
    518         try {
    519             if(resourceHandle != null) {
    520                 resourceHandle.close();
    521                 resourceHandle = null;
    522             }
    523         } catch(Exception e) {
    524             System.err.println("Exception closing resource: " + e.getMessage());
    525             e.printStackTrace();
    526         }
     656
     657
     658    // From interface SafeProcess.ExceptionHandler
     659    // Called when an exception happens during the running of our perl process. However,
     660    // exceptions when reading from our perl process' stderr and stdout streams are handled by
     661    // SynchronizedProcessLineByLineHandler.gotException() below.
     662    public void gotException(Exception e) {
     663
     664    // do what original runPerlCommand() code always did when an exception occurred
     665    // when running the perl process:
     666    e.printStackTrace();
     667    sendProcessStatus(new ConstructionEvent(this,GSStatus.ERROR,
     668                        "Exception occurred " + e.toString())); // atomic
    527669    }
     670   
     671    // This class deals with each incoming line from the perl process' stderr or stdout streams. One
     672    // instance of this class for each stream. However, since multiple instances of this LineByLineHandler
     673    // could be (and in fact, are) writing to the same file in their own threads, the writer object needs
     674    // to be made threadsafe.
     675    // This class also handles exceptions during the running of the perl process.
     676    // The runPerlCommand code originally would do a sendProcessStatus on each exception, so we ensure
     677    // we do that here too, to continue original behaviour.
     678    protected class SynchronizedProcessLineByLineHandler implements SafeProcess.LineByLineHandler
     679    {
     680    public static final int STDERR = 0;
     681    public static final int STDOUT = 1;
     682
     683    private final int source;
     684    private final BufferedWriter bwHandle; // needs to be final to synchronize on the object
     685       
     686
     687    public SynchronizedProcessLineByLineHandler(BufferedWriter bw, int src) {
     688        this.bwHandle = bw;
     689        this.source = src; // STDERR or STDOUT
     690    }
     691
     692    public void gotLine(String line) {
     693        //if(this.source == STDERR) {
     694        ///System.err.println("ERROR: " + line);
     695        //} else {
     696        ///System.err.println("OUT: " + line);
     697        //}
     698
     699        // BufferedWriter writes may not be atomic
     700        // http://stackoverflow.com/questions/9512433/is-writer-an-atomic-method
     701        // Choosing to put try-catch outside of sync block, since it's okay to give up lock on exception
     702        // http://stackoverflow.com/questions/14944551/it-is-better-to-have-a-synchronized-block-inside-a-try-block-or-a-try-block-insi
     703        // "All methods on Logger are multi-thread safe", see
     704        // http://stackoverflow.com/questions/14211629/java-util-logger-write-synchronization
     705
     706        try {
     707        synchronized(bwHandle) { // get a lock on the writer handle, then write
     708       
     709            bwHandle.write(line + "\n");
     710        }
     711        } catch(IOException ioe) {
     712        String msg = (source == STDERR) ? "stderr" : "stdout";
     713        msg = "Exception when writing out a line read from perl process' " + msg + " stream.";
     714        GS2PerlConstructor.logger.error(msg, ioe);
     715        }
     716       
     717        // this next method is thread safe since only synchronized methods are invoked.
     718        // and only immutable (final) vars are used.
     719        sendProcessStatus(new ConstructionEvent(GS2PerlConstructor.this, GSStatus.CONTINUING, line));
     720    }   
     721
     722    // This is called when we get an exception during the processing of a perl's
     723    // input-, err- or output stream
     724    public void gotException(Exception e) {
     725        String msg = (source == STDERR) ? "stderr" : "stdout";
     726        msg = "Got exception when processing the perl process' " + msg + " stream.";
     727
     728        // now do what the original runPerlCommand() code always did:
     729        e.printStackTrace();
     730        sendProcessStatus(new ConstructionEvent(this, GSStatus.ERROR, "Exception occurred " + e.toString())); // atomic
     731    }
     732
     733    } // end inner class SynchronizedProcessLineByLineHandler
     734
    528735}
Note: See TracChangeset for help on using the changeset viewer.