blob: 7add4d4f0d315408c7a30c874ec68c0da570c96c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2010, 2012 IBM Corporation 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:
* IBM Corporation - initial API and implementation
* Jeff Overbey (Illinois) - Environment management support
*******************************************************************************/
package org.eclipse.ptp.internal.rdt.sync.cdt.core.remotemake;
import java.io.IOException;
import java.io.OutputStream;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.ICommandLauncher;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.ptp.ems.core.EnvManagerConfigString;
import org.eclipse.ptp.ems.core.EnvManagerRegistry;
import org.eclipse.ptp.ems.core.IEnvManager;
import org.eclipse.ptp.internal.rdt.sync.cdt.core.Activator;
import org.eclipse.ptp.rdt.sync.core.SyncConfig;
import org.eclipse.ptp.rdt.sync.core.SyncConfigManager;
import org.eclipse.ptp.rdt.sync.core.SyncFlag;
import org.eclipse.ptp.rdt.sync.core.SyncManager;
import org.eclipse.ptp.rdt.sync.core.exceptions.MissingConnectionException;
import org.eclipse.remote.core.IRemoteConnection;
import org.eclipse.remote.core.IRemoteFileManager;
import org.eclipse.remote.core.IRemoteProcess;
import org.eclipse.remote.core.IRemoteProcessBuilder;
import org.eclipse.remote.core.RemoteProcessAdapter;
import org.eclipse.remote.core.exception.RemoteConnectionException;
// TODO (Jeff): Remove/replace NON_ESCAPED_ASCII_CHARS, static initializer, and escape(String) after Bug 371691 is fixed
public class SyncCommandLauncher implements ICommandLauncher {
public static final String EMS_CONFIG_PROPERTY = "ems-configuration"; //$NON-NLS-1$
/** ASCII characters that do <i>not</i> need to be escaped on a Bash command line */
private static final Set<Character> NON_ESCAPED_ASCII_CHARS;
static {
NON_ESCAPED_ASCII_CHARS = new HashSet<Character>();
CharacterIterator it = new StringCharacterIterator("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/._-"); //$NON-NLS-1$
for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
NON_ESCAPED_ASCII_CHARS.add(c);
}
}
// See RemoteToolsProcessBuilder ctor and #charEscapify(String, Set<String>)
private static String escape(String inputString) {
if (inputString == null) {
return null;
}
final StringBuilder newString = new StringBuilder(inputString.length() + 16);
final CharacterIterator it = new StringCharacterIterator(inputString);
for (char c = it.first(); c != CharacterIterator.DONE; c = it.next()) {
if (c == '\'') {
newString.append("'\\\\\\''"); //$NON-NLS-1$
} else if (c > 127 || NON_ESCAPED_ASCII_CHARS.contains(c)) { // Do not escape non-ASCII characters (> 127)
newString.append(c);
} else {
newString.append("\\" + c); //$NON-NLS-1$
}
}
return newString.toString();
}
private static String getCommandAsString(IPath commandPath, String[] args) {
final StringBuilder sb = new StringBuilder();
sb.append(escape(commandPath.toOSString()));
sb.append(' ');
for (String arg : args) {
sb.append(escape(arg));
sb.append(' ');
}
return sb.toString();
}
protected IProject fProject;
protected Process fProcess;
protected IRemoteProcess fRemoteProcess;
protected boolean fShowCommand;
protected String[] fCommandArgs;
protected String lineSeparator = "\r\n"; //$NON-NLS-1$
protected String fErrorMessage;
protected Map<String, String> remoteEnvMap;
private boolean shouldSyncBeforeRun = true;
private boolean shouldSyncAfterRun = true;
/**
* The number of milliseconds to pause between polling.
*/
protected static final long DELAY = 50L;
private List<String> constructCommand(IPath commandPath, String[] args, IRemoteConnection connection, IProgressMonitor monitor)
throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 100);
SyncConfig config = SyncConfigManager.getActive(getProject());
final EnvManagerConfigString projectProperties = new EnvManagerConfigString(config.getProperty(EMS_CONFIG_PROPERTY));
if (projectProperties.isEnvMgmtEnabled()) {
// Environment management is enabled for the build. Issue custom Modules/SoftEnv commands to configure the environment.
IEnvManager envManager = EnvManagerRegistry.getEnvManager(progress.newChild(50), connection);
try {
// Create and execute a Bash script which will configure the environment and then execute the command
final List<String> command = new LinkedList<String>();
command.add("bash"); //$NON-NLS-1$
command.add("-l"); //$NON-NLS-1$
final String bashScriptFilename = envManager.createBashScript(progress.newChild(50), true, projectProperties,
getCommandAsString(commandPath, args));
command.add(bashScriptFilename);
return command;
} catch (final Exception e) {
// An error occurred creating the Bash script, so attempt to put the whole thing onto the command line
Activator.log("Error creating bash script for launch; reverting to bash -l -c", e); //$NON-NLS-1$
final List<String> command = new LinkedList<String>();
command.add("bash"); //$NON-NLS-1$
command.add("-l"); //$NON-NLS-1$
command.add("-c"); //$NON-NLS-1$
final String bashCommand = envManager.getBashConcatenation(
"; ", true, projectProperties, getCommandAsString(commandPath, args)); //$NON-NLS-1$
command.add(bashCommand);
return command;
}
} else {
// Environment management disabled. Execute the build command in a login shell (so the default environment is
// configured).
final List<String> command = new LinkedList<String>();
command.add("bash"); //$NON-NLS-1$
command.add("-l"); //$NON-NLS-1$
command.add("-c"); //$NON-NLS-1$
command.add(getCommandAsString(commandPath, args));
return command;
}
}
/**
* Constructs a command array that will be passed to the process
*/
protected String[] constructCommandArray(String command, String[] commandArgs) {
String[] args = new String[1 + commandArgs.length];
args[0] = command;
System.arraycopy(commandArgs, 0, args, 1, commandArgs.length);
return args;
}
private Properties convertEnvMapToProperties() {
Properties properties = new Properties();
for (String key : remoteEnvMap.keySet()) {
properties.put(key, remoteEnvMap.get(key));
}
return properties;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#execute(org.eclipse.core.runtime.IPath, java.lang.String[], java.lang.String[],
* org.eclipse.core.runtime.IPath)
*/
@Override
public Process execute(IPath commandPath, String[] args, String[] env, IPath changeToDirectory, final IProgressMonitor monitor)
throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor, 100);
// if there is no project associated to us then we cannot function... throw an exception
if (getProject() == null) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
"RemoteCommandLauncher has not been associated with a project.")); //$NON-NLS-1$
}
SyncConfig config = SyncConfigManager.getActive(getProject());
if (config == null) {
return null;
}
// Set correct directory
// For managed projects and configurations other than workspace, the directory is incorrect and needs to be fixed.
String projectLocalRoot = getProject().getLocation().toPortableString();
String projectActualRoot = config.getLocation(getProject());
String fixedDirectory = changeToDirectory.toString().replaceFirst(Pattern.quote(projectLocalRoot),
Matcher.quoteReplacement(projectActualRoot));
changeToDirectory = new Path(fixedDirectory);
fCommandArgs = constructCommandArray(commandPath.toPortableString(), args);
// Get and setup the connection and remote services for this sync configuration.
IRemoteConnection connection;
try {
connection = config.getRemoteConnection();
} catch (MissingConnectionException e2) {
throw new CoreException(new Status(IStatus.CANCEL, Activator.PLUGIN_ID,
"Build canceled because connection does not exist")); //$NON-NLS-1$
}
if (!connection.isOpen()) {
try {
connection.open(progress.newChild(20));
} catch (RemoteConnectionException e1) {
// rethrow as CoreException
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error opening connection.", e1)); //$NON-NLS-1$
}
}
// Set process's command and environment
List<String> command = constructCommand(commandPath, args, connection, progress.newChild(10));
IRemoteProcessBuilder processBuilder = connection.getProcessBuilder(command);
remoteEnvMap = processBuilder.environment();
for (String envVar : env) {
String[] splitStr = envVar.split("=", 2); //$NON-NLS-1$
if (splitStr.length > 1) {
remoteEnvMap.put(splitStr[0], splitStr[1]);
} else if (splitStr.length == 1) {
// Empty environment variable
remoteEnvMap.put(splitStr[0], ""); //$NON-NLS-1$
}
}
// set the directory in which to run the command
IRemoteFileManager fileManager = connection.getFileManager();
if (changeToDirectory != null && fileManager != null) {
processBuilder.directory(fileManager.getResource(changeToDirectory.toString()));
}
syncOnPreBuild(progress.newChild(60));
IRemoteProcess p = null;
try {
p = processBuilder.start();
} catch (IOException e) {
// rethrow as CoreException
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Error launching remote process.", e)); //$NON-NLS-1$
}
fRemoteProcess = p;
fProcess = new RemoteProcessAdapter(p);
return fProcess;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#getCommandArgs()
*/
@Override
public String[] getCommandArgs() {
return fCommandArgs;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#getCommandLine()
*/
@Override
public String getCommandLine() {
return getCommandLine(getCommandArgs());
}
private String getCommandLine(String[] commandArgs) {
if (fProject == null) {
return null;
}
StringBuffer buf = new StringBuffer();
if (fCommandArgs != null) {
for (String commandArg : commandArgs) {
buf.append(commandArg);
buf.append(' ');
}
buf.append(lineSeparator);
}
return buf.toString();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#getEnvironment()
*/
@Override
public Properties getEnvironment() {
return convertEnvMapToProperties();
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#getErrorMessage()
*/
@Override
public String getErrorMessage() {
return fErrorMessage;
}
@Override
public IProject getProject() {
return fProject;
}
protected void printCommandLine(OutputStream os) {
if (os != null) {
String cmd = getCommandLine(getCommandArgs());
try {
os.write(cmd.getBytes());
os.flush();
} catch (IOException e) {
// ignore;
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#setErrorMessage(java.lang.String)
*/
@Override
public void setErrorMessage(String error) {
fErrorMessage = error;
}
@Override
public void setProject(IProject project) {
fProject = project;
}
/**
* Set whether launcher should sync project after executing. The default behavior is to sync.
*
* @param shouldSync
*/
public void setSyncAfterRun(boolean shouldSync) {
shouldSyncAfterRun = shouldSync;
}
/**
* Set whether launcher should sync project before executing. The default behavior is to sync.
*
* @param shouldSync
*/
public void setSyncBeforeRun(boolean shouldSync) {
shouldSyncBeforeRun = shouldSync;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#showCommand(boolean)
*/
@Override
public void showCommand(boolean show) {
fShowCommand = show;
}
private void syncOnPostBuild(IProgressMonitor monitor) throws CoreException {
SyncConfig config = SyncConfigManager.getActive(getProject());
if (shouldSyncAfterRun && SyncManager.getSyncAuto() && config.isSyncOnPostBuild()) {
SyncManager.syncBlocking(null, getProject(), SyncFlag.RL_ONLY, monitor, null);
}
}
private void syncOnPreBuild(IProgressMonitor monitor) throws CoreException {
SyncConfig config = SyncConfigManager.getActive(getProject());
if (shouldSyncBeforeRun && config.isSyncOnPreBuild()) {
switch (SyncManager.getSyncMode(getProject())) {
case ACTIVE:
SyncManager.syncBlocking(null, getProject(), SyncFlag.LR_ONLY, monitor, null);
break;
case ALL:
SyncManager.syncAllBlocking(null, getProject(), SyncFlag.LR_ONLY, null);
break;
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#waitAndRead(java.io.OutputStream, java.io.OutputStream)
*/
@Override
public int waitAndRead(OutputStream out, OutputStream err) {
if (fShowCommand) {
printCommandLine(out);
}
if (fProcess == null) {
return ILLEGAL_COMMAND;
}
RemoteProcessClosure closure = new RemoteProcessClosure(fRemoteProcess, out, err);
closure.runBlocking(); // a blocking call
return OK;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.cdt.core.ICommandLauncher#waitAndRead(java.io.OutputStream, java.io.OutputStream,
* org.eclipse.core.runtime.IProgressMonitor)
*/
@Override
public int waitAndRead(OutputStream output, OutputStream err, IProgressMonitor monitor) {
SubMonitor progress = SubMonitor.convert(monitor, 15);
if (fShowCommand) {
printCommandLine(output);
}
if (fProcess == null) {
return ILLEGAL_COMMAND;
}
RemoteProcessClosure closure = new RemoteProcessClosure(fRemoteProcess, output, err);
closure.runNonBlocking();
while (!progress.isCanceled() && closure.isRunning()) {
try {
Thread.sleep(DELAY);
progress.worked(1);
} catch (InterruptedException ie) {
// ignore
}
}
// Poorly named function - actually closes streams and resets variables
closure.isAlive();
int state = OK;
// Operation canceled by the user, terminate abnormally.
if (progress.isCanceled()) {
closure.terminate();
state = COMMAND_CANCELED;
setErrorMessage(CCorePlugin.getResourceString("CommandLauncher.error.commandCanceled")); //$NON-NLS-1$
}
try {
fProcess.waitFor();
} catch (InterruptedException e) {
// ignore
}
try {
syncOnPostBuild(progress.newChild(5));
} catch (CoreException e) {
Activator.log(e);
}
try {
// 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 that for huge projects, it may take a while
getProject().refreshLocal(IResource.DEPTH_INFINITE, progress.newChild(5));
} catch (CoreException e) {
// this should never happen because we should never be building from a
// state where ressource changes are disallowed
}
return state;
}
}