From: Jan P. <jp...@us...> - 2006-12-02 16:04:52
|
Update of /cvsroot/e-p-i-c/org.epic.debug/src/org/epic/debug/cgi/server In directory sc8-pr-cvs5.sourceforge.net:/tmp/cvs-serv11289/src/org/epic/debug/cgi/server Added Files: ProcessExecutor.java CGIConfig.java EpicCgiHandler.java StreamForwarder.java ProcessOutput.java StringReaderThread.java CustomBrowser.java Log Message: - Global refactoring and clean-up, focused on PerlDB and launch configuration delegates. - New class DebuggerInterface for low-level communication with "perl -d", separate from the former gigantic PerlDB. - Created new packages to better reflect different types of launch configurations and related code dependencies. - Made "Perl Remote" launch configurations actually work (at least on simple examples). - Hopefully fixed bug [ 1602375 ] Debugger hangs under Windows. --- 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.server; 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: CGIConfig.java --- package org.epic.debug.cgi.server; import java.util.*; import sunlabs.brazil.server.Request; import sunlabs.brazil.server.Server; /** * Provides access to configuration settings of an EpicCgiHandler. * This configuration is passed down from EPIC using the Brazil * properties file. */ class CGIConfig { private final Server server; private final String propsPrefix; private final boolean debugMode; private String debugInc; private List runInc; private final String hostname; private final int portIn; private final int portOut; private final int portErr; private final String protocol; private final String perlExecutable; private final String perlParams; private final int serverPort; /** * Creates the CGIConfig instance. * * @param server Brazil server from which configuration * should be retrieved * @param propsPrefix prefix used by all EPIC-specific configuration * properties */ public CGIConfig(Server server, String propsPrefix) { this.server = server; this.propsPrefix = propsPrefix; serverPort = server.listen.getLocalPort(); hostname = server.hostName; protocol = server.protocol; portIn = getIntProperty("InPort"); portOut = getIntProperty("OutPort"); portErr = getIntProperty("ErrorPort"); debugMode = getProperty("Debug").equalsIgnoreCase("true"); debugInc = getProperty("DebugInclude"); runInc = getListProperty("RunInclude"); perlParams = getProperty("PerlParams"); perlExecutable = getProperty("executable"); } /** * @return a string with include (-I) command-line options to * be used when executing scripts in debug mode */ public String getDebugInclude() { return debugInc; } /** * @return true if scripts should execute in debug mode; * false otherwise */ public boolean getDebugMode() { return debugMode; } /** * @return a TCP port on localhost to which stderr of executed * CGI scripts should be forwarded */ public int getErrorPort() { return portErr; } /** * @return DNS name or dot-quad IP address of the web server */ public String getHostname() { return hostname; } /** * @return a TCP port on localhost to which diagnostic messages * about executed scripts should be forwarded */ public int getDiagPort() { return portIn; } /** * @return a TCP port on localhost to which stderr of executed * CGI scripts should be forwarded (in addition to delivering * it to the requesting web client) */ public int getOutPort() { return portOut; } /** * @return path to the Perl interpreter executable that should * be used for running CGI scripts */ public String getPerlExecutable() { return perlExecutable; } /** * @return command-line parameters that should be passed to * the Perl interpreter when running CGI scripts */ public String getPerlParams() { return perlParams; } /** * @return prefix used by all EPIC-specific configuration properties */ public String getPropsPrefix() { return propsPrefix; } /** * @param an additional prefix used by requested property names; * this prefix should <b>not</b> include the one returned * by {@link #getPropsPrefix} (which is always assumed) * @return a mapping of property names <b>with the prefix stripped</b> * to their respective values */ public Map getProperties(String prefix) { Map ret = new HashMap(); int len = (propsPrefix + prefix).length(); for (Iterator i = server.props.keySet().iterator(); i.hasNext();) { String key = (String) i.next(); if (key.startsWith(propsPrefix + prefix)) ret.put(key.substring(len), server.props.getProperty(key)); } return ret; } /** * @param Request a request received from a web client * @param name (unprefixed) name of a configuration property * @param defaultValue default value to be returned if the property * is not defined * @return value of the property extracted from the request */ public String getRequestProperty( Request request, String name, String defaultValue) { return request.props.getProperty(propsPrefix + name, defaultValue); } /** * @return the protocol used by the web server ("http" or "https") */ public String getProtocol() { return protocol; } /** * @return a list with command-line parameters representing * the include path for the Perl interpreter */ public List getRunInclude() { // TODO why do we return a List here and a String in getDebugInclude? // more refactoring needed return Collections.unmodifiableList(runInc); } /** * @return the TCP port on which the web server awaits client requests */ public int getServerPort() { return serverPort; } private int getIntProperty(String name) { return Integer.parseInt(getProperty(name)); } private List getListProperty(String name) { List values = new ArrayList(); for (int i = 0;; i++) { String value = getProperty(name + "[" + i + "]"); if (value == null) break; else values.add(value); } return values; } private String getProperty(String name) { return server.props.getProperty(propsPrefix + name); } } --- NEW FILE: StreamForwarder.java --- package org.epic.debug.cgi.server; import java.io.*; /** * A thread which forwards all bytes read from a source InputStream * to a destination OutputStream. After the thread terminates, * {@link getError} can be called to check if the the InputStream * was closed properly or an exception has occured. */ class StreamForwarder extends Thread { private final InputStream src; private final OutputStream dst; private IOException error; public StreamForwarder(String name, InputStream src, OutputStream dst) { super(name); this.src = src; this.dst = dst; } public IOException getError() { return error; } public void run() { byte[] buf = new byte[1024]; try { int bread; while ((bread = src.read(buf, 0, buf.length)) > 0) { dst.write(buf, 0, bread); dst.flush(); } } catch (IOException e) { error = e; } } } --- NEW FILE: EpicCgiHandler.java --- /* * CgiHandler.java * * Brazil project web application toolkit, * export version: 2.0 * Copyright (c) 1998-2002 Sun Microsystems, Inc. * * Sun Public License Notice * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is included as the file "license.terms", * and also available at http://www.sun.com/ * * The Original Code is from: * Brazil project web application toolkit release 2.0. * The Initial Developer of the Original Code is: suhler. * Portions created by suhler are Copyright (C) Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): cstevens, drach, suhler. * * Version: 1.22 * Created by suhler on 98/09/14 * Last modified by suhler on 02/05/02 11:15:33 */ package org.epic.debug.cgi.server; import java.io.*; import java.net.Socket; import java.net.UnknownHostException; import java.util.*; import org.epic.debug.util.ExecutionArguments; import sunlabs.brazil.server.*; /** * Handler for implementing cgi/1.1 interface. This implementation allows either * suffix matching (e.g. .cgi) to identify cgi scripts, or prefix matching (e.g. * /cgi-bin). Defaults to "/". All output from the cgi script is buffered (e.g. * chunked encoding is not supported). <br> * NOTE: in versions of Java prior to release 1.3, the ability to set a working * directory when running an external process is missing. This handler * automatically checks for this ability and sets the proper working directory, * but only if the underlying VM supports it. * <p> * The following request properties are used: * <dl class=props> * <dt>root * <dd>The document root for cgi files * <dt>suffix * <dd>The suffix for cgi files (defaults to .cgi) * <dt>prefix * <dd>The prefix for all cgi files (e.g. /cgi-bin) * <dt>custom * <dd>set to "true" to enable custom environment variables. If set, all server * properties starting with this handler's prefix are placed into the * environment with the name: <code>CONFIG_<i>name</i></code>, where * <i>name </i> is the property key, in upper case, with the prefix removed. * This allows cgi scripts to be customized in the server's configuration file. * </dl> * * @author Stephen Uhler * @version 1.22, 02/05/02 */ public class EpicCgiHandler implements Handler { private static final Object LOCK = new Object(); private static final String ROOT = "root"; // property for document root private static final String SUFFIX = "suffix"; // property for suffix string 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 ENV = "ENV"; private static String software = "Mini Java CgiHandler 0.2"; private static Hashtable envMap; // environ maps private CGIConfig config; private Socket diagSocket; private Socket outSocket; private Socket errorSocket; 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 Exception defaultEnvError; /** * construct table of CGI environment variables that need special handling */ static { envMap = new Hashtable(2); envMap.put("content-length", "CONTENT_LENGTH"); envMap.put("content-type", "CONTENT_TYPE"); } public EpicCgiHandler() { } /** * One time initialization. The handler configuration properties are * extracted and set in {@link #respond(Request)}to allow upstream handlers * to modify the parameters. */ public boolean init(Server server, String prefix) { config = new CGIConfig(server, prefix); return connectToCGIProxy(); } /** * Dispatch and handle the CGI request. Gets called on ALL requests. Set up * the environment, exec the process, and deal appropriately with the input * and output. * * In this implementation, all cgi script files must end with a standard * suffix, although the suffix may omitted from the url. The url * /main/do/me/too?a=b will look, starting in DocRoot, for main.cgi, * main/do.cgi, etc until a matching file is found. * <p> * Input parameters examined in the request properties: * <dl> * <dt>Suffix * <dd>The suffix for all cgi scripts (defaults to .cgi) * <dt>DocRoot * <dd>The document root, for locating the script. * </dl> */ public boolean respond(Request request) { // The current implementation of EPIC debugger cannot reliably // process concurrent debug connections. Therefore, we serialise // processing of CGI requests at the web server level (LOCK). synchronized (LOCK) { return respondImpl(request); } } private boolean respondImpl(Request request) { String url = request.props.getProperty("url.orig", request.url); String prefix = config.getRequestProperty(request, PREFIX, "/"); if (!url.startsWith(prefix)) return false; if (url.endsWith("favicon.ico")) return false; String suffixes = config.getRequestProperty(request, SUFFIX, ".cgi"); String root = config.getRequestProperty( request, ROOT, request.props.getProperty(ROOT, ".")); request.log( Server.LOG_DIAGNOSTIC, "suffix=" + suffixes + " root=" + root + " url: " + url); File cgiFile; int pathInfoStartI; { Object[] ret = findCGIFile(request, url, suffixes, root); if (ret == null) return false; cgiFile = (File) ret[0]; pathInfoStartI = ((Integer) ret[1]).intValue(); } String[] command = createCommandLine(request, cgiFile); String[] env = createEnvironment( request, cgiFile, root, url, pathInfoStartI); execCGI(request, cgiFile, command, env); return true; } /** * Opens communication channels to the EPIC CGI proxy running * inside of the Eclipse JVM. The script output and diagnostic * information are forwarded to this proxy. */ private boolean connectToCGIProxy() { try { diagSocket = new Socket("localhost", config.getDiagPort()); outSocket = new Socket("localhost", config.getOutPort()); errorSocket = new Socket("localhost", config.getErrorPort()); 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) { // TODO: can this ever happen? will anyone see the error message? e.printStackTrace(); return false; } catch (IOException e) { // TODO: can this ever happen? will anyone see the error message? e.printStackTrace(); return false; } return true; } /** * @return the command line used to execute the CGI script */ private String[] createCommandLine(Request request, File cgiFile) { //Get Perl executable and generate comand array ArrayList commandList = new ArrayList(); commandList.add(config.getPerlExecutable()); commandList.addAll(config.getRunInclude()); if (config.getDebugMode()) { commandList.add(config.getDebugInclude()); commandList.add("-d"); // Add debug switch } String perlParams = config.getPerlParams(); if (perlParams != null && perlParams.length() > 0) { ExecutionArguments exArgs = new ExecutionArguments(perlParams); commandList.addAll(exArgs.getProgramArgumentsL()); } commandList.add(cgiFile.getAbsolutePath()); // Look at the query and check for an = // If no '=', then use '+' as an argument delimiter if (request.query.indexOf("=") == -1) commandList.add(request.query); String[] command = (String[]) commandList.toArray(new String[commandList.size()]); /* for (int i = 0; i < command.length; i++) request.log( Server.LOG_DIAGNOSTIC, "command[" + i + "]= " + command[i]); */ return command; } /** * @return the environment passed to the executed CGI script */ private String[] createEnvironment( Request request, File cgiFile, String root, String url, int pathInfoStartI) { List env = new ArrayList(); /* * Build the environment array. First, get all the http headers most * are transferred directly to the environment, some are handled * specially. Multiple headers with the same name are not handled * properly. */ Enumeration keys = request.headers.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); String special = (String) envMap.get(key.toLowerCase()); if (special != null) env.add(special + "=" + request.headers.get(key)); else env.add( "HTTP_" + key.toUpperCase().replace('-', '_') + "=" + request.headers.get(key)); } // Add in the rest of them env.add("GATEWAY_INTERFACE=CGI/1.1"); env.add("SERVER_SOFTWARE=" + software); env.add("SERVER_NAME=" + config.getHostname()); env.add("PATH_INFO=" + url.substring(pathInfoStartI)); String suffix = cgiFile.getName(); if (suffix.lastIndexOf('.') != -1) suffix = suffix.substring(suffix.lastIndexOf('.')); else suffix = ""; String pre = url.substring(0, pathInfoStartI); if (pre.endsWith(suffix)) { env.add("SCRIPT_NAME=" + pre); } else { env.add("SCRIPT_NAME=" + pre + suffix); } env.add("SERVER_PORT=" + config.getServerPort()); env.add("REMOTE_ADDR=" + request.getSocket().getInetAddress().getHostAddress()); env.add("PATH_TRANSLATED=" + root + url.substring(pathInfoStartI)); env.add("REQUEST_METHOD=" + request.method); env.add("SERVER_PROTOCOL=" + request.protocol); env.add("QUERY_STRING=" + request.query); if (config.getProtocol().equals("https")) env.add("HTTPS=on"); env.add("SERVER_URL=" + request.serverUrl()); // Append the "custom" environment variables (if requested) if (!config.getRequestProperty(request, CUSTOM, "").equals("")) { Map props = config.getProperties(""); for (Iterator i = props.keySet().iterator(); i.hasNext();) { String key = (String) i.next(); env.add("CONFIG_" + key + "=" + props.get(key)); } env.add("CONFIG_PREFIX=" + config.getPropsPrefix()); } // Append environment variables provided by EPIC // (configurable with the CGI Environment tab; if nothing // is configured, the environment of the workbench is used) Map userEnv = config.getProperties(ENV + "_"); for (Iterator i = userEnv.keySet().iterator(); i.hasNext();) { String key = (String) i.next(); env.add(key + "=" + userEnv.get(key)); } String[] environ = (String[]) env.toArray(new String[env.size()]); return environ; } /** * Executes the given CGI script file using the provided * command line and environment. Script stdout is returned * both to the browser and to the CGI proxy. Script stderr * is returned only to the CGI proxy. */ private void execCGI( Request request, File cgiFile, String[] command, String[] env) { mDiag.println("***********************************************************"); mDiag.println("Requested URI: " + request.props.getProperty("url.orig", request.url)); mDiag.println("---------------------CGI Command Line----------------------"); for (int i = 0; i < command.length; i++) mDiag.println(command[i]); mDiag.println("-------------------Environment Variables-------------------"); for (int i = 0; i < env.length; i++) mDiag.println(env[i]); Process cgi = null; StreamForwarder stderrFwd = null; try { cgi = Runtime.getRuntime().exec( command, env, new File(cgiFile.getParent())); DataInputStream in = new DataInputStream( new BufferedInputStream(cgi.getInputStream())); // If we have data, send it to the process if (request.postData != null) { OutputStream toCgi = cgi.getOutputStream(); toCgi.write(request.postData, 0, request.postData.length); toCgi.close(); mDiag.println("------------------------POST data--------------------------"); mDiag.println(new String(request.postData, "ISO-8859-1")); mDiag.flush(); } stderrFwd = new StreamForwarder( "EpicCgiHandler.readError", new BufferedInputStream(cgi.getErrorStream()), mError); stderrFwd.start(); mDiag.println("-----------------------Script Output-----------------------"); // Now get the output of the cgi script. Start by reading the // "mini header", then just copy the rest String head; String type = "text/html"; int status = 200; while (true) { head = in.readLine(); if (head == null || head.length() == 0) { mOut.write("\r\n".getBytes()); mOut.flush(); break; } mOut.write(head.getBytes("ISO-8859-1")); mOut.write("\r\n".getBytes()); mOut.flush(); int colonIndex = head.indexOf(':'); if (colonIndex < 0) { request.sendError(500, "Missing header from cgi output"); mError.write( "Error 500: Missing header from cgi output" .getBytes("ASCII")); return; } String lower = head.toLowerCase(); if (lower.startsWith("status:")) { try { status = Integer.parseInt( head.substring(colonIndex + 1).trim()); } catch (NumberFormatException e) { } } else if (lower.startsWith("content-type:")) { type = head.substring(colonIndex + 1).trim(); } else if (lower.startsWith("location:")) { status = 302; request.addHeader(head); } else { request.addHeader(head); } } /* * Now copy the rest of the data into a buffer, so we can count it. * we should be doing chunked encoding for 1.1 capable clients XXX */ ByteArrayOutputStream buff = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int bread; while ((bread = in.read(buf, 0, buf.length)) > 0) { buff.write(buf, 0, bread); mOut.write(buf, 0, bread); mOut.flush(); } request.sendHeaders(status, type, buff.size()); buff.writeTo(request.out); request.log(Server.LOG_DIAGNOSTIC, "CGI output " + buff.size() + " bytes."); cgi.waitFor(); } catch (Exception e) { if (cgi != null) cgi.destroy(); StringWriter trace = new StringWriter(); e.printStackTrace(new PrintWriter(trace, true)); request.sendError(500, "CGI failure", trace.toString()); try { mError.write( ("Error 500: " + "CGI failure: " + e.getMessage()).getBytes("ASCII") ); e.printStackTrace(new PrintStream(mError)); } catch (IOException _e) { /* not much we can do really */} } finally { try { if (stderrFwd != null) stderrFwd.join(); } catch (Exception e) { } } } /** * Resolves the CGI script file to be executed based on the * requested URI, configured CGI suffixes and the root directory. * * @return null if the resolution algorithm fails; * otherwise a 2-element array with: * File cgiFile (the resolved CGI script file) and * Integer pathInfoStartIndex (index in the uri at * which the CGI path ends and PATH_INFO to be passed * into the script begins) */ private Object[] findCGIFile( Request request, String uri, String suffixes, String root) { // Try to find the shortest prefix in url which can be mapped // to an existing CGI script. This is to correctly extract // PATH_INFO from URLs // like http://localhost/cgi-bin/foo.cgi/some/path/info // or even http://localhost/cgi-bin/foo/some/path/info // (PATH_INFO=/some/path/info) String suffix = null; StringTokenizer tok = new StringTokenizer(suffixes, ","); int start = 1; int end = 0; while (tok.hasMoreTokens()) { suffix = tok.nextToken(); request.log(Server.LOG_DIAGNOSTIC, "Checking for suffix: " + suffix); start = 1; end = 0; while (end < uri.length()) { end = uri.indexOf(File.separatorChar, start); if (end < 0) end = uri.length(); String s = uri.substring(1, end); if (!s.endsWith(suffix)) s += suffix; File cgiFile = new File(root, s); request.log(Server.LOG_DIAGNOSTIC, "looking for: " + cgiFile); if (cgiFile.isFile()) { request.log(Server.LOG_DIAGNOSTIC, "found: " + cgiFile); return new Object[] { cgiFile, new Integer(end) }; } start = end + 1; } } return null; } } --- 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.server; 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; } } --- NEW FILE: CustomBrowser.java --- /* * Created on 12.04.2004 * * To change the template for this generated file go to * Window>Preferences>Java>Code Generation>Code and Comments */ package org.epic.debug.cgi.server; import java.util.ArrayList; import java.util.StringTokenizer; import org.eclipse.help.browser.IBrowser; import org.eclipse.help.internal.HelpPlugin; import org.eclipse.help.internal.browser.StreamConsumer; import org.epic.debug.PerlDebugPlugin; /** * */ public class CustomBrowser implements IBrowser { public static final String CUSTOM_BROWSER_PATH_KEY = "custom_browser_path"; /** * @see org.eclipse.help.ui.browser.IBrowser#close() */ String mPath; Process pr; public CustomBrowser(String fPath) { mPath = fPath; } public void close() { pr.destroy(); } /** * @see org.eclipse.help.ui.browser.IBrowser#isCloseSupported() */ public boolean isCloseSupported() { return false; } /** * @see org.eclipse.help.ui.browser.IBrowser#displayURL(java.lang.String) */ public void displayURL(String url) throws Exception { String path = mPath; String[] command = prepareCommand(path, url); try { pr = Runtime.getRuntime().exec(command); Thread outConsumer = new StreamConsumer(pr.getInputStream()); outConsumer.setName("Custom browser adapter output reader"); outConsumer.start(); Thread errConsumer = new StreamConsumer(pr.getErrorStream()); errConsumer.setName("Custom browser adapter error reader"); errConsumer.start(); } catch (Exception e) { PerlDebugPlugin.getDefault().logError( "CustomBrowser.errorLaunching " + path, e); } } /** * @see org.eclipse.help.ui.browser.IBrowser#isSetLocationSupported() */ public boolean isSetLocationSupported() { return false; } /** * @see org.eclipse.help.ui.browser.IBrowser#isSetSizeSupported() */ public boolean isSetSizeSupported() { return false; } /** * @see org.eclipse.help.ui.browser.IBrowser#setLocation(int, int) */ public void setLocation(int x, int y) { } /** * @see org.eclipse.help.ui.browser.IBrowser#setSize(int, int) */ public void setSize(int width, int height) { } /** * Creates the final command to launch. * @param path * @param url * @return String[] */ private String[] prepareCommand(String path, String url) { ArrayList tokenList = new ArrayList(); //Divide along quotation marks StringTokenizer qTokenizer = new StringTokenizer(path.trim(), "\"", true); boolean withinQuotation = false; String quotedString = ""; while (qTokenizer.hasMoreTokens()) { String curToken = qTokenizer.nextToken(); if (curToken.equals("\"")) { if (withinQuotation) { tokenList.add("\"" + quotedString + "\""); } else { quotedString = ""; } withinQuotation = !withinQuotation; continue; } else if (withinQuotation) { quotedString = curToken; continue; } else { //divide unquoted strings along white space StringTokenizer parser = new StringTokenizer(curToken.trim()); while (parser.hasMoreTokens()) { tokenList.add(parser.nextToken()); } } } // substitute %1 by url boolean substituted = false; for (int i = 0; i < tokenList.size(); i++) { String token = (String) tokenList.get(i); if ("%1".equals(token)) { tokenList.set(i, url); substituted = true; } else if ("\"%1\"".equals(token)) { tokenList.set(i, "\"" + url + "\""); substituted = true; } } // add the url if not substituted already if (!substituted) tokenList.add(url); String[] command = new String[tokenList.size()]; tokenList.toArray(command); return command; } public static boolean isCustomBrowserID(String fID) { return fID.equals((HelpPlugin.PLUGIN_ID + ".base.custombrowser")); } } --- 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.server; 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(); } } } } |