001: /***** BEGIN LICENSE BLOCK *****
002: * Version: CPL 1.0/GPL 2.0/LGPL 2.1
003: *
004: * The contents of this file are subject to the Common Public
005: * License Version 1.0 (the "License"); you may not use this file
006: * except in compliance with the License. You may obtain a copy of
007: * the License at http://www.eclipse.org/legal/cpl-v10.html
008: *
009: * Software distributed under the License is distributed on an "AS
010: * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
011: * implied. See the License for the specific language governing
012: * rights and limitations under the License.
013: *
014: * Copyright (C) 2007 Nick Sieger <nicksieger@gmail.com>
015: *
016: * Alternatively, the contents of this file may be used under the terms of
017: * either of the GNU General Public License Version 2 or later (the "GPL"),
018: * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
019: * in which case the provisions of the GPL or the LGPL are applicable instead
020: * of those above. If you wish to allow use of your version of this file only
021: * under the terms of either the GPL or the LGPL, and not to allow others to
022: * use your version of this file under the terms of the CPL, indicate your
023: * decision by deleting the provisions above and replace them with the notice
024: * and other provisions required by the GPL or the LGPL. If you do not delete
025: * the provisions above, a recipient may use your version of this file under
026: * the terms of any one of the CPL, the GPL or the LGPL.
027: ***** END LICENSE BLOCK *****/package org.jruby.util;
028:
029: import java.io.File;
030: import java.io.IOException;
031: import java.io.InputStream;
032: import java.io.OutputStream;
033: import java.io.PipedInputStream;
034: import java.io.PipedOutputStream;
035: import java.io.PrintStream;
036: import java.util.HashMap;
037: import java.util.Iterator;
038: import java.util.List;
039: import java.util.Map;
040: import java.util.Stack;
041: import java.util.StringTokenizer;
042: import java.util.regex.Pattern;
043:
044: import org.jruby.Main;
045: import org.jruby.Ruby;
046: import org.jruby.RubyHash;
047: import org.jruby.RubyInstanceConfig;
048: import org.jruby.runtime.builtin.IRubyObject;
049:
050: /**
051: *
052: * @author nicksieger
053: */
054: public class ShellLauncher {
055:
056: private static final Pattern PATH_SEPARATORS = Pattern
057: .compile("[/\\\\]");
058:
059: private Ruby runtime;
060:
061: /** Creates a new instance of ShellLauncher */
062: public ShellLauncher(Ruby runtime) {
063: this .runtime = runtime;
064: }
065:
066: private static class ScriptThreadProcess extends Process implements
067: Runnable {
068: private String[] argArray;
069: private int result;
070: private RubyInstanceConfig config;
071: private Thread processThread;
072: private PipedInputStream processOutput = new PipedInputStream();
073: private PipedInputStream processError = new PipedInputStream();
074: private PipedOutputStream processInput = new PipedOutputStream();
075: private final String[] env;
076: private final File pwd;
077:
078: public ScriptThreadProcess(final String[] argArray,
079: final String[] env, final File dir) {
080: this .argArray = argArray;
081: this .env = env;
082: this .pwd = dir;
083: }
084:
085: public void run() {
086: this .result = new Main(config).run(argArray);
087: this .config.getOutput().close();
088: this .config.getError().close();
089: }
090:
091: private Map environmentMap(String[] env) {
092: Map m = new HashMap();
093: for (int i = 0; i < env.length; i++) {
094: String[] kv = env[i].split("=", 2);
095: m.put(kv[0], kv[1]);
096: }
097: return m;
098: }
099:
100: public void start() throws IOException {
101: this .config = new RubyInstanceConfig() {
102: {
103: setInput(new PipedInputStream(processInput));
104: setOutput(new PrintStream(new PipedOutputStream(
105: processOutput)));
106: setError(new PrintStream(new PipedOutputStream(
107: processError)));
108: setEnvironment(environmentMap(env));
109: setCurrentDirectory(pwd.toString());
110: }
111: };
112: processThread = new Thread(this , "ScriptThreadProcess: "
113: + argArray[0]);
114: processThread.start();
115: }
116:
117: public OutputStream getOutputStream() {
118: return processInput;
119: }
120:
121: public InputStream getInputStream() {
122: return processOutput;
123: }
124:
125: public InputStream getErrorStream() {
126: return processError;
127: }
128:
129: public int waitFor() throws InterruptedException {
130: closeStreams();
131: processThread.join();
132: return result;
133: }
134:
135: public int exitValue() {
136: return result;
137: }
138:
139: public void destroy() {
140: closeStreams();
141: processThread.interrupt();
142: }
143:
144: private void closeStreams() {
145: try {
146: processInput.close();
147: } catch (IOException io) {
148: }
149: try {
150: processOutput.close();
151: } catch (IOException io) {
152: }
153: try {
154: processError.close();
155: } catch (IOException io) {
156: }
157: }
158: }
159:
160: private String[] getCurrentEnv() {
161: RubyHash hash = (RubyHash) runtime.getObject().getConstant(
162: "ENV");
163: String[] ret = new String[hash.size()];
164: int i = 0;
165:
166: for (Iterator iter = hash.directEntrySet().iterator(); iter
167: .hasNext(); i++) {
168: Map.Entry e = (Map.Entry) iter.next();
169: ret[i] = e.getKey().toString() + "="
170: + e.getValue().toString();
171: }
172:
173: return ret;
174: }
175:
176: public int runAndWait(IRubyObject[] rawArgs) {
177: return runAndWait(rawArgs, runtime.getOutputStream());
178: }
179:
180: public int runAndWait(IRubyObject[] rawArgs, OutputStream output) {
181: OutputStream error = runtime.getErrorStream();
182: InputStream input = runtime.getInputStream();
183: try {
184: Process aProcess = run(rawArgs);
185: handleStreams(aProcess, input, output, error);
186: return aProcess.waitFor();
187: } catch (IOException e) {
188: throw runtime.newIOErrorFromException(e);
189: } catch (InterruptedException e) {
190: throw runtime.newThreadError("unexpected interrupt");
191: }
192: }
193:
194: public Process run(IRubyObject string) throws IOException {
195: return run(new IRubyObject[] { string });
196: }
197:
198: public Process run(IRubyObject[] rawArgs) throws IOException {
199: String shell = runtime.evalScript(
200: "require 'rbconfig'; Config::CONFIG['SHELL']")
201: .toString();
202: rawArgs[0] = runtime.newString(repairDirSeps(rawArgs[0]
203: .toString()));
204: Process aProcess = null;
205: File pwd = new File(runtime.getCurrentDirectory());
206:
207: if (shouldRunInProcess(rawArgs[0].toString())) {
208: List args = parseCommandLine(rawArgs);
209: String command = (String) args.get(0);
210:
211: // snip off ruby or jruby command from list of arguments
212: // leave alone if the command is the name of a script
213: int startIndex = command.endsWith(".rb") ? 0 : 1;
214: if (command.trim().endsWith("irb")) {
215: startIndex = 0;
216: args.set(0, runtime.getJRubyHome() + File.separator
217: + "bin" + File.separator + "jirb");
218: }
219: String[] argArray = (String[]) args.subList(startIndex,
220: args.size()).toArray(new String[0]);
221: ScriptThreadProcess ipScript = new ScriptThreadProcess(
222: argArray, getCurrentEnv(), pwd);
223: ipScript.start();
224: aProcess = ipScript;
225: } else if (shouldRunInShell(shell, rawArgs)) {
226: // execute command with sh -c or cmd.exe /c
227: // this does shell expansion of wildcards
228: String shellSwitch = shell.endsWith("sh") ? "-c" : "/c";
229: String[] argArray = new String[3];
230: argArray[0] = shell;
231: argArray[1] = shellSwitch;
232: argArray[2] = rawArgs[0].toString();
233: aProcess = Runtime.getRuntime().exec(argArray,
234: getCurrentEnv(), pwd);
235: } else {
236: // execute command directly, no wildcard expansion
237: if (rawArgs.length > 1) {
238: String[] argArray = new String[rawArgs.length];
239: for (int i = 0; i < rawArgs.length; i++) {
240: argArray[i] = rawArgs[i].toString();
241: }
242: aProcess = Runtime.getRuntime().exec(argArray,
243: getCurrentEnv(), pwd);
244: } else {
245: aProcess = Runtime.getRuntime().exec(
246: rawArgs[0].toString(), getCurrentEnv(), pwd);
247: }
248: }
249: return aProcess;
250: }
251:
252: private static class StreamCopier extends Thread {
253: private InputStream in;
254: private OutputStream out;
255: private boolean onlyIfAvailable;
256: private boolean quit;
257:
258: StreamCopier(InputStream in, OutputStream out, boolean avail) {
259: this .in = in;
260: this .out = out;
261: this .onlyIfAvailable = avail;
262: }
263:
264: public void run() {
265: byte[] buf = new byte[128];
266: int numRead;
267: try {
268: while (true) {
269: if (quit) {
270: break;
271: }
272: Thread.sleep(10);
273: if (onlyIfAvailable && in.available() == 0) {
274: continue;
275: }
276: if ((numRead = in.read(buf)) == -1) {
277: break;
278: }
279: out.write(buf, 0, numRead);
280: }
281: } catch (Exception e) {
282: }
283: }
284:
285: public void quit() {
286: this .quit = true;
287: }
288: }
289:
290: private void handleStreams(Process p, InputStream in,
291: OutputStream out, OutputStream err) throws IOException {
292: InputStream pOut = p.getInputStream();
293: InputStream pErr = p.getErrorStream();
294: OutputStream pIn = p.getOutputStream();
295:
296: StreamCopier t1 = new StreamCopier(pOut, out, false);
297: StreamCopier t2 = new StreamCopier(pErr, err, false);
298: StreamCopier t3 = new StreamCopier(in, pIn, true);
299: t1.start();
300: t2.start();
301: t3.start();
302:
303: try {
304: t1.join();
305: } catch (InterruptedException ie) {
306: }
307: try {
308: t2.join();
309: } catch (InterruptedException ie) {
310: }
311: t3.quit();
312:
313: try {
314: err.flush();
315: } catch (IOException io) {
316: }
317: try {
318: out.flush();
319: } catch (IOException io) {
320: }
321:
322: try {
323: pIn.close();
324: } catch (IOException io) {
325: }
326: try {
327: pOut.close();
328: } catch (IOException io) {
329: }
330: try {
331: pErr.close();
332: } catch (IOException io) {
333: }
334:
335: }
336:
337: /**
338: * For the first full token on the command, most likely the actual executable to run, replace
339: * all dir separators with that which is appropriate for the current platform. Return the new
340: * with this executable string at the beginning.
341: *
342: * @param command The all-forward-slashes command to be "fixed"
343: * @return The "fixed" full command line
344: */
345: private String repairDirSeps(String command) {
346: String executable = "", remainder = "";
347: command = command.trim();
348: if (command.startsWith("'")) {
349: String[] tokens = command.split("'", 3);
350: executable = "'" + tokens[1] + "'";
351: if (tokens.length > 2)
352: remainder = tokens[2];
353: } else if (command.startsWith("\"")) {
354: String[] tokens = command.split("\"", 3);
355: executable = "\"" + tokens[1] + "\"";
356: if (tokens.length > 2)
357: remainder = tokens[2];
358: } else {
359: String[] tokens = command.split(" ", 2);
360: executable = tokens[0];
361: if (tokens.length > 1)
362: remainder = " " + tokens[1];
363: }
364:
365: // Matcher.replaceAll treats backslashes in the replacement string as escaped characters
366: String replacement = File.separator;
367: if (File.separatorChar == '\\')
368: replacement = "\\\\";
369:
370: return PATH_SEPARATORS.matcher(executable).replaceAll(
371: replacement)
372: + remainder;
373: }
374:
375: private List parseCommandLine(IRubyObject[] rawArgs) {
376: String[] args = new String[rawArgs.length];
377: for (int i = 0; i < rawArgs.length; i++) {
378: args[i] = rawArgs[i].toString();
379: }
380: return new RawArgParser(args).getArgs();
381: }
382:
383: /**
384: * Only run an in-process script if the script name has "ruby", ".rb", or "irb" in the name
385: */
386: private boolean shouldRunInProcess(String command) {
387: command = command.trim();
388: String[] spaceDelimitedTokens = command.split(" ", 2);
389: String[] slashDelimitedTokens = spaceDelimitedTokens[0]
390: .split("/");
391: String finalToken = slashDelimitedTokens[slashDelimitedTokens.length - 1];
392: return (finalToken.indexOf("ruby") != -1
393: || finalToken.endsWith(".rb") || finalToken
394: .endsWith("irb"));
395: }
396:
397: private boolean shouldRunInShell(String shell, IRubyObject[] rawArgs) {
398: return shell != null && rawArgs.length == 1
399: && rawArgs[0].toString().indexOf(" ") >= 0;
400: }
401: }
|