/******************************************************************************* | |
* Copyright (c) 2012, 2016 Andrew Gvozdev and others. | |
* 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: | |
* Andrew Gvozdev - initial API and implementation | |
* IBM Corporation | |
*******************************************************************************/ | |
package org.eclipse.cdt.internal.core; | |
import java.io.Closeable; | |
import java.io.IOException; | |
import java.io.OutputStream; | |
import java.net.URI; | |
import java.text.SimpleDateFormat; | |
import java.util.ArrayList; | |
import java.util.Date; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import java.util.concurrent.TimeUnit; | |
import org.eclipse.cdt.core.CCorePlugin; | |
import org.eclipse.cdt.core.ErrorParserManager; | |
import org.eclipse.cdt.core.ICommandLauncher; | |
import org.eclipse.cdt.core.IConsoleParser; | |
import org.eclipse.cdt.core.envvar.IEnvironmentVariable; | |
import org.eclipse.cdt.core.envvar.IEnvironmentVariableManager; | |
import org.eclipse.cdt.core.model.ICModelMarker; | |
import org.eclipse.cdt.core.resources.IConsole; | |
import org.eclipse.cdt.core.resources.RefreshScopeManager; | |
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription; | |
import org.eclipse.cdt.utils.EFSExtensionManager; | |
import org.eclipse.core.resources.IMarker; | |
import org.eclipse.core.resources.IProject; | |
import org.eclipse.core.resources.IResource; | |
import org.eclipse.core.resources.IWorkspace; | |
import org.eclipse.core.resources.IWorkspaceRunnable; | |
import org.eclipse.core.resources.IncrementalProjectBuilder; | |
import org.eclipse.core.resources.ResourcesPlugin; | |
import org.eclipse.core.runtime.Assert; | |
import org.eclipse.core.runtime.CoreException; | |
import org.eclipse.core.runtime.IPath; | |
import org.eclipse.core.runtime.IProgressMonitor; | |
import org.eclipse.core.runtime.NullProgressMonitor; | |
import org.eclipse.core.runtime.Path; | |
import org.eclipse.core.runtime.QualifiedName; | |
/** | |
* Helper class attempting to unify interactions with build console, | |
* such as style of console output and handling of console output parsers. | |
* | |
* As of CDT 8.1, this class is experimental, internal and work in progress. | |
* <strong>API is unstable and subject to change.</strong> | |
*/ | |
public class BuildRunnerHelper implements Closeable { | |
private static final String PROGRESS_MONITOR_QUALIFIER = CCorePlugin.PLUGIN_ID + ".progressMonitor"; //$NON-NLS-1$ | |
private static final int PROGRESS_MONITOR_SCALE = 100; | |
private static final int TICKS_STREAM_PROGRESS_MONITOR = 1 * PROGRESS_MONITOR_SCALE; | |
private static final int TICKS_EXECUTE_PROGRAM = 1 * PROGRESS_MONITOR_SCALE; | |
private static final int TICKS_PARSE_OUTPUT = 1 * PROGRESS_MONITOR_SCALE; | |
private IProject project; | |
private IConsole console = null; | |
private ErrorParserManager errorParserManager = null; | |
private StreamProgressMonitor streamProgressMonitor = null; | |
private OutputStream stdout = null; | |
private OutputStream stderr = null; | |
private OutputStream consoleOut = null; | |
private OutputStream consoleInfo = null; | |
private long startTime = 0; | |
private long endTime = 0; | |
private QualifiedName progressPropertyName = null; | |
private ICommandLauncher launcher; | |
private IPath buildCommand; | |
private String[] args; | |
private URI workingDirectoryURI; | |
String[] envp; | |
private boolean isStreamsOpen = false; | |
boolean isCancelled = false; | |
/** | |
* Constructor. | |
*/ | |
public BuildRunnerHelper(IProject project) { | |
this.project = project; | |
} | |
/** | |
* Set parameters for the launch. | |
* @param envp - String[] array of environment variables in format "var=value" suitable for using | |
* as "envp" with Runtime.exec(String[] cmdarray, String[] envp, File dir) | |
*/ | |
public void setLaunchParameters(ICommandLauncher launcher, IPath buildCommand, String[] args, URI workingDirectoryURI, String[] envp) { | |
this.launcher = launcher; | |
launcher.setProject(project); | |
// Print the command for visual interaction. | |
launcher.showCommand(true); | |
this.buildCommand = buildCommand; | |
this.args = args; | |
this.workingDirectoryURI = workingDirectoryURI; | |
this.envp = envp; | |
} | |
/** | |
* Open and set up streams for use by {@link BuildRunnerHelper}. | |
* This must be followed by {@link #close()} to close the streams. Use try...finally for that. | |
* | |
* @param epm - ErrorParserManger for error parsing and coloring errors on the console | |
* @param buildOutputParsers - list of console output parsers or {@code null}. | |
* @param con - the console. | |
* @param monitor - progress monitor in the initial state where {@link IProgressMonitor#beginTask(String, int)} | |
* has not been called yet. | |
* @throws CoreException | |
*/ | |
public void prepareStreams(ErrorParserManager epm, List<IConsoleParser> buildOutputParsers, IConsole con, IProgressMonitor monitor) throws CoreException { | |
errorParserManager = epm; | |
console = con; | |
// Visualize the flow of the streams: | |
// | |
// console <- EPM | |
// ^ | |
// IConsoleParsers (includes EPM + other parsers) | |
// ^ | |
// null <- StreamMomitor <= Sniffer <= Process (!!! the flow starts here!) | |
// | |
isStreamsOpen = true; | |
consoleOut = console.getOutputStream(); | |
// stdout/stderr get to the console through ErrorParserManager | |
errorParserManager.setOutputStream(consoleOut); | |
List<IConsoleParser> parsers = new ArrayList<IConsoleParser>(); | |
// Using ErrorParserManager as console parser helps to avoid intermixing buffered streams | |
// as ConsoleOutputSniffer waits for EOL to send a line to console parsers | |
// separately for each stream. | |
parsers.add(errorParserManager); | |
if (buildOutputParsers != null) { | |
parsers.addAll(buildOutputParsers); | |
} | |
Integer lastWork = null; | |
if (buildCommand != null && project != null) { | |
progressPropertyName = getProgressPropertyName(buildCommand, args); | |
lastWork = (Integer)project.getSessionProperty(progressPropertyName); | |
} | |
if (lastWork == null) { | |
lastWork = TICKS_STREAM_PROGRESS_MONITOR; | |
} | |
streamProgressMonitor = new StreamProgressMonitor(monitor, null, lastWork.intValue()); | |
ConsoleOutputSniffer sniffer = new ConsoleOutputSniffer(streamProgressMonitor, streamProgressMonitor, parsers.toArray(new IConsoleParser[parsers.size()])); | |
stdout = sniffer.getOutputStream(); | |
stderr = sniffer.getErrorStream(); | |
} | |
/** | |
* @return the output stream to connect stdout of a process | |
*/ | |
public OutputStream getOutputStream() { | |
return stdout; | |
} | |
/** | |
* @return the output stream to connect stderr of a process | |
*/ | |
public OutputStream getErrorStream() { | |
return stderr; | |
} | |
/** | |
* Remove problem markers created for the resource by previous build. | |
* | |
* @param rc - resource to remove its markers. | |
* @param monitor - progress monitor in the initial state where {@link IProgressMonitor#beginTask(String, int)} | |
* has not been called yet. | |
* @throws CoreException | |
*/ | |
public void removeOldMarkers(IResource rc, IProgressMonitor monitor) throws CoreException { | |
if (monitor == null) { | |
monitor = new NullProgressMonitor(); | |
} | |
try { | |
monitor.beginTask("", IProgressMonitor.UNKNOWN); //$NON-NLS-1$ | |
try { | |
if (rc != null) { | |
monitor.subTask(CCorePlugin.getFormattedString("BuildRunnerHelper.removingMarkers", rc.getFullPath().toString())); //$NON-NLS-1$ | |
rc.deleteMarkers(ICModelMarker.C_MODEL_PROBLEM_MARKER, false, IResource.DEPTH_INFINITE); | |
} | |
} catch (CoreException e) { | |
// ignore | |
} | |
if (project != null) { | |
// Remove markers which source is this project from other projects | |
try { | |
IWorkspace workspace = project.getWorkspace(); | |
IMarker[] markers = workspace.getRoot().findMarkers(ICModelMarker.C_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE); | |
String projectName = project.getName(); | |
List<IMarker> markersList = new ArrayList<IMarker>(); | |
for (IMarker marker : markers) { | |
if (projectName.equals(marker.getAttribute(IMarker.SOURCE_ID))) { | |
markersList.add(marker); | |
} | |
} | |
if (markersList.size() > 0) { | |
workspace.deleteMarkers(markersList.toArray(new IMarker[markersList.size()])); | |
} | |
} catch (CoreException e) { | |
// ignore | |
} | |
} | |
} finally { | |
monitor.done(); | |
} | |
} | |
/** | |
* Launch build command and process console output. | |
* | |
* @param monitor - progress monitor in the initial state where {@link IProgressMonitor#beginTask(String, int)} | |
* has not been called yet. | |
* @throws CoreException | |
* @throws IOException | |
*/ | |
public int build(IProgressMonitor monitor) throws CoreException, IOException { | |
Assert.isNotNull(launcher, "Launch parameters must be set before calling this method"); //$NON-NLS-1$ | |
Assert.isNotNull(errorParserManager, "Streams must be created and connected before calling this method"); //$NON-NLS-1$ | |
int status = ICommandLauncher.ILLEGAL_COMMAND; | |
if (monitor == null) { | |
monitor = new NullProgressMonitor(); | |
} | |
try { | |
monitor.beginTask("", TICKS_EXECUTE_PROGRAM + TICKS_PARSE_OUTPUT); //$NON-NLS-1$ | |
isCancelled = false; | |
String pathFromURI = null; | |
if (workingDirectoryURI != null) { | |
pathFromURI = EFSExtensionManager.getDefault().getPathFromURI(workingDirectoryURI); | |
} | |
if (pathFromURI == null) { | |
// fallback to CWD | |
pathFromURI = System.getProperty("user.dir"); //$NON-NLS-1$ | |
} | |
IPath workingDirectory = new Path(pathFromURI); | |
String errMsg = null; | |
monitor.subTask(CCorePlugin.getFormattedString("BuildRunnerHelper.invokingCommand", guessCommandLine(buildCommand.toString(), args))); //$NON-NLS-1$ | |
Process p = launcher.execute(buildCommand, args, envp, workingDirectory, monitor); | |
monitor.worked(TICKS_EXECUTE_PROGRAM); | |
if (p != null) { | |
try { | |
// Close the input of the Process explicitly. | |
// We will never write to it. | |
p.getOutputStream().close(); | |
} catch (IOException e) { | |
} | |
status = launcher.waitAndRead(stdout, stderr, monitor); | |
monitor.worked(TICKS_PARSE_OUTPUT); | |
if (status != ICommandLauncher.OK) { | |
errMsg = launcher.getErrorMessage(); | |
} | |
} else { | |
errMsg = launcher.getErrorMessage(); | |
} | |
if (errMsg != null && !errMsg.isEmpty()) { | |
stderr.write(errMsg.getBytes()); | |
} | |
isCancelled = monitor.isCanceled(); | |
if (!isCancelled && project != null) { | |
project.setSessionProperty(progressPropertyName, Integer.valueOf(streamProgressMonitor.getWorkDone())); | |
} | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} finally { | |
monitor.done(); | |
} | |
return status; | |
} | |
/** | |
* Close all streams except console Info stream which is handled by {@link #greeting(String)}/{@link #goodbye()}. | |
*/ | |
@Override | |
public void close() throws IOException { | |
if (!isStreamsOpen) | |
return; | |
try { | |
if (stdout != null) | |
stdout.close(); | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} finally { | |
stdout = null; | |
try { | |
if (stderr != null) | |
stderr.close(); | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} finally { | |
stderr = null; | |
try { | |
if (streamProgressMonitor != null) | |
streamProgressMonitor.close(); | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} finally { | |
streamProgressMonitor = null; | |
try { | |
if (consoleOut != null) | |
consoleOut.close(); | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} finally { | |
consoleOut = null; | |
} | |
} | |
} | |
} | |
isStreamsOpen = false; | |
} | |
/** | |
* Refresh project in the workspace. | |
* | |
* @param configName - the configuration to refresh | |
* @param monitor - progress monitor in the initial state where {@link IProgressMonitor#beginTask(String, int)} | |
* has not been called yet. | |
*/ | |
public void refreshProject(String configName, IProgressMonitor monitor) { | |
if (monitor == null) { | |
monitor = new NullProgressMonitor(); | |
} | |
try { | |
monitor.beginTask(CCorePlugin.getFormattedString("BuildRunnerHelper.refreshingProject", project.getName()), IProgressMonitor.UNKNOWN); //$NON-NLS-1$ | |
monitor.subTask(""); //$NON-NLS-1$ | |
// Do not allow the cancel of the refresh, since the builder is external | |
// to Eclipse, files may have been created/modified and we will be out-of-sync. | |
// The caveat is for huge projects, it may take sometimes at every build. | |
// Use the refresh scope manager to refresh | |
RefreshScopeManager refreshManager = RefreshScopeManager.getInstance(); | |
IWorkspaceRunnable runnable = refreshManager.getRefreshRunnable(project, configName); | |
ResourcesPlugin.getWorkspace().run(runnable, null, IWorkspace.AVOID_UPDATE, null); | |
} catch (CoreException e) { | |
// ignore exceptions | |
} finally { | |
monitor.done(); | |
} | |
} | |
/** | |
* Print a standard greeting to the console. | |
* Note that start time of the build is recorded by this method. | |
* | |
* This method may open an Info stream which must be closed by call to {@link #goodbye()} | |
* after all informational messages are printed. | |
* | |
* @param kind - kind of build. {@link IncrementalProjectBuilder} constants such as | |
* {@link IncrementalProjectBuilder#FULL_BUILD} should be used. | |
*/ | |
public void greeting(int kind) { | |
String msg = CCorePlugin.getFormattedString("BuildRunnerHelper.buildProject", //$NON-NLS-1$ | |
new String[] { buildKindToString(kind), project.getName() }); | |
greeting(msg); | |
} | |
/** | |
* Print a standard greeting to the console. | |
* Note that start time of the build is recorded by this method. | |
* | |
* This method may open an Info stream which must be closed by call to {@link #goodbye()} | |
* after all informational messages are printed. | |
* | |
* @param kind - kind of build. {@link IncrementalProjectBuilder} constants such as | |
* {@link IncrementalProjectBuilder#FULL_BUILD} should be used. | |
* @param cfgName - configuration name. | |
* @param toolchainName - tool-chain name. | |
* @param isSupported - flag indicating if tool-chain is supported on the system. | |
*/ | |
public void greeting(int kind, String cfgName, String toolchainName, boolean isSupported) { | |
greeting(buildKindToString(kind), cfgName, toolchainName, isSupported); | |
} | |
/** | |
* Print a standard greeting to the console. | |
* Note that start time of the build is recorded by this method. | |
* | |
* This method may open an Info stream which must be closed by call to {@link #goodbye()} | |
* after all informational messages are printed. | |
* | |
* @param kind - kind of build as a String. | |
* @param cfgName - configuration name. | |
* @param toolchainName - tool-chain name. | |
* @param isSupported - flag indicating if tool-chain is supported on the system. | |
*/ | |
public void greeting(String kind, String cfgName, String toolchainName, boolean isSupported) { | |
String msg = CCorePlugin.getFormattedString("BuildRunnerHelper.buildProjectConfiguration", //$NON-NLS-1$ | |
new String[] { kind, cfgName, project.getName() }); | |
greeting(msg); | |
if (!isSupported ){ | |
String errMsg = CCorePlugin.getFormattedString("BuildRunnerHelper.unsupportedConfiguration", //$NON-NLS-1$ | |
new String[] { cfgName, toolchainName }); | |
printLine(errMsg); | |
} | |
} | |
/** | |
* Print the specified greeting to the console. | |
* Note that start time of the build is recorded by this method. | |
* | |
* This method may open an Info stream which must be closed by call to {@link #goodbye()} | |
* after all informational messages are printed. | |
*/ | |
public void greeting(String msg) { | |
startTime = System.currentTimeMillis(); | |
if (consoleInfo == null) { | |
try { | |
consoleInfo = console.getInfoStream(); | |
} catch (CoreException e) { | |
CCorePlugin.log(e); | |
} | |
} | |
toConsole(BuildRunnerHelper.timestamp(startTime) + "**** " + msg + " ****"); //$NON-NLS-1$ //$NON-NLS-2$ | |
} | |
/** | |
* Print a standard footer to the console and close Info stream (must be open with one of {@link #greeting(String)} calls). | |
* That prints duration of the build determined by start time recorded in {@link #greeting(String)}. | |
* | |
* <br><strong>Important: {@link #close()} the streams BEFORE calling this method to properly flush all outputs</strong> | |
*/ | |
public void goodbye() { | |
Assert.isTrue(startTime != 0, "Start time must be set before calling this method."); //$NON-NLS-1$ | |
Assert.isTrue(consoleInfo != null, "consoleInfo must be open with greetings(...) call before using this method."); //$NON-NLS-1$ | |
endTime = System.currentTimeMillis(); | |
String duration = durationToString(endTime - startTime); | |
String msg = isCancelled ? CCorePlugin.getFormattedString("BuildRunnerHelper.buildCancelled", duration) //$NON-NLS-1$ | |
: CCorePlugin.getFormattedString("BuildRunnerHelper.buildFinished", duration); //$NON-NLS-1$ | |
String goodbye = '\n' + timestamp(endTime) + msg + '\n'; | |
try { | |
toConsole(goodbye); | |
} finally { | |
try { | |
consoleInfo.close(); | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} finally { | |
consoleInfo = null; | |
} | |
} | |
} | |
/** | |
* Print the given message to the console. | |
* @param msg - message to print. | |
*/ | |
public void printLine(String msg) { | |
Assert.isNotNull(errorParserManager, "Streams must be created and connected before calling this method"); //$NON-NLS-1$ | |
errorParserManager.processLine(msg); | |
} | |
/** | |
* Compose command line that presumably will be run by launcher. | |
*/ | |
private static String guessCommandLine(String command, String[] args) { | |
StringBuilder buf = new StringBuilder(command + ' '); | |
if (args != null) { | |
for (String arg : args) { | |
buf.append(arg); | |
buf.append(' '); | |
} | |
} | |
return buf.toString().trim(); | |
} | |
/** | |
* Print a message to the console info output. Note that this message is colored | |
* with the color assigned to "Info" stream. | |
* @param msg - message to print. | |
*/ | |
private void toConsole(String msg) { | |
Assert.isNotNull(console, "Streams must be created and connected before calling this method"); //$NON-NLS-1$ | |
try { | |
consoleInfo.write((msg+"\n").getBytes()); //$NON-NLS-1$ | |
} catch (Exception e) { | |
CCorePlugin.log(e); | |
} | |
} | |
/** | |
* Qualified name to keep previous value of build duration in project session properties. | |
*/ | |
private static QualifiedName getProgressPropertyName(IPath buildCommand, String[] args) { | |
String name = "buildCommand." + buildCommand.toString(); //$NON-NLS-1$ | |
if (args != null) { | |
for (String arg : args) { | |
name = name + ' ' + arg; | |
} | |
} | |
return new QualifiedName(PROGRESS_MONITOR_QUALIFIER, name); | |
} | |
/** | |
* Get environment variables from configuration as array of "var=value" suitable | |
* for using as "envp" with Runtime.exec(String[] cmdarray, String[] envp, File dir) | |
* | |
* @param envMap - map of environment variables | |
* @return String array of environment variables in format "var=value" | |
*/ | |
public static String[] envMapToEnvp(Map<String, String> envMap) { | |
// Convert into envp strings | |
List<String> strings = new ArrayList<String>(envMap.size()); | |
for (Entry<String, String> entry : envMap.entrySet()) { | |
strings.add(entry.getKey() + '=' + entry.getValue()); | |
} | |
return strings.toArray(new String[strings.size()]); | |
} | |
/** | |
* Get environment variables from configuration as array of "var=value" suitable | |
* for using as "envp" with Runtime.exec(String[] cmdarray, String[] envp, File dir) | |
* | |
* @param cfgDescription - configuration description. | |
* @return String array of environment variables in format "var=value". Does not return {@code null}. | |
*/ | |
public static String[] getEnvp(ICConfigurationDescription cfgDescription) { | |
IEnvironmentVariableManager mngr = CCorePlugin.getDefault().getBuildEnvironmentManager(); | |
IEnvironmentVariable[] vars = mngr.getVariables(cfgDescription, true); | |
// Convert into envp strings | |
List<String> strings = new ArrayList<String>(vars.length); | |
for (IEnvironmentVariable var : vars) { | |
strings.add(var.getName() + '=' + var.getValue()); | |
} | |
return strings.toArray(new String[strings.size()]); | |
} | |
/** | |
* Convert duration to human friendly format. | |
*/ | |
@SuppressWarnings("nls") | |
private static String durationToString(long duration) { | |
String result = ""; | |
long days = TimeUnit.MILLISECONDS.toDays(duration); | |
if (days > 0) { | |
result += days + "d,"; | |
} | |
long hours = TimeUnit.MILLISECONDS.toHours(duration) % 24; | |
if (hours > 0) { | |
result += hours + "h:"; | |
} | |
long minutes = TimeUnit.MILLISECONDS.toMinutes(duration) % 60; | |
if (minutes > 0) { | |
result += minutes + "m:"; | |
} | |
long seconds = TimeUnit.MILLISECONDS.toSeconds(duration) % 60; | |
if (seconds > 0) { | |
result += seconds + "s."; | |
} | |
long milliseconds = TimeUnit.MILLISECONDS.toMillis(duration) % 1000; | |
result += milliseconds + "ms"; | |
return result; | |
} | |
/** | |
* Supply timestamp to prefix informational messages. | |
*/ | |
@SuppressWarnings("nls") | |
private static String timestamp(long time) { | |
return new SimpleDateFormat("HH:mm:ss").format(new Date(time)) + " "; | |
} | |
/** | |
* Convert build kind to human friendly format. | |
*/ | |
private static String buildKindToString(int kind) { | |
switch (kind) { | |
case IncrementalProjectBuilder.FULL_BUILD: | |
return CCorePlugin.getResourceString("BuildRunnerHelper.build"); //$NON-NLS-1$ | |
case IncrementalProjectBuilder.INCREMENTAL_BUILD: | |
return CCorePlugin.getResourceString("BuildRunnerHelper.incrementalBuild"); //$NON-NLS-1$ | |
case IncrementalProjectBuilder.AUTO_BUILD: | |
return CCorePlugin.getResourceString("BuildRunnerHelper.autoBuild"); //$NON-NLS-1$ | |
case IncrementalProjectBuilder.CLEAN_BUILD: | |
return CCorePlugin.getResourceString("BuildRunnerHelper.cleanBuild"); //$NON-NLS-1$ | |
default: | |
return CCorePlugin.getResourceString("BuildRunnerHelper.build"); //$NON-NLS-1$ | |
} | |
} | |
} |