/******************************************************************************* * 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.core.launch; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.IStreamListener; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IStreamMonitor; import org.grails.ide.eclipse.core.GrailsCoreActivator; /** * Wrapper around a LaunchConfiguration that can be used to "synchExec" this configuration. * @author Kris De Volder * @author Nieraj Singh * @author Andrew Eisenberg */ public class SynchLaunch { private static final boolean DEBUG = false; public SynchLaunch(ILaunchConfiguration launchConf, int timeOut, int outputLimit) { Assert.isLegal(outputLimit>0); this.launchConf = launchConf; this.timeOut = timeOut; this.outputLimit = outputLimit; // System.out.println("SynchLaunch Timeout is set to "+timeOut); Assert.isTrue(LaunchListenerManager.isSupported(launchConf), "Launch doesn't provide SynchLaunch support"); } /** * Execute Launch synchronously (i.e. block thread and wait for command to terminate) * * @return The command result if the command terminated normally * @throw CoreException if the command terminated with an Error of some kind. */ public ILaunchResult synchExec() throws CoreException { LaunchResult r = synchExecInternal(); if (r.isOK()) return r; else { throw r.getCoreException(); } } public interface ILaunchResult { /** * Exit code used when the process/command is terminated because it timed out. */ public static final int EXIT_TIMEOUT = -713007; /** * @return Text that was written to System.out */ String getOutput(); /** * @return Text that was written to System.err */ String getErrorOutput(); /** * @return the status of the launch */ IStatus getStatus(); boolean isOK(); } /////////////////////////////////////////////////////////////////////////////// // Implementation of public API /////////////////////////////////////////////////////////////////////////////// /** * Launched process is automatically terminated after this amount of time in milliseconds * has elapsed without any new output coming from the process. */ private int timeOut; /** * If more than this number of characters are logged into the output buffer, the oldest * output will be discarded. */ private int outputLimit; public static abstract class LaunchResult implements ILaunchResult { private int exitValue; protected LaunchResult(int exitValue) { this.exitValue = exitValue; } protected final int getExitValue() { return exitValue; } public boolean isOK() { return exitValue == 0; } /** * Override to provide better error messages! */ protected String getErrorMessage() { if (isOK()) return ""; else { return "Exit Value = " + getExitValue(); } } public String getOutput() { return ""; } public String getErrorOutput() { return ""; } @Override public String toString() { return "GrailsCommandResult(" + getExitValue() + ", " + getErrorMessage() + ")"; } /** * Produces something you can throw. You should only call this method if * isOK returned false. * <p> * Override to provide a better implementation. */ public Exception getException() { return new Exception(getErrorMessage()); } public IStatus getStatus() { if (isOK()) { return Status.OK_STATUS; } else { return new Status(IStatus.ERROR, GrailsCoreActivator.PLUGIN_ID, getErrorMessage(), getException()); } } public CoreException getCoreException() { Exception e = getException(); if (e instanceof CoreException) return (CoreException) e; else return new CoreException(getStatus()); } } public static class ExceptionResult extends LaunchResult { private Throwable e; public ExceptionResult(Throwable e) { super(-99); this.e = e; } @Override public String getErrorMessage() { return e.getMessage(); } @Override public Exception getException() { if (e instanceof Exception) { return (Exception) e; } else { return new Exception(e); } } } private ILaunchConfiguration launchConf; private LaunchListener launchListener; private LaunchResult synchExecInternal() { try { LaunchListenerManager.launchWithListener(launchConf, this, launchListener = new LaunchListener()); if (launchListener.isActive()) { return launchListener.waitForResult(); } else { return new ErrorMessage("LaunchListener did not become active. Launch was canceled?"); } } catch (CoreException e) { return new ExceptionResult(e); } } private long timeLimit = Long.MAX_VALUE; private boolean isShowOutput = true; //Set this to false to avoid output being shown in the Eclipse UI Console. private boolean isDoBuild = false; //Set this to true to force a build before the launch private static class ErrorMessage extends LaunchResult { private String msg; ErrorMessage(String msg) { super(-99); this.msg = msg; } @Override public String getErrorMessage() { return msg; } } public static class ResultFromTerminatedLaunch extends LaunchResult { private String output; private String errorOutput; private String commandString = "<unknown>"; public ResultFromTerminatedLaunch(String commandString, int exitValue, String output, String errorOutput) { super(exitValue); Assert.isNotNull(commandString); this.commandString = commandString; this.output = output; this.errorOutput = errorOutput; } @Override public String getOutput() { return output; } @Override public String getErrorOutput() { return errorOutput; } @Override public String getErrorMessage() { return "Command: "+commandString+"\n"+ "---- System.out ----\n"+ getOutput() + "\n---- System.err ----\n" + getErrorOutput(); } @Override public IStatus getStatus() { String shortSummary; int code = getExitValue(); Exception e = getException(); if (code==0 && e==null) { shortSummary = "Command terminated normally"; } else if (code==ILaunchResult.EXIT_TIMEOUT) { shortSummary = "The command '"+commandString+"' was terminated because it didn't produce new output for some time.\n" + "\n" + "See details for the output produced so far.\n" + "\n" + "If you think the command simply needed more time, you can increase the " + "time limit in the Grails preferences page.\n" + "\n" + "See menu Windows >> Preferences >> Grails >> Launch"; } else if (e!=null) { String exceptionText = e.toString(); //Avoid very long error messages in 'short sumary' because it will be blow up the popup error dialog if (exceptionText.length()<200) { shortSummary = "Command terminated with an exception: "+e+" (see details for partial output)"; } else { shortSummary = "Command terminated with an exception: "+e.getClass().getName()+" (see details for partial output)"; } } else { shortSummary = "Command terminated with an error code (see details for output)"; } int statusCode = code==0 ? IStatus.OK : IStatus.ERROR; MultiStatus status = new MultiStatus(GrailsCoreActivator.PLUGIN_ID, statusCode, shortSummary, e); status.add(new Status(statusCode, GrailsCoreActivator.PLUGIN_ID, "------System.out:-----------\n "+getOutput(), null)); status.add(new Status(statusCode, GrailsCoreActivator.PLUGIN_ID, "------System.err:-----------\n"+getErrorOutput(), null)); return status; } } private class LaunchListener extends AbstractLaunchProcessListener { private StringBuffer output = new StringBuffer(); private StringBuffer errorOutput = new StringBuffer(); private LaunchResult result; @Override public void init(IProcess process) { super.init(process); process.getStreamsProxy().getOutputStreamMonitor() .addListener(new CaptureOutput(output)); process.getStreamsProxy().getErrorStreamMonitor() .addListener(new CaptureOutput(errorOutput)); } /** * Kill the process if it is still running. */ public void killProcess() { if (!getProcess().isTerminated()) { try { getProcess().terminate(); setResult(new ResultFromTerminatedLaunch(getProcess().getLabel(), ILaunchResult.EXIT_TIMEOUT, getOutput(), getErrorOutput()+"\nTerminating process: Timeout: no new output for "+timeOut+" milliseconds")); } catch (DebugException e) { //Ignore } } } @Override protected void handleTerminate(DebugEvent debugEvent) { try { setResult(new ResultFromTerminatedLaunch(getProcess().getLabel(), getProcess().getExitValue(), getOutput(), getErrorOutput())); } catch (DebugException e) { setResult(new ExceptionResult(e)); } } private synchronized void setResult(LaunchResult result) { this.result = result; notify(); } private synchronized LaunchResult waitForResult() { while (result == null) { try { wait(timeOut); if (System.currentTimeMillis()>=timeLimit) { if (result==null) { launchListener.killProcess(); } } } catch (InterruptedException e) { } } if (DEBUG) { System.out.println(result); } return result; } private String getErrorOutput() { return getLimitedOutput(errorOutput, outputLimit); } private String getOutput() { return getLimitedOutput(output, outputLimit); } } private static String getLimitedOutput(StringBuffer output, int outputLimit) { if (output.length()>outputLimit) { return output.substring(output.length()-outputLimit); } else { return output.toString(); } } private class CaptureOutput implements IStreamListener { private StringBuffer buffer; public CaptureOutput(StringBuffer buffer) { Assert.isNotNull(buffer); this.buffer = buffer; } public void streamAppended(String text, IStreamMonitor monitor) { refreshTimeLimit(); this.buffer.append(text); if (outputLimit>0) { if (buffer.length() > outputLimit + outputLimit/10) { //We use 10% over limit margin to avoid doing costly buffer delete for every character buffer.delete(0, buffer.length()-outputLimit); } } } @Override public String toString() { return "CaptureOutput(" + buffer.toString() + ")"; } } /** * Resets the timeOut, timeLimit. This happens at the beginning * of execution, as well as every time new output is received * from the command. */ public void refreshTimeLimit() { timeLimit = System.currentTimeMillis()+timeOut; } public boolean isShowOutput() { return isShowOutput; } public boolean isDoBuild() { return isDoBuild; } /** * This corresponds to the "register" parameter of the * {@link ILaunchConfiguration#launch(String, org.eclipse.core.runtime.IProgressMonitor, boolean, boolean)} * method. * <p> * If set to true, then the output of the launch will be shown in the UI, otherwise it will not. * The default value for this property is true. */ public void setShowOutput(boolean isShowOutput) { this.isShowOutput = isShowOutput; } /** * This corresponds to the "build" parameter of the * {@link ILaunchConfiguration#launch(String, org.eclipse.core.runtime.IProgressMonitor, boolean, boolean)} * method. * <p> * If set to true, this will force a build to happen before launching. * The default value for this property is false. */ public void setDoBuild(boolean isDoBuild) { this.isDoBuild = isDoBuild; } @Override public String toString() { return this.launchConf.toString(); } }