Update of /cvsroot/e-p-i-c/org.epic.debug/src/org/epic/debug/cgi In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv10829/src/org/epic/debug/cgi Modified Files: CGIConfig.java EpicCgiHandler.java Added Files: StringReaderThread.java ProcessExecutor.java ProcessOutput.java Log Message: Re-enabled retrieval of default environment in EpicCgiHandler. It turns out that strange things happen under Windows when the environment variable SystemRoot is not defined (getprotobynumber stops working). This might explain some of the problems reported by Windows users about debugger failing to connect (the 0.3.12 version of the environment query was buggy). --- NEW FILE: ProcessOutput.java --- // NOTE: this is org.epic.core.util.ProcessOutput, duplicated here to // work around classpath issues (EpicCGIHandler has no access to // org.epic.perleditor code). TODO: think up a better solution. package org.epic.debug.cgi; import java.io.BufferedReader; import java.io.StringReader; import java.util.ArrayList; import java.util.List; /** * The collected output of an executed process. * * @author jploski */ class ProcessOutput { /** * Output provided through stdout. */ public final String stdout; /** * Output provided through stderr. */ public final String stderr; public ProcessOutput(String stdout, String stderr) { this.stdout = stdout; this.stderr = stderr; } public List getStderrLines() { return getLines(stderr); } public List getStdoutLines() { return getLines(stdout); } private List getLines(String str) { BufferedReader r = new BufferedReader(new StringReader(str)); List lines = new ArrayList(); String l; try { while ((l = r.readLine()) != null) lines.add(l); } catch (java.io.IOException e) { /* can't occur */ } return lines; } } Index: EpicCgiHandler.java =================================================================== RCS file: /cvsroot/e-p-i-c/org.epic.debug/src/org/epic/debug/cgi/EpicCgiHandler.java,v retrieving revision 1.11 retrieving revision 1.12 diff -u -d -r1.11 -r1.12 --- EpicCgiHandler.java 7 Mar 2006 19:40:21 -0000 1.11 +++ EpicCgiHandler.java 8 Mar 2006 22:01:16 -0000 1.12 @@ -72,7 +72,6 @@ private static final String PREFIX = "prefix"; // all cgi scripts must start with this private static final String CUSTOM = "custom"; // add custom query variables - private static final String EXECUTABLE = "executable"; private static final String ENV = "ENV"; private static String software = "Mini Java CgiHandler 0.2"; @@ -87,6 +86,8 @@ private PrintWriter mDiag; // diagnostic info to CGI proxy private OutputStream mOut; // forwards CGI stdout to CGI proxy private OutputStream mError; // forwards CGI stderr to CGI proxy + private List defaultEnv; + private Exception defaultEnvError; /** * construct table of CGI environment variables that need special handling @@ -110,6 +111,10 @@ public boolean init(Server server, String prefix) { config = new CGIConfig(server, prefix); + + try { defaultEnv = getDefaultEnvironment(); } + catch (Exception e) { defaultEnvError = e; } + return connectToCGIProxy(); } @@ -194,6 +199,13 @@ mError = errorSocket.getOutputStream(); mOut = outSocket.getOutputStream(); mDiag = new PrintWriter(diagSocket.getOutputStream(), true); + + if (defaultEnvError != null) + { + mDiag.println("Failed to retrieve global environment variables:"); + defaultEnvError.printStackTrace(mDiag); + mDiag.println("CGI scripts might not be executed properly."); + } } catch (UnknownHostException e) { @@ -217,7 +229,7 @@ { //Get Perl executable and generate comand array ArrayList commandList = new ArrayList(); - commandList.add(config.getRequestProperty(request, EXECUTABLE, "perl")); + commandList.add(config.getPerlExecutable()); commandList.addAll(config.getRunInclude()); if (config.getDebugMode()) @@ -256,7 +268,8 @@ String url, int pathInfoStartI) { - List env = new ArrayList(); + List env = new ArrayList(); + env.addAll(defaultEnv); /* * Build the environment array. First, get all the http headers most @@ -544,4 +557,56 @@ } return null; } + + private List getDefaultEnvironment() + throws InterruptedException, IOException + { + // The environment passed to Runtime.exec for scripts overrides + // the global environment (at least in Windows XP). However, + // if the environment variable SystemRoot is not set in + // Windows XP, the system call getprotobyname (needed by + // the Perl debugger) will fail miserably. Therefore, + // we save all default environment variables here to pass + // them down to the Perl interpreter each time a script + // is executed. + + ProcessExecutor executor = new ProcessExecutor(); + try + { + // Use a random marker (current time) to increase + // the likelihood of properly parsing environment + // variables with multi-line values + + String marker = System.currentTimeMillis() + " "; + List varDefs = new ArrayList(); + List lines = executor.execute( + new String[] { config.getPerlExecutable() }, + "foreach $k(keys(%ENV)) { print \"" + + marker + "$k=$ENV{$k}\n\"; }", + new File(".") + ).getStdoutLines(); + + StringBuffer buf = new StringBuffer(); + for (Iterator i = lines.iterator(); i.hasNext();) + { + String line = (String) i.next(); + + if (!line.startsWith(marker)) // continuation + { + buf.append(System.getProperty("line.separator")); + buf.append(line); + } + else + { + if (buf.length() > 0) varDefs.add(buf.toString()); + buf.setLength(0); + buf.append(line.substring(marker.length())); + } + } + if (buf.length() > 0) varDefs.add(buf.toString()); + + return varDefs; + } + finally { executor.dispose(); } + } } \ No newline at end of file Index: CGIConfig.java =================================================================== RCS file: /cvsroot/e-p-i-c/org.epic.debug/src/org/epic/debug/cgi/CGIConfig.java,v retrieving revision 1.1 retrieving revision 1.2 diff -u -d -r1.1 -r1.2 --- CGIConfig.java 7 Mar 2006 19:39:50 -0000 1.1 +++ CGIConfig.java 8 Mar 2006 22:01:16 -0000 1.2 @@ -23,6 +23,7 @@ private final int portOut; private final int portErr; private final String protocol; + private final String perlExecutable; private final int serverPort; /** @@ -50,6 +51,8 @@ debugInc = getProperty("DebugInclude"); runInc = getListProperty("RunInclude"); + + perlExecutable = getProperty("executable"); } /** @@ -107,6 +110,15 @@ } /** + * @return path to the Perl interpreter executable that should + * be used for running CGI scripts + */ + public String getPerlExecutable() + { + return perlExecutable; + } + + /** * @return prefix used by all EPIC-specific configuration properties */ public String getPropsPrefix() --- NEW FILE: ProcessExecutor.java --- // NOTE: this is org.epic.core.util.ProcessExecutor, duplicated here to // work around classpath issues (EpicCGIHandler has no access to // org.epic.perleditor code). TODO: think up a better solution. package org.epic.debug.cgi; import java.io.*; import java.util.List; /** * Responsible for execution of external, non-interactive processes which * expect input in form of command-line parameters and stdin and provide * output through stdout and/or stderr. * <p> * The Perl interpreter is one such process (when run non-interactively). * A specialized client class is available for this case and should be used * instead of the generic ProcessExecutor. * </p> * * @see org.epic.core.util.PerlExecutor2 * @author jploski */ class ProcessExecutor { private boolean disposed; private boolean ignoreBrokenPipe; private final String charsetName; private final StringReaderThread stdout; private final StringReaderThread stderr; /** * Creates a ProcessExecutor which will use the platform's default charset. */ public ProcessExecutor() { this(null); } /** * Creates a ProcessExecutor which will use the given charset. * * @param charsetName * The name of a supported * {@link java.nio.charset.Charset </code>charset<code>} */ public ProcessExecutor(String charsetName) { this.charsetName = null; this.stdout = new StringReaderThread(":ProcessExecutor:stdout"); this.stderr = new StringReaderThread(":ProcessExecutor:stderr"); } /** * Releases resources held by this ProcessExecutor. * Any active calls to {@link #execute} will throw an InterruptedException. * This ProcessExecutor must no longer be used after dispose. */ public void dispose() { disposed = true; try { stdout.dispose(); } catch (InterruptedException e) { /* should never happen */ } try { stderr.dispose(); } catch (InterruptedException e) { /* should never happen */ } } /** * Same as {@link #execute(String[], String, File)}, except the command-line * is provided as a List of Strings rather than an array. */ public ProcessOutput execute(List commandLine, String input, File workingDir) throws InterruptedException, IOException { return execute( (String[]) commandLine.toArray(new String[commandLine.size()]), input, workingDir); } /** * Executes a process specified by the (platform-specific) command line, * collects output and blocks until the process terminates. The process * is executed in the given working directory, with the provided input, * which is converted to bytes using this ProcessExecutor's charset. * * @param commandLine path to the process and command line parameters * @param input input to be passed to the process via stdin * @param workingDir working directory in which to execute the process * @return output provided by the process through stdour or stderr, * depending on this ProcessExecutor's configuration * @exception java.lang.InterruptedException * if {@link #dispose} was called during this operation * @execption java.io.IOException * if the process could not be started or communication problems * were encountered */ public ProcessOutput execute(String[] commandLine, String input, File workingDir) throws InterruptedException, IOException { if (disposed) throw new IllegalStateException("ProcessExecutor disposed"); Process proc = null; try { proc = Runtime.getRuntime().exec(commandLine, null, workingDir); /* * Due to Java Bug #4763384 sleep for a very small amount of time * immediately after starting the subprocess */ try { Thread.sleep(1); } catch (InterruptedException e) { // believe it or not, InterruptedException sometimes occurs here // we can't help it ... ignore } InputStream procStderr = proc.getErrorStream(); InputStream procStdout = proc.getInputStream(); OutputStream procStdin = proc.getOutputStream(); Reader stderrReader; Reader stdoutReader; Writer inputWriter; if (charsetName != null) { stderrReader = new InputStreamReader(procStderr, charsetName); stdoutReader = new InputStreamReader(procStdout, charsetName); inputWriter = new OutputStreamWriter(procStdin, charsetName); } else { stderrReader = new InputStreamReader(procStderr); stdoutReader = new InputStreamReader(procStdout); inputWriter = new OutputStreamWriter(procStdin); } stderr.read(stderrReader); stdout.read(stdoutReader); if (input.length() > 0) { // We split delivery of the process input into two steps // to support our client PerlValidator: // The first character is written to check that the output stream // is ready and not throwing exceptions... inputWriter.write(input.charAt(0)); inputWriter.flush(); // The remaining write operation will often result in // a "broken pipe" IOException because Perl does not wait // until WE close the stream (and Java unfortunately treats // this condition as an exception). To make things worse, there is // no way to detect that the thrown exception is indeed a "broken // pipe": there is no error code (not even platform-specific one), // and the error message carried by the exception is localized. try { inputWriter.write(input.substring(1)); inputWriter.write(0x1a); //this should avoid problem with Win98 inputWriter.flush(); } catch (IOException e) { /* let's hope it's just a broken pipe */ brokenPipe(e); // call it to support testing for this condition } } inputWriter.close(); ProcessOutput ret = new ProcessOutput( stdout.getResult(), stderr.getResult()); stderrReader.close(); stdoutReader.close(); return ret; } catch (InterruptedException e) { // it must have been dispose() if (proc != null) proc.destroy(); throw e; } catch (IOException e) { if (proc != null) proc.destroy(); throw e; } } /** * This method is for the benefit of PerlValidator and should not concern * other clients. * <p> * Configures this ProcessExecutor to ignore suspected broken pipe * exceptions while delivering input to the executed process. * It is intended as a workaround for a situation in which the Perl * interpreter (invoked with a -c switch) closes its output stream * before it has received the entire input; this particular behavior * causes an exception on the Java side, but can be safely ignored. */ public void ignoreBrokenPipe() { this.ignoreBrokenPipe = true; } /** * Invoked when a suspected broken pipe exception was thrown * while delivering input to the executed process. This method * is here to enable testing for this condition. */ protected void brokenPipe(IOException e) throws IOException { if (!ignoreBrokenPipe) throw e; // just rethrow by default } } --- NEW FILE: StringReaderThread.java --- // NOTE: this is org.epic.core.util.StringReaderThread, duplicated here to // work around classpath issues (EpicCGIHandler has no access to // org.epic.perleditor code). TODO: think up a better solution. package org.epic.debug.cgi; import java.io.IOException; import java.io.Reader; /** * Asynchronously reads String content from a specified Reader. * This is mostly useful for interprocess communication, when output * from a subprocess must be consumed while delivering input data to it * in order to avoid blocking due to full IO buffers. */ class StringReaderThread extends Thread { private final Object lock = new Object(); private Reader reader; private String result; private IOException exception; private boolean disposed; public StringReaderThread(String name) { super("EPIC:StringReader" + name); this.start(); } public StringReaderThread() { this(""); } /** * Terminates this StringReaderThread. * If any invocations of {@link #getResult} are in progress, * they will throw an InterruptedException. * * @exception java.lang.InterruptedException * if the <code>dispose</code> operation is interrupted * while waiting for thread's termination */ public void dispose() throws InterruptedException { synchronized (lock) { disposed = true; lock.notifyAll(); } join(); } /** * Starts reading from the specified source. * Consecutive invocations of this method are disallowed. * The next method invocation after <code>read</code> must * be either {@link #getResult} or {@link #dispose}. */ public void read(Reader r) { synchronized (lock) { if (disposed) throw new IllegalStateException("thread disposed"); if (reader != null) throw new IllegalStateException( "this thread is already reading; call getResult or dispose next"); reader = r; result = null; exception = null; lock.notifyAll(); } } /** * Blocks until the reading is finished or aborted. * After this method returns, another {@link #read} may be started * (possibly from a different source). * * @return the content which has been read, if reading finished successfully * @exception java.lang.InterruptedException * if this StringReaderThread has been disposed * @exception java.io.IOException * if an exception occured during the reading */ public String getResult() throws IOException, InterruptedException { synchronized (lock) { while (reader != null && !disposed) { try { lock.wait(); } catch (InterruptedException e) { /* should not happen, we don't care if it does */ } } if (disposed) throw new InterruptedException( "StringReaderThread disposed during getResult"); if (exception != null) throw exception; return result; } } public void run() { while (!disposed) { // wait for read() to be invoked Reader r; synchronized (lock) { while (reader == null && !disposed) { try { lock.wait(); } catch (InterruptedException e) { /* should not happen, we don't care if it does */ } } if (disposed) break; r = reader; } // process read() request StringBuffer sb = new StringBuffer(); IOException e = null; char[] b = new char[1024]; try { int bread; while ((bread = r.read(b)) >= 0) sb.append(b, 0, bread); } catch (IOException e2) { sb.setLength(0); e = e2; } synchronized (lock) { reader = null; result = sb.toString(); exception = e; lock.notifyAll(); } } } } |