blob: 1b2737665fd227afd0038ef544485f9736259dab [file] [log] [blame]
/*******************************************************************************
* 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$
}
}
}