/******************************************************************************* * Copyright (c) 2012 Pivotal Software, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.grails.ide.eclipse.longrunning.client; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeoutException; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.launching.IVMInstall; import org.eclipse.jdt.launching.JavaRuntime; import org.grails.ide.eclipse.commands.GrailsCommand; import org.grails.ide.eclipse.core.GrailsCoreActivator; import org.grails.ide.eclipse.core.launch.GrailsLaunchArgumentUtils; import org.grails.ide.eclipse.core.model.GrailsVersion; import org.grails.ide.eclipse.core.model.IGrailsInstall; import org.grails.ide.eclipse.longrunning.Console; import org.grails.ide.eclipse.longrunning.GrailsProcessManager; import org.grails.ide.eclipse.runtime.shared.longrunning.GrailsProcessConstants; import org.grails.ide.eclipse.runtime.shared.longrunning.ProtocolException; /** * A client that is able to send request to execute Grails commands to * an external grails process. (Note that the class GrailsProcessConstants itself which * implements the process, shouldn't be instantiated since it runs on an external JVM). * <p> * Normally you shouldn't instantiate GrailsClient instances directly. Instead use {@link GrailsProcessManager} to obtain client instances. * * @author Kris De Volder * @author Andy Clement * @since 2.6 */ public class GrailsClient { /** * Set this to a value other than null to add debugging stuff to the command line to * start the GrailsProcessConstants that this client connects to in debugging mode. To be able to * actually debug it, you will need to create and start a remote debugging launch configuration * in Eclipse. * <p> * Uncomment one of the tree choices below. * <p> * Option 1: debuggin disabled. * <p> * Option 2: Enable debug process in 'server' mode. This means that the debugged process itself * is a 'server' and the debugger (Remote Eclipse debugging launch conf) connects to it. * <p> * Option 3: Enable debug process in 'client' mode. This means that the debugger is a server, and * the debugged process connects to it as a client. * <p> * To use this you must create an Eclipse remote debugging launch configuration and change it * to a 'Standard socket listening' setting. The launched process will attempt to connect * to this remote debugging session when it starts. * <p> * Note that if the process is running in 'client' mode but a corresponding debug process isn't * started in Eclipse, then the GrailsProcess will fail to start. * <p> * See also http://www.grails.org/GrailsDevEnvironment for a bit more info on debugging * Grails. */ public static String DEBUG_PROCESS = null; //Disabled //public static final String DEBUG_PROCESS = "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"; //public static final String DEBUG_PROCESS = "-Xrunjdwp:transport=dt_socket,server=n,address=8000"; /** * When this flag is set we will echo anything sent from the external process to the client * or vice versa onto System.out. */ private static final boolean DEBUG_PROTOCOL = (""+Platform.getLocation()).contains("kdvolder") || (""+Platform.getLocation()).contains("bamboo") || (""+Platform.getLocation()).contains("hudson"); /** * When this flag is set to true, the client will produce some debugging output onto system * out. */ private static final boolean DEBUG_CLIENT = (""+Platform.getLocation()).contains("kdvolder") || (""+Platform.getLocation()).contains("bamboo") || (""+Platform.getLocation()).contains("hudson"); /** * Polling interval used to check for data coming from the process. * * If this time is longer the process will become less responsive * but if it is shorter it will be taking more CPU polling for input. */ public static final long POLLING_INTERVAL = 300; /** * Time stamps of files that, when changed, mean we need to restart the external process. */ private long[] timeStamps; /** * Call this to print messages about * @param string */ private void debug_client(String string) { if (DEBUG_CLIENT) { System.out.println(string); } } static void debug_protocol(String string) { if (DEBUG_PROTOCOL) { System.out.println(string); } } private Process process; // Design note: to interact with the Reader's and Writer's below, do not reference them directly! // use the println and readLine methods in this class instead. private LineReader fromProcess; // Read this to get the output from the process private PrintWriter toProcess; // Write to this to send something to the process private PrintWriter toConsoleOut; // Write to this to show regular output to the user. private PrintWriter toConsoleErr; // Write to this to show errors output to the user. // private BufferedReader fromConsole; // Read from this to get input from the user. // Why the above is removed? Because Grails implicitly uses System.in, not much we can do about that. private IGrailsInstall install; private File workingDir; private String javaVM; private boolean wasDestroyed = false; public GrailsClient(IGrailsInstall install, File workingDir) { this.install = install; this.workingDir = workingDir; } private void init() throws IOException { String grailsHome = install.getHome(); List<String> args = new ArrayList<String>(); args.add(javaVM()); if (DEBUG_PROCESS!=null) { args.add(DEBUG_PROCESS); } //VM ARGS GrailsLaunchArgumentUtils.addUserDefinedJVMArgs(args); GrailsLaunchArgumentUtils.addMemorySettings(args); // args.add("-Xmx512M"); // args.add("-XX:MaxPermSize=192m"); args.add("-classpath"); args.add(bootstrapClassPath(install)); // System properties Map<String, String> systemProps = GrailsCoreActivator.getDefault().getLaunchSystemProperties(); GrailsLaunchArgumentUtils.setMaybe(systemProps, "file.encoding", "UTF-8"); GrailsLaunchArgumentUtils.setMaybe(systemProps, "grails.home", grailsHome); GrailsLaunchArgumentUtils.setMaybe(systemProps, "tools.jar", toolsJar()); for (Entry<String, String> entry : systemProps.entrySet()) { String key = entry.getKey(); Assert.isTrue(!key.contains("="), "Property "+key+" contains '='"); args.add("-D"+key+"="+entry.getValue()); } // Main class args.add("org.codehaus.groovy.grails.cli.support.GrailsStarter"); // Program args args.add("--main"); args.add(GrailsProcessConstants.PROCESS_CLASS_NAME); args.add("--conf"); args.add(grailsHome+"/conf/groovy-starter.conf"); args.add("--classpath"); args.add(neededPlugins()); if (DEBUG_PROCESS!=null) { args.add("--debug"); } if (install.getVersion().compareTo(GrailsVersion.V_2_0_0)>=0) { args.add("--is14"); } debug_client(">>> Grails exec args"); for (String string : args) { debug_client(string); } debug_client("<<< Grails exec args"); ProcessBuilder processBuilder = new ProcessBuilder(args); processBuilder.directory(workingDir); processBuilder.redirectErrorStream(true); process = processBuilder.start(); fromProcess = new LineReader(this, process.getInputStream(), DEBUG_PROTOCOL); toProcess = new PrintWriter(process.getOutputStream()); timeStamps = getTimeStamps(); Assert.isTrue(isRunning()); } private long[] getTimeStamps() { return new long[] { getTimeStamp("application.properties"), getTimeStamp("grails-app/conf/BuildConfig.groovy") }; } /** * Gets the timestamp of a file, with path relative to the processes workingdir */ private long getTimeStamp(String fileName) { File file = new File(workingDir, fileName); if (file.exists()) { return file.lastModified(); } return 0; } public void changeDir(File newDir) throws IOException, TimeoutException { workingDir = newDir; if (isRunning()) { //This will ensure that if process is running it assumes the correct dir... or shuts down println(toProcess, GrailsProcessConstants.CHANGE_DIR+newDir.getCanonicalPath()); String ack = fromProcess.readLine(POLLING_INTERVAL+20000); if (GrailsProcessConstants.ACK_BAD.equals(ack)) { waitFor(process); // We expect the process to shutdown soon. } else if (GrailsProcessConstants.ACK_OK.equals(ack)) { // fine } else { throw new ProtocolException("Expected an 'ack' but got:\n"+ack); } } } /** * Is the assocated process running? */ public boolean isRunning() { if (process!=null) { try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { return true; // this exception indicates it isn't terminated yet. } } return false; } /** * Determines where the output from this process will be sent by default (unless overriden * by parameter to executeCommand. * * @param out */ public void setDefaultConsole(OutputStream out) { toConsoleOut = out == null ? null : new PrintWriter(out); toConsoleErr = out == null ? null : toConsoleOut; } private String neededPlugins() throws IOException { List<String> entries = GrailsLaunchArgumentUtils.getBuildListenerClassPath(install.getVersion()); return GrailsLaunchArgumentUtils.toPathsString(entries); } private String javaVM() { if (javaVM==null) { IVMInstall vm = JavaRuntime.getDefaultVMInstall(); File jrePath = vm.getInstallLocation(); //TODO: this probably doesn't work on all platforms. File javaExePath = new File(new File(jrePath, "bin"), "java"); if (!javaExePath.exists()) { // Maybe on windows? javaExePath = new File(new File(jrePath, "bin"), "javaw.exe"); } Assert.isTrue(javaExePath.exists()); javaVM = javaExePath.getAbsolutePath(); } return javaVM; } private String toolsJar() { IVMInstall vm = JavaRuntime.getDefaultVMInstall(); File jrePath = vm.getInstallLocation(); File toolsPath = new File(new File(jrePath, "lib"), "tools.jar"); return toolsPath.getAbsolutePath(); } private String bootstrapClassPath(IGrailsInstall install) { StringBuffer buf = new StringBuffer(); File[] entries = install.getBootstrapClasspath(); for (int i = 0; i < entries.length; i++) { if (i>0) { buf.append(File.pathSeparatorChar); } buf.append(entries[i]); } return buf.toString(); } // /** // * Executes a given command remotely redirecting output from this command to a given output Stream. // * <p> // * If null is passed as the outputStream, then executeCommand will send output to the default // * console instead (if it is set). // * @throws TimeoutException // */ // public synchronized int executeCommand(String scriptName, String args, OutputStream out, long timeOut) throws IOException, TimeoutException { // PrintWriter savedConsole = toConsole; // try { // if (out!=null) { // toConsole = new PrintWriter(out); // } // toProcess.println(GrailsProcessConstants.COMMAND_SCRIPT_NAME +scriptName); // toProcess.println(GrailsProcessConstants.COMMAND_ARGS +args); // toProcess.println(GrailsProcessConstants.END_COMMAND); // toProcess.flush(); //Must call 'flush or output may remain buffered-up and not processed by // // the GrailsProcessConstants. This will cause both the client and the GrailsProcessConstants to hang waiting for // // input that is not coming. // return getCommandResult(timeOut); // } // finally { // flush(toConsole); //Ensure all output is written before proceeding // toConsole = savedConsole; // } // } // /** * Executes a given command remotely redirecting output from this command to a given output Stream. * <p> * If null is passed as the outputStream, then executeCommand will send output to the default * console instead (if it is set). * @throws TimeoutException */ public synchronized int executeCommand(GrailsCommand cmd, Console console) throws IOException, TimeoutException { restartProcessIfNeeded(); Assert.isTrue(isRunning(), "The external Grails process is no longer running (crashed?)"); PrintWriter savedToConsole = toConsoleOut; try { // checkSystemProps(cmd); // check disabled, all properties are now supported if (console!=null) { toConsoleOut = new PrintWriter(console.getOutputStream()); toConsoleErr = new PrintWriter(console.getErrorStream()); } println(toProcess, GrailsProcessConstants.BEGIN_COMMAND); println(toProcess, GrailsProcessConstants.COMMAND_UNPARSED + cmd.getCommand()); File depFile = cmd.getDependencyFile(); if (depFile !=null) { println(toProcess, GrailsProcessConstants.COMMAND_DEPENDENCY_FILE+depFile.getCanonicalPath()); } println(toProcess, GrailsProcessConstants.END_COMMAND); toProcess.flush(); //Must call 'flush or output may remain buffered-up and not processed by // the GrailsProcess. This will cause both the client and the GrailsProcess to hang waiting for // input that is not coming. SendCommandInput sendInput = new SendCommandInput(this, console.getInputStream(), toProcess); try { return getCommandResult(cmd.getGrailsCommandTimeOut()); } finally { sendInput.terminate(); } } catch (TimeoutException e) { System.out.println("Connection to GrailsProcess timed out"); // Try to get some diagnostic info from grails process before killing it. String traces = null; if (isRunning()) { traces = new GrailsProcessStackTracer().getStackTraces(); } shutDown(); if (traces!=null) { throw new TimeoutException(traces); } else { throw e; } } catch (GrailsProcessDiedException e) { if (wasDestroyed) { toConsoleErr.println("\nProcess was killed"); toConsoleErr.flush(); //Since someone clicked 'stop' button the error is expected. Don't treat as a problem: return 0; } throw e; } finally { flush(toConsoleOut); //Ensure all output is written before proceeding toConsoleOut = savedToConsole; } } /** * Checks variety of conditions that require the external process to be restarted. */ private void restartProcessIfNeeded() throws IOException { debug_client("Checking external process"); if (!isRunning()) { debug_client("External process not running. Starting it"); init(); } else { // process is running, but maybe it is stale? long[] newStamps = getTimeStamps(); if (!Arrays.equals(timeStamps, newStamps)) { debug_client("Timestamps changed, process needs to be restarted"); shutDown(); init(); } } } private void flush(PrintWriter out) { if (out!=null) { out.flush(); } } private int getCommandResult(long timeOut) throws IOException, TimeoutException { if (DEBUG_PROCESS!=null) { timeOut = timeOut * 30; // Otherwise with breakpoints in the process, it will timeout } String line = fromProcess.readLine(timeOut); while (line!=null && !line.startsWith(GrailsProcessConstants.END_COMMAND)) { if (line.startsWith(GrailsProcessConstants.CONSOLE_OUT)) { println(toConsoleOut, line.substring(GrailsProcessConstants.PROTOCOL_HEADER_LEN)); } else if (line.startsWith(GrailsProcessConstants.CONSOLE_ERR)) { println(toConsoleErr, line.substring(GrailsProcessConstants.PROTOCOL_HEADER_LEN)); } else { //anything else... not explicitly tagged as 'out' or 'err' output. This may be some error messages // produced while trying to start the JVM. E.g. when illegal JVM args are passed etc. //We'll just pretend this output is going to system.err. println(toConsoleErr, line); } line = fromProcess.readLine(timeOut); } if (line==null) { //That really shouldn't be happening! // But it might if something goes wrong that makes the grails process crash. return -1; } return Integer.valueOf(line.substring(GrailsProcessConstants.PROTOCOL_HEADER_LEN)); } void println(PrintWriter out, String line) { if (out!=null) { if (toProcess==out) { debug_protocol("send>>> "+line); } out.println(line); out.flush(); } } /** * Calling this method ensures that the external process associated with this client is terminated. * Note that this method is synchronized as is the executeCommand method. Thus it will wait for * the current command to finish. To more quickly and forcibly shutdown the process, call the * 'destroy' method instead. * <p> * Calling this method when the process is already terminated has no effect. */ public synchronized void shutDown() { if (process==null) { return; } //Ask the process (nicely) to terminate println(toProcess, GrailsProcessConstants.EXIT); flush(toProcess); try { String ack = fromProcess.readLine(POLLING_INTERVAL+300); if (GrailsProcessConstants.ACK_OK.equals(ack)) { waitFor(process); process = null; } } catch (TimeoutException e) { //This somewhat expected, when some Grails command caused Grails to block, waiting for user input // don't log it. } catch (Exception e) { GrailsCoreActivator.log(e); } finally { if (process!=null) { // Whatever happened, timeout, nonsense response... // try your best to shut this process down. process.destroy(); process = null; } } } public void destroy() { Process p = this.process; if (p!=null) { p.destroy(); this.process = null; } wasDestroyed = true; // Will suppress the now expected error when we detect later that process has died. } private void waitFor(Process process) { boolean done = false; while (!done) { try { process.waitFor(); done = true; } catch (InterruptedException e) { //Ignore this and keep waiting until it really terminates } } } public IGrailsInstall getInstall() { return install; } }