/* * This file is part of the OpenJML project. * Author: David R. Cok */ package org.jmlspecs.openjml.utils; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import org.jmlspecs.annotation.NonNull; import org.jmlspecs.annotation.Nullable; import org.jmlspecs.openjml.Strings; import org.jmlspecs.openjml.proverinterface.ProverException; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Log; import com.sun.tools.javac.util.Log.WriterKind; public class ExternalProcess implements IExternalProcess { /** This class reads an input Reader into a StringBuilder on a separate * thread. If prompt is null, the thread will read to end of file; if prompt * is non-empty, the thread will read until that string is at the end of the * text read; if prompt is an empty string, text is read until halt() is * called or end-of-file is reached. Threads are used to read the * regular and error output because otherwise the process can deadlock, * with reading or writing methods blocked because buffers are full but * not being read; also, both the regular and error output need to be read * simultaneously. */ static class ThreadedReader extends Thread { /** The input to be read. */ protected Reader is; /** The StringBuilder that accumulates the text that was read. */ protected StringBuilder output; /** The string that will terminate reading the output, * or null to read to end-of-file */ protected @Nullable String prompt; /** Creates a class instance (but does not start it) for reading * the given input into the specified StringBuilder. */ public ThreadedReader(@NonNull Reader is, @Nullable String prompt, @NonNull StringBuilder output) { this.is = is; this.prompt = prompt; this.output = output; } /** A buffer used simply to receive characters read, trying to do as * little copying and conversion as possible, within run(). */ // There is no magic about the size of this buffer - enough for a gulp // of reading, but not huge. private char[] cbuf = new char[4096]; /** The method that is run in the new thread, when start is called; * don't call it directly. */ public void run() { try { int numRead = 0; while (!stop && numRead != -1) { if (!is.ready()) Thread.sleep(1); while ((prompt == null || is.ready()) && (numRead = is.read(cbuf)) != -1) { output.append(cbuf,0,numRead); } if (prompt != null && !prompt.isEmpty() && output.length() >= prompt.length()) { if (output.indexOf(prompt,output.length()-prompt.length()) != -1) return; } } } catch (Exception x) { output.append("Exception in thread: "); output.append(x.getMessage()); output.append(System.getProperty("line.separator")); throw new RuntimeException("Exception in executable process reader",x); // FIXME - what error reporting mechanism should be used here and elsewhere (ProverException) } stop = false; } /** Flag that will halt the read loop */ private boolean stop = false; /** Call this to cause the thread to cleanly exit without yet having * seen an end-of-file. */ // FIXME - do we have concurrency/memory-model issues if this is written from a different thread? public void halt() { stop = true; } } /** If non-zero then log communication: * <UL> * <LI> =0 : no communication written * <LI> =1 : log errors (default) * <LI> =2 : log errors and strings sent * <LI> =3 " log errors, strings sent, and output heard * </UL> * This behavior can be overridden by showCommunication(). */ public int showCommunication = 1; /** The OpenJML log, for notification and warning and error messages */ protected Log log; /** The executable and its options. */ protected String[] app; /** The string that terminates reading the output. */ protected @Nullable String prompt; /** Creates an instance of the class, but does not start execution; * The prompt string may be null (non-interactive) but * may not contain any CR/NL characters. */ public ExternalProcess(Context context, String prompt, String... app) { this.log = Log.instance(context); this.prompt = prompt; this.app = app; } /** The process being managed */ protected Process process = null; /** The stream connection to send information to the process. */ //@ invariant process != null ==> toProver != null; protected Writer toProver; /** The stream connection to read information from the process. */ //@ invariant process != null ==> fromProver != null; protected Reader fromProver; /** The error stream connection to read information from the process. */ //@ invariant process != null ==> errors != null; protected Reader errors; /** The StringBuilder that collects output text */ public StringBuilder outputString = new StringBuilder(); /** The StringBuilder that collects error text */ public StringBuilder errorString = new StringBuilder(); /** The thread used for the error output. */ private ThreadedReader errorReader; /** The thread used for the usual output. */ private ThreadedReader outputReader; /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#app() */ @Override public String[] app() { return app; } /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#prompt() */ @Override public @Nullable String prompt() { return prompt; } /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#start() */ @Override public void start() throws ProverException { outputString.setLength(0); errorString.setLength(0); if (app() == null) { throw new ProverException("No path to the executable found"); } try { process = new ProcessBuilder(app()).start(); } catch (IOException e) { process = null; throw new ProverException("Failed to launch process: " + app()[0] + Strings.space + e); } // TODO: assess performance of using buffered readers/writers toProver = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); fromProver = new BufferedReader(new InputStreamReader(process.getInputStream())); errors = new InputStreamReader(process.getErrorStream()); errorReader = new ThreadedReader(errors, prompt==null? null : "", errorString); outputReader = new ThreadedReader(fromProver, prompt, outputString); errorReader.start(); outputReader.start(); } // @Override // public String output() { // return outputString.toString(); // } // // @Override // public String errorOutput() { // return errorString.toString(); // } /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#kill() */ @Override public void kill() { process.destroy(); process = null; } /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#send(java.lang.String) */ @Override public void send(String s) throws ProverException { outputString.setLength(0); errorString.setLength(0); if (showCommunication >= 2) { log.getWriter(WriterKind.NOTICE).println("SENDING ["+s.length()+ "]" + s); log.getWriter(WriterKind.NOTICE).flush(); } try { // The number 2000 here is arbitrary - it is just a significant // amount to send at once, breaking up long inputs so that the // prover process has a chance to catch up. Not sure it is or // should be needed, but it seemed to help avoid deadlocks at one // time. final int gulp = 2000; if (s.length() > gulp) { int i = 0; for (; i< s.length()-gulp; i+= gulp) { toProver.append(s.substring(i,i+gulp)); try { Thread.sleep(1); } catch (Exception e) {} } toProver.append(s.substring(i)); } else { toProver.append(s); } toProver.flush(); } catch (IOException e) { throw new ProverException("Failed to write to prover: (" + s.length() + " chars) " + e); } } /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#readToCompletion() */ @Override public int readToCompletion() throws ProverException { if (prompt() != null) log.error("jml.internal","ExternalProcess.runToCompletion shouldnot be called when prompt has been set"); int exitVal = -1; try { exitVal = process.waitFor(); errorReader.join(); // Handle condition where the outputReader.join(); // process ends before the threads finish } catch (InterruptedException e) { throw new ProverException("readToCompletion was interrupted",e); } return exitVal; } /* (non-Javadoc) * @see org.jmlspecs.openjml.utils.IExternalProcess#eatPrompt() */ @Override public String eatPrompt() throws ProverException { try { outputReader.join(); errorReader.halt(); errorReader.join(); String err = errorString.toString(); String out = outputString.toString(); showCommunication(out,err); errorReader = new ThreadedReader(errors, prompt==null? null : "", errorString); outputReader = new ThreadedReader(fromProver, prompt, outputString); errorReader.start(); outputReader.start(); return out; } catch (InterruptedException e) { return null; } } /** A method meant to be overridden by other classes to control what * communication information is printed out. * @throws ProverException */ protected void showCommunication(String out, String err) throws ProverException { if (showCommunication >= 3) { log.getWriter(WriterKind.NOTICE).println("HEARD: " + out); } if (showCommunication >= 1) { if (!err.isEmpty() && !err.startsWith("\nWARNING") && !err.startsWith("CVC3 (version") && !err.startsWith("Yices (version") && !err.contains("searching") && !err.trim().isEmpty()) { if (showCommunication >= 1) log.getWriter(WriterKind.NOTICE).println("HEARD ERROR: " + err); //throw new ProverException("Prover error message: " + errorString); } else { if (showCommunication >= 3) log.getWriter(WriterKind.NOTICE).println("HEARD ERROR: " + err); } } } }