/* Copyright (C) 2009 Mobile Sorcery AB
This program is free software; you can redistribute it and/or modify it
under the terms of the Eclipse Public License v1.0.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License v1.0 for
more details.
You should have received a copy of the Eclipse Public License v1.0 along
with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html
*/
package com.mobilesorcery.sdk.core;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.internal.core.LaunchManager;
import com.mobilesorcery.sdk.core.LineReader.ILineHandler;
public class CommandLineExecutor {
private static final int MAX_DEPTH = 100;
private final ArrayList<String[]> lines = new ArrayList<String[]>();
private final ArrayList<String> consoleMsgs = new ArrayList<String>();
private CascadingProperties parameters;
private String dir;
private Process currentProcess;
private final boolean killed = false;
private final String consoleName;
private ILineHandler stdoutHandler;
private ILineHandler stderrHandler;
private HashMap<String, String> envs;
public CommandLineExecutor(String consoleName) {
this.consoleName = consoleName;
}
/**
* Adds a <emph>parameterized</emph> command line
*
* @param line
*/
public void addCommandLine(String[] line) {
addCommandLine(line, null);
}
/**
* Adds a <emph>parameterized</emph> command line
*
* @param line
* @param consoleMsg the line to display in the console, or <code>null</code> for
* just outputting the resolved <code>line</code>. Any parameters in <code>consoleMsg</code>
* will also be resolved
*/
public void addCommandLine(String[] line, String consoleMsg) {
lines.add(line);
consoleMsgs.add(consoleMsg);
}
public int runCommandLine(Map<String, String> env, String[] commandLine, String consoleMsg) throws IOException {
for (Map.Entry<String, String> var : env.entrySet()) {
addEnv(var.getKey(), var.getValue());
}
return runCommandLine(commandLine, consoleMsg);
}
/**
* Convenience method for running exactly one line.
*
* @param commandLine
* @throws IOException
*/
public int runCommandLine(String[] commandLine) throws IOException {
int res;
lines.clear();
consoleMsgs.clear();
addCommandLine(commandLine, null);
res = execute();
lines.clear();
consoleMsgs.clear();
return res;
}
public int runCommandLine(String[] commandLine, String consoleMsg) throws IOException {
int res;
lines.clear();
consoleMsgs.clear();
addCommandLine(commandLine, consoleMsg);
res = execute();
lines.clear();
consoleMsgs.clear();
return res;
}
/**
* Convenience method for running exactly one line.
*
* @param commandLine
* @return Exit code of the process
* @throws IOException
*/
public int runCommandLineWithRes ( String[] commandLine )
throws IOException
{
return runCommandLine(commandLine);
}
public void setParameters(CascadingProperties parameters) {
this.parameters = parameters;
}
public void setExecutionDirectory(String dir) {
this.dir = dir;
}
public void addEnv(String env, String value) {
if (envs == null) {
envs = new HashMap<String, String>();
}
envs.put(env, value);
}
/**
* Splits a command line string into an array of string, splitting on
* space and other whitespace. But unlike the way exec does this, it
* actually takes quotation into consideration, treating it as a single
* argument independently from any whitespace it might contain. This is
* necessary on Mac OS X, and perhaps even Linux.
*
* @param cmd Command line to split
* @return Array that contains the result
*/
public static String[] parseCommandLine ( String cmd )
{
ArrayList<String> cmdarray = new ArrayList<String>();
StringTokenizer tok = new StringTokenizer( cmd, " \t\n\r\f", true );
while ( tok.hasMoreTokens() == true )
{
// Is token whitespace ?
String t = tok.nextToken( );
if ( t.matches( "[ \t\n\r\f]" ) == true )
continue;
// Does it start with a quotation mark?, if
// so, go into merge state
if ( t.startsWith( "\"" ) == true )
{
while ( tok.hasMoreTokens( ) == true )
{
String s = tok.nextToken( );
t += s;
if ( s.endsWith( "\"" ) == true )
break;
}
}
// Add new token to result
cmdarray.add( t );
}
return cmdarray.toArray( new String[0] );
}
public void fork() throws IOException {
execute(true);
}
public int execute() throws IOException {
return execute(false);
}
private int execute(boolean fork) throws IOException {
IProcessConsole console = createConsole();
int result = -1;
for (int i = 0; !killed && i < lines.size(); i++) {
String[] line = lines.get(i);
String[] resolvedLine = new String[line.length];
for (int j = 0; j < resolvedLine.length; j++) {
resolvedLine[j] = replace(line[j], parameters);
}
String mergedCommandLine = mergeCommandLine(resolvedLine);
String consoleMsg = consoleMsgs.get(i);
if (consoleMsg == null) {
console.addMessage(mergedCommandLine);
} else {
console.addMessage(replace(consoleMsg, parameters));
}
/* It is better to pass the command as an array here since then Java will
* fix all problems with quotations and such that are suitable for the
* platform. */
if (dir == null && envs == null) {
currentProcess = Runtime.getRuntime().exec(resolvedLine);
} else {
currentProcess = Runtime.getRuntime().exec(resolvedLine, getEnv(), dir == null ? null : new File(dir));
}
console.attachProcess(currentProcess, stdoutHandler, stderrHandler);
// Ok, we're up and running
if (stdoutHandler != null) { stdoutHandler.start(currentProcess); }
if (stderrHandler != null) { stderrHandler.start(currentProcess); }
try {
result = fork ? 0 : currentProcess.waitFor();
} catch (InterruptedException e) {
throw new IOException("Process interrupted.");
}
}
return result;
}
private String[] getEnv() {
if (this.envs == null) {
return null;
}
ArrayList<String> envs = new ArrayList<String>();
Map nativeEnvs = DebugPlugin.getDefault().getLaunchManager().getNativeEnvironmentCasePreserved();
for (Object env : nativeEnvs.keySet()) {
Object nativeValue = nativeEnvs.get(env);
if (!this.envs.containsKey(env)) {
envs.add(env + "=" + nativeValue);
}
}
for (Map.Entry<String, String> env : this.envs.entrySet()) {
envs.add(env.getKey() + "=" + env.getValue());
}
return envs.toArray(new String[envs.size()]);
}
private String mergeCommandLine(String[] commandLine) {
StringBuffer correctCommandLine = new StringBuffer();
for (int i = 0; i < commandLine.length; i++) {
if (i > 0) {
correctCommandLine.append(" ");
}
correctCommandLine.append(assertQuoted(commandLine[i]));
}
return correctCommandLine.toString();
}
private String assertQuoted(String str) {
boolean isQuoted = str.length() > 0 && str.charAt(0) == '\"' && str.charAt(str.length() - 1) == '\"';
if (!isQuoted && (str.indexOf(' ') != -1 || str.indexOf('\t') != -1)) {
return "\"" + str + "\"";
}
return str;
}
public void kill() {
if (currentProcess == null) {
currentProcess.destroy();
}
}
public Process getCurrentProcess() {
return currentProcess;
}
public static String replace(String originalString, CascadingProperties parameters) {
int tries = 0;
String last = "";
String result = originalString;
while (!result.equals(last)) {
tries++;
last = result;
result = replaceOne(result, parameters);
if (tries > MAX_DEPTH) {
return originalString; // Circular dependencies, but we want no exception thrown.
}
}
return result;
}
/**
* Replaces and string in %'s with a parameter in <code>parameters</code>.
*
* @param originalString
* @param parameters
* @return
*/
public static String replaceOne(String originalString, CascadingProperties parameters) {
if (parameters == null) {
return originalString;
}
String result = originalString;
for (Iterator<String> parameterKeys = parameters.keySet().iterator(); parameterKeys.hasNext();) {
String parameterKey = parameterKeys.next();
result = result.replaceAll("%" + parameterKey + "%", Matcher.quoteReplacement(parameters.get(parameterKey)));
}
return result;
}
public IProcessConsole createConsole() {
return CoreMoSyncPlugin.getDefault().createConsole(consoleName);
}
public void setLineHandlers(ILineHandler stdoutHandler,
ILineHandler stderrHandler) {
this.stdoutHandler = stdoutHandler;
this.stderrHandler = stderrHandler;
}
}