package org.jruby.util;
import static com.headius.backport9.buffer.Buffers.clearBuffer;
import static com.headius.backport9.buffer.Buffers.flipBuffer;
import static java.lang.System.out;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jruby.Main;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyHash;
import org.jruby.RubyIO;
import org.jruby.RubyInstanceConfig;
import org.jruby.RubyModule;
import org.jruby.RubyString;
import jnr.posix.util.Platform;
import org.jruby.runtime.Helpers;
import org.jruby.ext.rbconfig.RbConfigLibrary;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.io.ChannelHelper;
import org.jruby.util.io.IOOptions;
import org.jruby.util.io.ModeFlags;
@SuppressWarnings("deprecation")
public class ShellLauncher {
private static final boolean DEBUG = false;
private static final String PATH_ENV = "PATH";
private static final String[] DEFAULT_PATH =
{ "/usr/local/bin", "/usr/ucb", "/usr/bin", "/bin" };
private static final String[] WINDOWS_EXE_SUFFIXES =
{ ".exe", ".com", ".bat", ".cmd" };
private static final String[] WINDOWS_INTERNAL_CMDS = {
"assoc", "break", "call", "cd", "chcp",
"chdir", "cls", "color", "copy", "ctty", "date", "del", "dir", "echo", "endlocal",
"erase", "exit", "for", "ftype", "goto", "if", "lfnfor", "lh", "lock", "md", "mkdir",
"move", "path", "pause", "popd", "prompt", "pushd", "rd", "rem", "ren", "rename",
"rmdir", "set", "setlocal", "shift", "start", "time", "title", "truename", "type",
"unlock", "ver", "verify", "vol", };
private static final Pattern SHELL_METACHARACTER_PATTERN =
Pattern.compile("[*?{}\\[\\]<>()~&|$;'`\\\\\"\\n]");
private static final Pattern WIN_ENVVAR_PATTERN = Pattern.compile("%\\w+%");
private static class ScriptThreadProcess extends Process implements Runnable {
private final String[] argArray;
private final String[] env;
private final File pwd;
private final boolean pipedStreams;
private final PipedInputStream processOutput;
private final PipedInputStream processError;
private final PipedOutputStream processInput;
private RubyInstanceConfig config;
private Thread processThread;
private int result;
private Ruby parentRuntime;
public ScriptThreadProcess(Ruby parentRuntime, final String[] argArray, final String[] env, final File dir) {
this(parentRuntime, argArray, env, dir, true);
}
public ScriptThreadProcess(Ruby parentRuntime, final String[] argArray, final String[] env, final File dir, final boolean pipedStreams) {
this.parentRuntime = parentRuntime;
this.argArray = argArray;
this.env = env;
this.pwd = dir;
this.pipedStreams = pipedStreams;
if (pipedStreams) {
processOutput = new PipedInputStream();
processError = new PipedInputStream();
processInput = new PipedOutputStream();
} else {
processOutput = processError = null;
processInput = null;
}
}
public void run() {
try {
this.result = (new Main(config).run(argArray)).getStatus();
} catch (Throwable throwable) {
throwable.printStackTrace(this.config.getError());
this.result = -1;
} finally {
this.config.getOutput().close();
this.config.getError().close();
try {this.config.getInput().close();} catch (IOException ioe) {}
}
}
private static Map<String, String> environmentMap(String[] env) {
Map<String, String> map = new HashMap<>(env.length + 2, 1);
for (int i = 0; i < env.length; i++) {
List<String> kv = StringSupport.split(env[i], '=', 2);
map.put(kv.get(0), kv.get(1));
}
return map;
}
public void start() throws IOException {
config = new RubyInstanceConfig(parentRuntime.getInstanceConfig());
config.setCurrentDirectory(pwd.toString());
config.setEnvironment(environmentMap(env));
if (pipedStreams) {
config.setInput(new PipedInputStream(processInput));
config.setOutput(new PrintStream(new PipedOutputStream(processOutput)));
config.setError(new PrintStream(new PipedOutputStream(processError)));
}
String procName = "piped";
if (argArray.length > 0) {
procName = argArray[0];
}
processThread = new Thread(this, "ScriptThreadProcess: " + procName);
processThread.setDaemon(true);
processThread.start();
}
public OutputStream getOutputStream() {
return processInput;
}
public InputStream getInputStream() {
return processOutput;
}
public InputStream getErrorStream() {
return processError;
}
public int waitFor() throws InterruptedException {
processThread.join();
return result;
}
public int exitValue() {
return result;
}
public void destroy() {
if (pipedStreams) {
closeStreams();
}
processThread.interrupt();
}
private void closeStreams() {
try { processInput.close(); } catch (IOException io) {}
try { processOutput.close(); } catch (IOException io) {}
try { processError.close(); } catch (IOException io) {}
}
}
public static String[] getCurrentEnv(Ruby runtime) {
return getModifiedEnv(runtime, Collections.EMPTY_LIST, false);
}
private static String[] getCurrentEnv(Ruby runtime, Map mergeEnv) {
return getModifiedEnv(runtime, mergeEnv == null ? Collections.EMPTY_LIST : mergeEnv.entrySet(), false);
}
public static String[] getModifiedEnv(Ruby runtime, Collection mergeEnv, boolean clearEnv) {
ThreadContext context = runtime.getCurrentContext();
boolean traceEnabled = context.isEventHooksEnabled();
context.setEventHooksEnabled(false);
try {
RubyHash hash = null;
if (!clearEnv) {
hash = (RubyHash) runtime.getObject().getConstant("ENV").dup();
}
String[] ret;
if (mergeEnv != null) {
ret = new String[hash.size() + mergeEnv.size()];
} else {
ret = new String[hash.size()];
}
int i = 0;
if (hash != null) {
for (Map.Entry<String, String> e : (Set<Map.Entry<String, String>>)hash.entrySet()) {
if (e.getKey() == null) {
throw runtime.newTypeError(runtime.getNil(), runtime.getStructClass());
}
if (e.getValue() == null) {
continue;
}
ret[i] = e.getKey() + '=' + e.getValue();
i++;
}
}
if (mergeEnv != null) {
if (mergeEnv instanceof Set) {
for (Map.Entry e : (Set<Map.Entry>)mergeEnv) {
if (e.getKey() == null) {
throw runtime.newTypeError(runtime.getNil(), runtime.getStructClass());
}
if (e.getValue() == null) {
continue;
}
ret[i] = e.getKey().toString() + '=' + e.getValue();
i++;
}
} else if (mergeEnv instanceof RubyArray) {
for (int j = 0; j < mergeEnv.size(); j++) {
RubyArray e = ((RubyArray)mergeEnv).eltOk(j).convertToArray();
if (e.size() != 2) {
throw runtime.newArgumentError("env assignments must come in pairs");
}
if (e.eltOk(0) == null) {
throw runtime.newTypeError(runtime.getNil(), runtime.getStructClass());
}
if (e.eltOk(1) == null) {
continue;
}
ret[i] = e.eltOk(0).toString() + '=' + e.eltOk(1).toString();
i++;
}
}
}
return arrayOfLength(ret, i);
} finally {
context.setEventHooksEnabled(traceEnabled);
}
}
private static String[] arrayOfLength(final String[] ary, final int len) {
return len == ary.length ? ary : Arrays.copyOf(ary, len);
}
private static boolean filenameIsPathSearchable(String fname, boolean forExec) {
if (fname.startsWith("/") ||
fname.startsWith("./") ||
fname.startsWith("../") ||
(forExec && (fname.indexOf('/') != -1))) {
return false;
}
if (Platform.IS_WINDOWS) {
if (fname.startsWith("\\") ||
fname.startsWith(".\\") ||
fname.startsWith("..\\") ||
((fname.length() > 2) && fname.charAt(1) == ':') ||
(forExec && (fname.indexOf('\\') != -1))) {
return false;
}
}
return true;
}
private static File tryFile(Ruby runtime, String fdir, String fname) {
File pathFile;
if (fdir == null) {
pathFile = new File(fname);
} else {
pathFile = new File(fdir, fname);
}
if (!pathFile.isAbsolute()) {
pathFile = new File(runtime.getCurrentDirectory(), pathFile.getPath());
}
log(runtime, "Trying file " + pathFile);
if (pathFile.exists()) return pathFile;
return null;
}
private static boolean withExeSuffix(String fname) {
String lowerCaseFname = fname.toLowerCase();
for (String suffix : WINDOWS_EXE_SUFFIXES) {
if (lowerCaseFname.endsWith(suffix)) {
return true;
}
}
return false;
}
private static File isValidFile(Ruby runtime, String fdir, String fname, boolean isExec) {
File validFile = null;
if (isExec && Platform.IS_WINDOWS) {
if (withExeSuffix(fname)) {
validFile = tryFile(runtime, fdir, fname);
} else {
for (String suffix: WINDOWS_EXE_SUFFIXES) {
validFile = tryFile(runtime, fdir, fname + suffix);
if (validFile != null) {
break;
}
}
}
} else {
validFile = tryFile(runtime, fdir, fname);
if (validFile != null) {
if (validFile.isDirectory()) {
return null;
}
if (isExec && !runtime.getPosix().stat(validFile.getAbsolutePath()).isExecutable()) {
throw runtime.newErrnoEACCESError(validFile.getAbsolutePath());
}
}
}
return validFile;
}
private static File isValidFile(Ruby runtime, String fname, boolean isExec) {
String fdir = null;
return isValidFile(runtime, fdir, fname, isExec);
}
private static File findPathFile(Ruby runtime, String fname, String[] path, boolean isExec) {
File pathFile = null;
if (Platform.IS_WINDOWS && fname.startsWith("\"") && fname.endsWith("\"")) {
fname = fname.substring(1, fname.length() - 1);
}
boolean doPathSearch = filenameIsPathSearchable(fname, isExec);
if (doPathSearch) {
for (String fdir: path) {
try {
pathFile = isValidFile(runtime, fdir, fname, isExec);
if (pathFile != null) {
break;
}
} catch (SecurityException se) {
continue;
}
}
} else {
pathFile = isValidFile(runtime, fname, isExec);
}
return pathFile;
}
public static File findPathExecutable(Ruby runtime, String fname) {
RubyHash env = (RubyHash) runtime.getObject().getConstant("ENV");
IRubyObject pathObject = env.op_aref(runtime.getCurrentContext(), RubyString.newString(runtime, PATH_ENV));
return findPathExecutable(runtime, fname, pathObject);
}
public static File findPathExecutable(Ruby runtime, String fname, IRubyObject pathObject) {
String[] pathNodes;
if (pathObject == null || pathObject.isNil()) {
RubyHash env = (RubyHash) runtime.getObject().getConstant("ENV");
pathObject = env.op_aref(runtime.getCurrentContext(), RubyString.newString(runtime, PATH_ENV));
}
if (pathObject == null) {
pathNodes = DEFAULT_PATH;
}
else {
String pathSeparator = System.getProperty("path.separator");
String path = pathObject.toString();
if (Platform.IS_WINDOWS) {
path = "." + pathSeparator + path;
}
pathNodes = path.split(pathSeparator);
}
return findPathFile(runtime, fname, pathNodes, true);
}
public static int runAndWait(Ruby runtime, IRubyObject[] rawArgs) {
return runAndWait(runtime, rawArgs, runtime.getOutputStream());
}
public static long[] runAndWaitPid(Ruby runtime, IRubyObject[] rawArgs) {
return runAndWaitPid(runtime, rawArgs, runtime.getOutputStream(), true);
}
public static long runWithoutWait(Ruby runtime, IRubyObject[] rawArgs) {
return runWithoutWait(runtime, rawArgs, runtime.getOutputStream());
}
public static int runExternalAndWait(Ruby runtime, IRubyObject[] rawArgs, Map mergeEnv) {
OutputStream output = runtime.getOutputStream();
OutputStream error = runtime.getErrorStream();
InputStream input = runtime.getInputStream();
File pwd = new File(runtime.getCurrentDirectory());
LaunchConfig cfg = new LaunchConfig(runtime, rawArgs, true);
try {
Process process;
try {
if (cfg.shouldRunInShell()) {
log(runtime, "Launching with shell");
cfg.verifyExecutableForShell();
process = buildProcess(runtime, cfg.getExecArgs(), getCurrentEnv(runtime, mergeEnv), pwd);
} else {
log(runtime, "Launching directly (no shell)");
cfg.verifyExecutableForDirect();
}
final String[] execArgs = cfg.getExecArgs();
if (changeDirInsideJar(runtime, execArgs)) {
pwd = new File(System.getProperty("user.dir"));
}
process = buildProcess(runtime, execArgs, getCurrentEnv(runtime, mergeEnv), pwd);
} catch (SecurityException se) {
throw runtime.newSecurityError(se.getLocalizedMessage());
}
handleStreams(runtime, process, input, output, error);
return process.waitFor();
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
} catch (InterruptedException e) {
throw runtime.newThreadError("unexpected interrupt");
}
}
public static long runExternalWithoutWait(Ruby runtime, IRubyObject env, IRubyObject prog, IRubyObject options, IRubyObject args) {
return runExternal(runtime, env, prog, options, args, false);
}
public static long runExternal(Ruby runtime, IRubyObject env, IRubyObject prog, IRubyObject options, IRubyObject args, boolean wait) {
if (env.isNil() || !(env instanceof Map)) {
env = null;
}
IRubyObject[] rawArgs = args.convertToArray().toJavaArray();
OutputStream output = runtime.getOutputStream();
OutputStream error = runtime.getErrorStream();
InputStream input = runtime.getInputStream();
File pwd = new File(runtime.getCurrentDirectory());
LaunchConfig cfg = new LaunchConfig(runtime, rawArgs, true);
try {
Process process;
try {
if (cfg.shouldRunInShell()) {
log(runtime, "Launching with shell");
cfg.verifyExecutableForShell();
} else {
log(runtime, "Launching directly (no shell)");
cfg.verifyExecutableForDirect();
}
final String[] execArgs = cfg.getExecArgs();
if (changeDirInsideJar(runtime, execArgs)) {
pwd = new File(".");
}
process = buildProcess(runtime, execArgs, getCurrentEnv(runtime, (Map) env), pwd);
} catch (SecurityException se) {
throw runtime.newSecurityError(se.getLocalizedMessage());
}
if (wait) {
handleStreams(runtime, process, input, output, error);
try {
return process.waitFor();
} catch (InterruptedException e) {
throw runtime.newThreadError("unexpected interrupt");
}
} else {
handleStreamsNonblocking(runtime, process, runtime.getOutputStream(), error);
return getPidFromProcess(process);
}
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
}
}
private static boolean changeDirInsideJar(final Ruby runtime, final String[] args) {
final String arg;
if ((arg = changeDirInsideJar(runtime, args[args.length - 1])) != null) {
args[args.length - 1] = arg;
return true;
}
return false;
}
public static String changeDirInsideJar(final Ruby runtime, final String arg) {
if (runtime.getCurrentDirectory().startsWith("uri:classloader:") && arg.contains("org.jruby.Main")) {
return StringSupport.replaceFirst(arg, "org.jruby.Main", "org.jruby.Main -C " + runtime.getCurrentDirectory()).toString();
}
return null;
}
public static Process buildProcess(Ruby runtime, String[] args, String[] env, File pwd) throws IOException {
return runtime.getPosix().newProcessMaker(args)
.environment(env)
.directory(pwd)
.start();
}
public static long runExternalWithoutWait(Ruby runtime, IRubyObject[] rawArgs) {
return runWithoutWait(runtime, rawArgs, runtime.getOutputStream());
}
public static int execAndWait(Ruby runtime, IRubyObject[] rawArgs) {
return execAndWait(runtime, rawArgs, Collections.EMPTY_MAP);
}
public static int execAndWait(Ruby runtime, IRubyObject[] rawArgs, Map mergeEnv) {
File pwd = new File(runtime.getCurrentDirectory());
LaunchConfig cfg = new LaunchConfig(runtime, rawArgs, true);
if (cfg.shouldRunInProcess()) {
log(runtime, "ExecAndWait in-process");
try {
ScriptThreadProcess ipScript = new ScriptThreadProcess(
runtime, cfg.getExecArgs(), getCurrentEnv(runtime, mergeEnv), pwd, false);
ipScript.start();
return ipScript.waitFor();
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
} catch (InterruptedException e) {
throw runtime.newThreadError("unexpected interrupt");
}
} else {
return runExternalAndWait(runtime, rawArgs, mergeEnv);
}
}
public static int runAndWait(Ruby runtime, IRubyObject[] rawArgs, OutputStream output) {
return runAndWait(runtime, rawArgs, output, true);
}
public static int runAndWait(Ruby runtime, IRubyObject[] rawArgs, OutputStream output, boolean doExecutableSearch) {
return (int)runAndWaitPid(runtime, rawArgs, output, doExecutableSearch)[0];
}
public static long[] runAndWaitPid(Ruby runtime, IRubyObject[] rawArgs, OutputStream output, boolean doExecutableSearch) {
OutputStream error = runtime.getErrorStream();
InputStream input = runtime.getInputStream();
try {
Process aProcess = run(runtime, rawArgs, doExecutableSearch);
handleStreams(runtime, aProcess, input, output, error);
return new long[] {aProcess.waitFor(), getPidFromProcess(aProcess)};
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
} catch (InterruptedException e) {
throw runtime.newThreadError("unexpected interrupt");
}
}
private static long runWithoutWait(Ruby runtime, IRubyObject[] rawArgs, OutputStream output) {
OutputStream error = runtime.getErrorStream();
try {
Process aProcess = run(runtime, rawArgs, true);
handleStreamsNonblocking(runtime, aProcess, output, error);
return getPidFromProcess(aProcess);
} catch (IOException e) {
throw runtime.newIOErrorFromException(e);
}
}
public static long getPidFromProcess(Process process) {
if (process instanceof ScriptThreadProcess) {
return process.hashCode();
} else if (process instanceof POpenProcess) {
return reflectPidFromProcess(((POpenProcess)process).getChild());
} else {
return reflectPidFromProcess(process);
}
}
private static final Class UNIXProcess;
private static final Field UNIXProcess_pid;
private static final Class ProcessImpl;
private static final Field ProcessImpl_handle;
private interface PidGetter { long getPid(Process process); }
private static final PidGetter PID_GETTER;
static {
PidGetter pidGetter;
Class up = null;
Field pid = null;
try {
up = Class.forName("java.lang.UNIXProcess");
pid = up.getDeclaredField("pid");
pid.setAccessible(true);
} catch (Exception e) {
}
UNIXProcess = up;
UNIXProcess_pid = pid;
Class pi = null;
Field handle = null;
try {
pi = Class.forName("java.lang.ProcessImpl");
handle = pi.getDeclaredField("handle");
handle.setAccessible(true);
} catch (Exception e) {
}
ProcessImpl = pi;
ProcessImpl_handle = handle;
if (UNIXProcess_pid != null) {
if (ProcessImpl_handle != null) {
pidGetter = new PidGetter() {
public long getPid(Process process) {
try {
if (UNIXProcess.isInstance(process)) {
return (Integer)UNIXProcess_pid.get(process);
} else if (ProcessImpl.isInstance(process)) {
Long hproc = (Long) ProcessImpl_handle.get(process);
return WindowsFFI.getKernel32().GetProcessId(hproc);
}
} catch (Exception e) {
}
return process.hashCode();
}
};
} else {
pidGetter = new PidGetter() {
public long getPid(Process process) {
try {
if (UNIXProcess.isInstance(process)) {
return (Integer)UNIXProcess_pid.get(process);
}
} catch (Exception e) {
}
return process.hashCode();
}
};
}
} else if (ProcessImpl_handle != null) {
pidGetter = new PidGetter() {
public long getPid(Process process) {
try {
if (ProcessImpl.isInstance(process)) {
Long hproc = (Long) ProcessImpl_handle.get(process);
return WindowsFFI.getKernel32().GetProcessId(hproc);
}
} catch (Exception e) {
}
return process.hashCode();
}
};
} else {
pidGetter = new PidGetter() {
public long getPid(Process process) {
return process.hashCode();
}
};
}
PID_GETTER = pidGetter;
}
public static long reflectPidFromProcess(Process process) {
return PID_GETTER.getPid(process);
}
public static Process run(Ruby runtime, IRubyObject string) throws IOException {
return run(runtime, new IRubyObject[] {string}, false);
}
public static POpenProcess popen(Ruby runtime, IRubyObject string, ModeFlags modes) throws IOException {
return new POpenProcess(popenShared(runtime, new IRubyObject[] {string}, null, true), runtime, modes);
}
public static POpenProcess popen(Ruby runtime, IRubyObject[] strings, Map env, ModeFlags modes) throws IOException {
return new POpenProcess(popenShared(runtime, strings, env), runtime, modes);
}
public static POpenProcess popen(Ruby runtime, IRubyObject string, Map env, ModeFlags modes) throws IOException {
return new POpenProcess(popenShared(runtime, new IRubyObject[] {string}, env, true), runtime, modes);
}
@Deprecated
public static POpenProcess popen(Ruby runtime, IRubyObject string, IOOptions modes) throws IOException {
return new POpenProcess(popenShared(runtime, new IRubyObject[] {string}, null, true), runtime, modes);
}
@Deprecated
public static POpenProcess popen(Ruby runtime, IRubyObject[] strings, Map env, IOOptions modes) throws IOException {
return new POpenProcess(popenShared(runtime, strings, env), runtime, modes);
}
@Deprecated
public static POpenProcess popen3(Ruby runtime, IRubyObject[] strings) throws IOException {
return new POpenProcess(popenShared(runtime, strings));
}
@Deprecated
public static POpenProcess popen3(Ruby runtime, IRubyObject[] strings, boolean addShell) throws IOException {
return new POpenProcess(popenShared(runtime, strings, null, addShell));
}
private static Process popenShared(Ruby runtime, IRubyObject[] strings) throws IOException {
return popenShared(runtime, strings, null);
}
private static Process popenShared(Ruby runtime, IRubyObject[] strings, Map env) throws IOException {
return popenShared(runtime, strings, env, false);
}
private static Process popenShared(Ruby runtime, IRubyObject[] strings, Map env, boolean addShell) throws IOException {
String shell = getShell(runtime);
Process childProcess;
File pwd = new File(runtime.getCurrentDirectory());
try {
IRubyObject envHash;
if (env == null && strings.length > 0 && !(envHash = TypeConverter.checkHashType(runtime, strings[0])).isNil()) {
strings = Arrays.copyOfRange(strings, 1, strings.length);
env = (Map) envHash;
}
if (strings.length > 1 && !(envHash = TypeConverter.checkHashType(runtime, strings[strings.length - 1])).isNil()) {
if (!((RubyHash)envHash).isEmpty()) {
runtime.getWarnings().warn("popen3 does not support spawn options in JRuby 1.7");
}
strings = Arrays.copyOfRange(strings, 0, strings.length - 1);
}
String[] args = parseCommandLine(runtime.getCurrentContext(), runtime, strings);
LaunchConfig lc = new LaunchConfig(runtime, strings, false);
boolean useShell = Platform.IS_WINDOWS ? lc.shouldRunInShell() : false;
if (addShell) for (String arg : args) useShell |= shouldUseShell(arg);
if (strings.length == 1) {
if (useShell) {
String[] argArray = new String[3];
argArray[0] = shell;
argArray[1] = shell.endsWith("sh") ? "-c" : "/c";
argArray[2] = strings[0].asJavaString();
childProcess = buildProcess(runtime, argArray, getCurrentEnv(runtime, env), pwd);
} else {
childProcess = buildProcess(runtime, args, getCurrentEnv(runtime, env), pwd);
}
} else {
if (useShell) {
String[] argArray = new String[args.length + 2];
argArray[0] = shell;
argArray[1] = shell.endsWith("sh") ? "-c" : "/c";
System.arraycopy(args, 0, argArray, 2, args.length);
childProcess = buildProcess(runtime, argArray, getCurrentEnv(runtime, env), pwd);
} else {
childProcess = buildProcess(runtime, args, getCurrentEnv(runtime, env), pwd);
}
}
} catch (SecurityException se) {
throw runtime.newSecurityError(se.getLocalizedMessage());
}
return childProcess;
}
public static class POpenProcess extends Process {
private final Process child;
private final boolean waitForChild;
private InputStream realInput;
private OutputStream realOutput;
private InputStream realInerr;
private InputStream input;
private OutputStream output;
private InputStream inerr;
private FileChannel inputChannel;
private FileChannel outputChannel;
private FileChannel inerrChannel;
private Pumper inputPumper;
private Pumper inerrPumper;
@Deprecated
public POpenProcess(Process child, Ruby runtime, IOOptions modes) {
this(child, runtime, modes.getModeFlags());
}
public POpenProcess(Process child, Ruby runtime, ModeFlags modes) {
this.child = child;
if (modes.isWritable()) {
this.waitForChild = true;
prepareOutput(child);
} else {
this.waitForChild = false;
try {child.getOutputStream().close();} catch (IOException ioe) {}
}
if (modes.isReadable()) {
prepareInput(child);
} else {
pumpInput(child, runtime);
}
pumpInerr(child, runtime);
}
public POpenProcess(Process child) {
this.child = child;
this.waitForChild = false;
prepareOutput(child);
prepareInput(child);
prepareInerr(child);
}
@Override
public OutputStream getOutputStream() {
return output;
}
@Override
public InputStream getInputStream() {
return input;
}
@Override
public InputStream getErrorStream() {
return inerr;
}
public FileChannel getInput() {
return inputChannel;
}
public FileChannel getOutput() {
return outputChannel;
}
public FileChannel getError() {
return inerrChannel;
}
public boolean hasOutput() {
return output != null || outputChannel != null;
}
public Process getChild() {
return child;
}
@Override
public int waitFor() throws InterruptedException {
return child.waitFor();
}
@Override
public int exitValue() {
return child.exitValue();
}
@Override
public void destroy() {
try {
try {if (input != null) input.close();} catch (Exception e) {}
try {if (inerr != null) inerr.close();} catch (Exception e) {}
try {if (output != null) output.close();} catch (Exception e) {}
try {if (inputChannel != null) inputChannel.close();} catch (Exception e) {}
try {if (inerrChannel != null) inerrChannel.close();} catch (Exception e) {}
try {if (outputChannel != null) outputChannel.close();} catch (Exception e) {}
synchronized (this) {
if (inputPumper != null) synchronized(inputPumper) {inputPumper.quit();}
if (inerrPumper != null) synchronized(inerrPumper) {inerrPumper.quit();}
if (waitForChild) {
waitFor();
} else {
RubyIO.obliterateProcess(child);
}
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
private void prepareInput(Process child) {
realInput = child.getInputStream();
input = realInput;
inputChannel = null;
inputPumper = null;
}
private void prepareInerr(Process child) {
realInerr = child.getErrorStream();
inerr = realInerr;
inerrChannel = null;
inerrPumper = null;
}
private void prepareOutput(Process child) {
realOutput = child.getOutputStream();
output = ChannelHelper.unwrapBufferedStream(realOutput);
if (output instanceof FileOutputStream) {
outputChannel = ((FileOutputStream) output).getChannel();
} else {
outputChannel = null;
}
}
private void pumpInput(Process child, Ruby runtime) {
InputStream childIn = ChannelHelper.unwrapBufferedStream(child.getInputStream());
FileChannel childInChannel = null;
if (childIn instanceof FileInputStream) {
childInChannel = ((FileInputStream) childIn).getChannel();
}
OutputStream parentOut = ChannelHelper.unwrapBufferedStream(runtime.getOut());
FileChannel parentOutChannel = null;
if (parentOut instanceof FileOutputStream) {
parentOutChannel = ((FileOutputStream) parentOut).getChannel();
}
if (childInChannel != null && parentOutChannel != null) {
inputPumper = new ChannelPumper(runtime, childInChannel, parentOutChannel, Pumper.Slave.IN, this);
} else {
inputPumper = new StreamPumper(runtime, childIn, parentOut, false, Pumper.Slave.IN, this);
}
inputPumper.start();
input = null;
inputChannel = null;
}
private void pumpInerr(Process child, Ruby runtime) {
InputStream childIn = ChannelHelper.unwrapBufferedStream(child.getErrorStream());
FileChannel childInChannel = null;
if (childIn instanceof FileInputStream) {
childInChannel = ((FileInputStream) childIn).getChannel();
}
OutputStream parentOut = ChannelHelper.unwrapBufferedStream(runtime.getOut());
FileChannel parentOutChannel = null;
if (parentOut instanceof FileOutputStream) {
parentOutChannel = ((FileOutputStream) parentOut).getChannel();
}
if (childInChannel != null && parentOutChannel != null) {
inerrPumper = new ChannelPumper(runtime, childInChannel, parentOutChannel, Pumper.Slave.IN, this);
} else {
inerrPumper = new StreamPumper(runtime, childIn, parentOut, false, Pumper.Slave.IN, this);
}
inerrPumper.start();
inerr = null;
inerrChannel = null;
}
}
public static class LaunchConfig {
public LaunchConfig(Ruby runtime, IRubyObject[] rawArgs, boolean doExecutableSearch) {
this.runtime = runtime;
this.rawArgs = rawArgs;
this.doExecutableSearch = doExecutableSearch;
shell = getShell(runtime);
args = parseCommandLine(runtime.getCurrentContext(), runtime, rawArgs);
}
public boolean shouldRunInProcess() {
if (!runtime.getInstanceConfig().isRunRubyInProcess()
|| RubyInstanceConfig.hasLoadedNativeExtensions()) {
return false;
}
for (int i = 0; i < args.length; i++) {
String c = args[i];
if (c.trim().length() == 0) continue;
char[] firstLast = new char[] {c.charAt(0), c.charAt(c.length()-1)};
for (int j = 0; j < firstLast.length; j++) {
switch (firstLast[j]) {
case '<': case '>': case '|': case ';': case '(': case ')':
case '~': case '&': case '$': case '"': case '`': case '\n':
case '\\': case '\'':
return false;
case '2':
if(c.length() > 1 && c.charAt(1) == '>') return false;
}
}
}
String command = args[0];
if (Platform.IS_WINDOWS) command = command.toLowerCase();
String[] slashDelimitedTokens = command.split("[/\\\\]");
String finalToken = slashDelimitedTokens[slashDelimitedTokens.length - 1];
boolean inProc = (finalToken.endsWith("ruby")
|| (Platform.IS_WINDOWS && finalToken.endsWith("ruby.exe"))
|| finalToken.endsWith(".rb")
|| finalToken.endsWith("irb"));
if (!inProc) return false;
int startIndex = command.endsWith(".rb") ? 0 : 1;
if (command.trim().endsWith("irb")) {
startIndex = 0;
args[0] = runtime.getJRubyHome() + File.separator + "bin" + File.separator + "jirb";
}
execArgs = new String[args.length - startIndex];
System.arraycopy(args, startIndex, execArgs, 0, execArgs.length);
return true;
}
public boolean shouldRunInShell() {
if (rawArgs.length != 1) {
return false;
}
if (!Platform.IS_WINDOWS) return true;
if (shell == null) return false;
for (String arg : args) {
if (!shouldVerifyPathExecutable(arg.trim())) {
return true;
}
}
executable = args[0].trim();
executableFile = findPathExecutable(runtime, executable);
if (executableFile != null) {
log(runtime, "Got it: " + executableFile);
if (isBatch(executableFile)) {
log(runtime, "This is a BAT/CMD file, will start in shell");
return true;
}
return false;
} else {
log(runtime, "Didn't find executable: " + executable);
}
if (isCmdBuiltin(executable)) {
cmdBuiltin = true;
return true;
}
return false;
}
private boolean isBatch(File executableFile) {
return executableFile.getName().toLowerCase().endsWith(".bat");
}
public void verifyExecutableForShell() {
String cmdline = rawArgs[0].toString().trim();
if (doExecutableSearch && shouldVerifyPathExecutable(cmdline) && !cmdBuiltin) {
verifyExecutable();
}
execArgs = new String[3];
execArgs[0] = shell;
execArgs[1] = shell.endsWith("sh") ? "-c" : "/c";
if (Platform.IS_WINDOWS) {
execArgs[2] = "\"" + cmdline + "\"";
} else {
execArgs[2] = cmdline;
}
}
public void verifyExecutableForDirect() {
if (isCmdBuiltin(args[0].trim())) {
execArgs = new String[args.length + 2];
execArgs[0] = shell;
execArgs[1] = "/c";
execArgs[2] = args[0].trim();
System.arraycopy(args, 1, execArgs, 3, args.length - 1);
} else {
verifyExecutable();
execArgs = args;
execArgs[0] = executableFile.getAbsolutePath();
}
}
private void verifyExecutable() {
if (executableFile == null) {
if (executable == null) {
executable = args[0].trim();
}
executableFile = findPathExecutable(runtime, executable);
}
if (executableFile == null) {
throw runtime.newErrnoENOENTError(executable);
}
}
public String[] getExecArgs() {
return execArgs;
}
private boolean isCmdBuiltin(String cmd) {
if (!shell.endsWith("sh")) {
int idx = Arrays.binarySearch(WINDOWS_INTERNAL_CMDS, cmd.toLowerCase());
if (idx >= 0) {
log(runtime, "Found Windows shell's built-in command: " + cmd);
return true;
}
}
return false;
}
private static boolean hasRedirection(String cmdline) {
if (Platform.IS_WINDOWS) {
char quote = '\0';
for (int idx = 0; idx < cmdline.length();) {
char ptr = cmdline.charAt(idx);
switch (ptr) {
case '\'':
case '\"':
if (quote == '\0') {
quote = ptr;
} else if (quote == ptr) {
quote = '\0';
}
idx++;
break;
case '>':
case '<':
case '|':
case '\n':
if (quote == '\0') {
return true;
}
idx++;
break;
case '%':
Matcher envVarMatcher = WIN_ENVVAR_PATTERN.matcher(cmdline.substring(idx));
if (envVarMatcher.find()) {
return true;
} else {
idx++;
}
break;
case '\\':
idx++;
default:
idx++;
break;
}
}
return false;
} else {
Matcher metaMatcher = SHELL_METACHARACTER_PATTERN.matcher(cmdline);
return metaMatcher.find();
}
}
private static boolean shouldVerifyPathExecutable(String cmdline) {
boolean verifyPathExecutable = true;
if (hasRedirection(cmdline)) {
return false;
}
return verifyPathExecutable;
}
private Ruby runtime;
private boolean doExecutableSearch;
private IRubyObject[] rawArgs;
private String shell;
private String[] args;
private String[] execArgs;
private boolean cmdBuiltin = false;
private String executable;
private File executableFile;
}
public static Process run(Ruby runtime, IRubyObject[] rawArgs, boolean doExecutableSearch) throws IOException {
return run(runtime, rawArgs, doExecutableSearch, false);
}
private static boolean hasGlobCharacter(String word) {
return word.contains("*") || word.contains("?") || word.contains("[") || word.contains("{");
}
private static String[] expandGlobs(Ruby runtime, String[] originalArgs) {
List<String> expandedList = new ArrayList<String>(originalArgs.length);
for (int i = 0; i < originalArgs.length; i++) {
if (hasGlobCharacter(originalArgs[i])) {
List<ByteList> globs = Dir.push_glob(runtime, runtime.getCurrentDirectory(),
new ByteList(originalArgs[i].getBytes()), 0);
for (ByteList glob: globs) {
expandedList.add(glob.toString());
}
} else {
expandedList.add(originalArgs[i]);
}
}
String[] args = new String[expandedList.size()];
expandedList.toArray(args);
return args;
}
public static Process run(Ruby runtime, IRubyObject[] rawArgs, boolean doExecutableSearch, boolean forceExternalProcess) throws IOException {
Process aProcess;
String virtualCWD = runtime.getCurrentDirectory();
File pwd = new File(virtualCWD);
LaunchConfig cfg = new LaunchConfig(runtime, rawArgs, doExecutableSearch);
try {
if (!forceExternalProcess && cfg.shouldRunInProcess()) {
log(runtime, "Launching in-process");
ScriptThreadProcess ipScript = new ScriptThreadProcess(runtime,
expandGlobs(runtime, cfg.getExecArgs()), getCurrentEnv(runtime), pwd);
ipScript.start();
return ipScript;
} else {
if (cfg.shouldRunInShell()) {
log(runtime, "Launching with shell");
cfg.verifyExecutableForShell();
} else {
log(runtime, "Launching directly (no shell)");
cfg.verifyExecutableForDirect();
}
String[] args = cfg.getExecArgs();
if (virtualCWD.startsWith("uri:classloader:")) {
pwd = new File(System.getProperty("user.dir"));
if (args[args.length - 1].contains("org.jruby.Main")) {
args[args.length - 1] = args[args.length - 1].replace("org.jruby.Main",
"org.jruby.Main -C " + virtualCWD);
}
}
aProcess = buildProcess(runtime, args, getCurrentEnv(runtime), pwd);
}
} catch (SecurityException se) {
throw runtime.newSecurityError(se.getLocalizedMessage());
}
return aProcess;
}
private interface Pumper extends Runnable {
enum Slave { IN, OUT }
void start();
void quit();
}
private static class StreamPumper extends Thread implements Pumper {
private final InputStream in;
private final OutputStream out;
private final boolean onlyIfAvailable;
private final Object waitLock = new Object();
private final Object sync;
private final Slave slave;
private volatile boolean quit;
private final Ruby runtime;
StreamPumper(Ruby runtime, InputStream in, OutputStream out, boolean avail, Slave slave, Object sync) {
this.in = ChannelHelper.unwrapBufferedStream(in);
this.out = ChannelHelper.unwrapBufferedStream(out);
this.onlyIfAvailable = avail;
this.slave = slave;
this.sync = sync;
this.runtime = runtime;
setDaemon(true);
}
@Override
public void run() {
runtime.getCurrentContext().setEventHooksEnabled(false);
byte[] buf = new byte[1024];
int numRead;
boolean hasReadSomething = false;
try {
while (!quit) {
if (onlyIfAvailable && !hasReadSomething) {
if (in.available() == 0) {
synchronized (waitLock) {
waitLock.wait(10);
}
continue;
} else {
hasReadSomething = true;
}
}
if ((numRead = in.read(buf)) == -1) {
break;
}
out.write(buf, 0, numRead);
}
} catch (Exception e) {
} finally {
if (onlyIfAvailable) {
synchronized (sync) {
if (slave == Slave.OUT) {
try { out.close(); } catch (IOException ioe) {}
}
}
}
}
}
public void quit() {
this.quit = true;
synchronized (waitLock) {
waitLock.notify();
}
interrupt();
}
}
private static class ChannelPumper extends Thread implements Pumper {
private final FileChannel inChannel;
private final FileChannel outChannel;
private final Slave slave;
private final Object sync;
private volatile boolean quit;
private final Ruby runtime;
ChannelPumper(Ruby runtime, FileChannel inChannel, FileChannel outChannel, Slave slave, Object sync) {
if (DEBUG) out.println("using channel pumper");
this.inChannel = inChannel;
this.outChannel = outChannel;
this.slave = slave;
this.sync = sync;
this.runtime = runtime;
setDaemon(true);
}
@Override
public void run() {
runtime.getCurrentContext().setEventHooksEnabled(false);
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
clearBuffer(buf);
try {
while (!quit && inChannel.isOpen() && outChannel.isOpen()) {
int read = inChannel.read(buf);
if (read == -1) break;
flipBuffer(buf);
outChannel.write(buf);
clearBuffer(buf);
}
} catch (Exception e) {
} finally {
synchronized (sync) {
switch (slave) {
case OUT:
try { outChannel.close(); } catch (IOException ioe) {}
break;
case IN:
try { inChannel.close(); } catch (IOException ioe) {}
}
}
}
}
public void quit() {
this.quit = true;
interrupt();
}
}
private static void handleStreams(Ruby runtime, Process p, InputStream in, OutputStream out, OutputStream err) throws IOException {
InputStream pOut = p.getInputStream();
InputStream pErr = p.getErrorStream();
OutputStream pIn = p.getOutputStream();
StreamPumper t1 = new StreamPumper(runtime, pOut, out, false, Pumper.Slave.IN, p);
StreamPumper t2 = new StreamPumper(runtime, pErr, err, false, Pumper.Slave.IN, p);
StreamPumper t3 = new StreamPumper(runtime, in, pIn, true, Pumper.Slave.OUT, p);
t1.start();
t2.start();
t3.start();
try { t1.join(); } catch (InterruptedException ie) {}
try { t2.join(); } catch (InterruptedException ie) {}
t3.quit();
try { err.flush(); } catch (IOException io) {}
try { out.flush(); } catch (IOException io) {}
try { pIn.close(); } catch (IOException io) {}
try { pOut.close(); } catch (IOException io) {}
try { pErr.close(); } catch (IOException io) {}
try {
t1.quit();
t2.quit();
t3.quit();
t1.interrupt();
t2.interrupt();
t3.interrupt();
} catch (SecurityException se) {}
try { t1.join(); } catch (InterruptedException ie) {}
try { t2.join(); } catch (InterruptedException ie) {}
try { t3.join(); } catch (InterruptedException ie) {}
}
private static void handleStreamsNonblocking(Ruby runtime, Process p, OutputStream out, OutputStream err) throws IOException {
InputStream pOut = p.getInputStream();
InputStream pErr = p.getErrorStream();
StreamPumper t1 = new StreamPumper(runtime, pOut, out, false, Pumper.Slave.IN, p);
StreamPumper t2 = new StreamPumper(runtime, pErr, err, false, Pumper.Slave.IN, p);
t1.start();
t2.start();
}
private static String[] parseCommandLine(ThreadContext context, Ruby runtime, IRubyObject[] rawArgs) {
String[] args;
if (rawArgs.length == 1) {
if (hasLeadingArgvArray(rawArgs)) {
args = new String[] { getPathEntry((RubyArray) rawArgs[0]) };
} else {
synchronized (runtime.getLoadService()) {
runtime.getLoadService().require("jruby/path_helper");
}
RubyModule pathHelper = runtime.getClassFromPath("JRuby::PathHelper");
RubyArray parts = (RubyArray) Helpers.invoke(
context, pathHelper, "smart_split_command", rawArgs);
args = new String[parts.getLength()];
for (int i = 0; i < parts.getLength(); i++) {
args[i] = parts.entry(i).toString();
}
}
} else {
args = new String[rawArgs.length];
int start = 0;
if (hasLeadingArgvArray(rawArgs)) {
start = 1;
args[0] = getPathEntry((RubyArray) rawArgs[0]);
}
for (int i = start; i < rawArgs.length; i++) {
args[i] = rawArgs[i].toString();
}
}
return args;
}
private static boolean hasLeadingArgvArray(IRubyObject[] rawArgs) {
return (rawArgs.length >= 1
&& (rawArgs[0] instanceof RubyArray)
&& (((RubyArray) rawArgs[0]).getLength() == 2));
}
private static String getPathEntry(RubyArray initArray) {
return initArray.entry(0).toString();
}
private static String getShell(Ruby runtime) {
return RbConfigLibrary.jrubyShell();
}
public static boolean shouldUseShell(String command) {
boolean useShell = false;
for (char c : command.toCharArray()) {
if (c != ' ' && !Character.isLetter(c) && "*?{}[]<>()~&|\\$;'`\"\n".indexOf(c) != -1) {
useShell = true;
}
}
if (Platform.IS_WINDOWS && command.length() >= 1 && command.charAt(0) == '@') {
useShell = true;
}
return useShell;
}
static void log(Ruby runtime, String msg) {
if (RubyInstanceConfig.DEBUG_LAUNCHING) {
runtime.getErr().println("ShellLauncher: " + msg);
}
}
@Deprecated
public static OutputStream unwrapBufferedStream(OutputStream filteredStream) {
return ChannelHelper.unwrapBufferedStream(filteredStream);
}
@Deprecated
public static InputStream unwrapBufferedStream(InputStream filteredStream) {
return ChannelHelper.unwrapBufferedStream(filteredStream);
}
@Deprecated
public static OutputStream unwrapFilterOutputStream(OutputStream filteredStream) {
return ChannelHelper.unwrapFilterOutputStream(filteredStream);
}
@Deprecated
public static InputStream unwrapFilterInputStream(InputStream filteredStream) {
return ChannelHelper.unwrapFilterInputStream(filteredStream);
}
}