1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19 package org.apache.tools.ant.taskdefs;
20
21 import java.io.BufferedReader;
22 import java.io.ByteArrayOutputStream;
23 import java.io.File;
24 import java.io.FileWriter;
25 import java.io.IOException;
26 import java.io.OutputStream;
27 import java.io.PrintWriter;
28 import java.io.StringReader;
29 import java.lang.reflect.InvocationTargetException;
30 import java.lang.reflect.Method;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.Vector;
34
35 import org.apache.tools.ant.BuildException;
36 import org.apache.tools.ant.MagicNames;
37 import org.apache.tools.ant.Project;
38 import org.apache.tools.ant.Task;
39 import org.apache.tools.ant.taskdefs.condition.Os;
40 import org.apache.tools.ant.types.Commandline;
41 import org.apache.tools.ant.util.FileUtils;
42 import org.apache.tools.ant.util.StringUtils;
43
44 /**
45 * Runs an external program.
46 *
47 * @since Ant 1.2
48 *
49 */
50 public class Execute {
51
52 private static final int ONE_SECOND = 1000;
53
54 /** Invalid exit code.
55 * set to {@link Integer#MAX_VALUE}
56 */
57 public static final int INVALID = Integer.MAX_VALUE;
58
59 private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
60
61 private String[] cmdl = null;
62 private String[] env = null;
63 private int exitValue = INVALID;
64 private ExecuteStreamHandler streamHandler;
65 private ExecuteWatchdog watchdog;
66 private File workingDirectory = null;
67 private Project project = null;
68 private boolean newEnvironment = false;
69 //TODO: nothing appears to read this but is set using a public setter.
70 private boolean spawn = false;
71
72
73 /** Controls whether the VM is used to launch commands, where possible. */
74 private boolean useVMLauncher = true;
75
76 private static String antWorkingDirectory = System.getProperty("user.dir");
77 private static CommandLauncher vmLauncher = null;
78 private static CommandLauncher shellLauncher = null;
79 private static Vector procEnvironment = null;
80
81 /** Used to destroy processes when the VM exits. */
82 private static ProcessDestroyer processDestroyer = new ProcessDestroyer();
83
84 /** Used for replacing env variables */
85 private static boolean environmentCaseInSensitive = false;
86
87 /*
88 * Builds a command launcher for the OS and JVM we are running under.
89 */
90 static {
91 // Try using a JDK 1.3 launcher
92 try {
93 if (!Os.isFamily("os/2")) {
94 vmLauncher = new Java13CommandLauncher();
95 }
96 } catch (NoSuchMethodException exc) {
97 // Ignore and keep trying
98 }
99 if (Os.isFamily("mac") && !Os.isFamily("unix")) {
100 // Mac
101 shellLauncher = new MacCommandLauncher(new CommandLauncher());
102 } else if (Os.isFamily("os/2")) {
103 // OS/2
104 shellLauncher = new OS2CommandLauncher(new CommandLauncher());
105 } else if (Os.isFamily("windows")) {
106 environmentCaseInSensitive = true;
107 CommandLauncher baseLauncher = new CommandLauncher();
108
109 if (!Os.isFamily("win9x")) {
110 // Windows XP/2000/NT
111 shellLauncher = new WinNTCommandLauncher(baseLauncher);
112 } else {
113 // Windows 98/95 - need to use an auxiliary script
114 shellLauncher
115 = new ScriptCommandLauncher("bin/antRun.bat", baseLauncher);
116 }
117 } else if (Os.isFamily("netware")) {
118
119 CommandLauncher baseLauncher = new CommandLauncher();
120
121 shellLauncher
122 = new PerlScriptCommandLauncher("bin/antRun.pl", baseLauncher);
123 } else if (Os.isFamily("openvms")) {
124 // OpenVMS
125 try {
126 shellLauncher = new VmsCommandLauncher();
127 } catch (NoSuchMethodException exc) {
128 // Ignore and keep trying
129 }
130 } else {
131 // Generic
132 shellLauncher = new ScriptCommandLauncher("bin/antRun",
133 new CommandLauncher());
134 }
135 }
136
137 /**
138 * Set whether or not you want the process to be spawned.
139 * Default is not spawned.
140 *
141 * @param spawn if true you do not want Ant
142 * to wait for the end of the process.
143 *
144 * @since Ant 1.6
145 */
146 public void setSpawn(boolean spawn) {
147 this.spawn = spawn;
148 }
149
150 /**
151 * Find the list of environment variables for this process.
152 *
153 * @return a vector containing the environment variables.
154 * The vector elements are strings formatted like variable = value.
155 */
156 public static synchronized Vector getProcEnvironment() {
157 if (procEnvironment != null) {
158 return procEnvironment;
159 }
160 procEnvironment = new Vector();
161 try {
162 ByteArrayOutputStream out = new ByteArrayOutputStream();
163 Execute exe = new Execute(new PumpStreamHandler(out));
164 exe.setCommandline(getProcEnvCommand());
165 // Make sure we do not recurse forever
166 exe.setNewenvironment(true);
167 int retval = exe.execute();
168 if (retval != 0) {
169 // Just try to use what we got
170 }
171 BufferedReader in =
172 new BufferedReader(new StringReader(toString(out)));
173
174 if (Os.isFamily("openvms")) {
175 procEnvironment = addVMSLogicals(procEnvironment, in);
176 return procEnvironment;
177 }
178 String var = null;
179 String line, lineSep = StringUtils.LINE_SEP;
180 while ((line = in.readLine()) != null) {
181 if (line.indexOf('=') == -1) {
182 // Chunk part of previous env var (UNIX env vars can
183 // contain embedded new lines).
184 if (var == null) {
185 var = lineSep + line;
186 } else {
187 var += lineSep + line;
188 }
189 } else {
190 // New env var...append the previous one if we have it.
191 if (var != null) {
192 procEnvironment.addElement(var);
193 }
194 var = line;
195 }
196 }
197 // Since we "look ahead" before adding, there's one last env var.
198 if (var != null) {
199 procEnvironment.addElement(var);
200 }
201 } catch (java.io.IOException exc) {
202 exc.printStackTrace();
203 // Just try to see how much we got
204 }
205 return procEnvironment;
206 }
207
208 /**
209 * This is the operation to get our environment.
210 * It is a notorious troublespot pre-Java1.5, and should be approached
211 * with extreme caution.
212 * @return
213 */
214 private static String[] getProcEnvCommand() {
215 if (Os.isFamily("os/2")) {
216 // OS/2 - use same mechanism as Windows 2000
217 return new String[] {"cmd", "/c", "set" };
218 } else if (Os.isFamily("windows")) {
219 // Determine if we're running under XP/2000/NT or 98/95
220 if (Os.isFamily("win9x")) {
221 // Windows 98/95
222 return new String[] {"command.com", "/c", "set" };
223 } else {
224 // Windows XP/2000/NT/2003
225 return new String[] {"cmd", "/c", "set" };
226 }
227 } else if (Os.isFamily("z/os") || Os.isFamily("unix")) {
228 // On most systems one could use: /bin/sh -c env
229
230 // Some systems have /bin/env, others /usr/bin/env, just try
231 String[] cmd = new String[1];
232 if (new File("/bin/env").canRead()) {
233 cmd[0] = "/bin/env";
234 } else if (new File("/usr/bin/env").canRead()) {
235 cmd[0] = "/usr/bin/env";
236 } else {
237 // rely on PATH
238 cmd[0] = "env";
239 }
240 return cmd;
241 } else if (Os.isFamily("netware") || Os.isFamily("os/400")) {
242 // rely on PATH
243 return new String[] {"env"};
244 } else if (Os.isFamily("openvms")) {
245 return new String[] {"show", "logical"};
246 } else {
247 // MAC OS 9 and previous
248 //TODO: I have no idea how to get it, someone must fix it
249 return null;
250 }
251 }
252
253 /**
254 * ByteArrayOutputStream#toString doesn't seem to work reliably on
255 * OS/390, at least not the way we use it in the execution
256 * context.
257 *
258 * @param bos the output stream that one wants to read.
259 * @return the output stream as a string, read with
260 * special encodings in the case of z/os and os/400.
261 *
262 * @since Ant 1.5
263 */
264 public static String toString(ByteArrayOutputStream bos) {
265 if (Os.isFamily("z/os")) {
266 try {
267 return bos.toString("Cp1047");
268 } catch (java.io.UnsupportedEncodingException e) {
269 //noop default encoding used
270 }
271 } else if (Os.isFamily("os/400")) {
272 try {
273 return bos.toString("Cp500");
274 } catch (java.io.UnsupportedEncodingException e) {
275 //noop default encoding used
276 }
277 }
278 return bos.toString();
279 }
280
281 /**
282 * Creates a new execute object using <code>PumpStreamHandler</code> for
283 * stream handling.
284 */
285 public Execute() {
286 this(new PumpStreamHandler(), null);
287 }
288
289 /**
290 * Creates a new execute object.
291 *
292 * @param streamHandler the stream handler used to handle the input and
293 * output streams of the subprocess.
294 */
295 public Execute(ExecuteStreamHandler streamHandler) {
296 this(streamHandler, null);
297 }
298
299 /**
300 * Creates a new execute object.
301 *
302 * @param streamHandler the stream handler used to handle the input and
303 * output streams of the subprocess.
304 * @param watchdog a watchdog for the subprocess or <code>null</code> to
305 * to disable a timeout for the subprocess.
306 */
307 public Execute(ExecuteStreamHandler streamHandler,
308 ExecuteWatchdog watchdog) {
309 setStreamHandler(streamHandler);
310 this.watchdog = watchdog;
311 //By default, use the shell launcher for VMS
312 //
313 if (Os.isFamily("openvms")) {
314 useVMLauncher = false;
315 }
316 }
317
318 /**
319 * Set the stream handler to use.
320 * @param streamHandler ExecuteStreamHandler.
321 * @since Ant 1.6
322 */
323 public void setStreamHandler(ExecuteStreamHandler streamHandler) {
324 this.streamHandler = streamHandler;
325 }
326
327 /**
328 * Returns the commandline used to create a subprocess.
329 *
330 * @return the commandline used to create a subprocess.
331 */
332 public String[] getCommandline() {
333 return cmdl;
334 }
335
336 /**
337 * Sets the commandline of the subprocess to launch.
338 *
339 * @param commandline the commandline of the subprocess to launch.
340 */
341 public void setCommandline(String[] commandline) {
342 cmdl = commandline;
343 }
344
345 /**
346 * Set whether to propagate the default environment or not.
347 *
348 * @param newenv whether to propagate the process environment.
349 */
350 public void setNewenvironment(boolean newenv) {
351 newEnvironment = newenv;
352 }
353
354 /**
355 * Returns the environment used to create a subprocess.
356 *
357 * @return the environment used to create a subprocess.
358 */
359 public String[] getEnvironment() {
360 return (env == null || newEnvironment)
361 ? env : patchEnvironment();
362 }
363
364 /**
365 * Sets the environment variables for the subprocess to launch.
366 *
367 * @param env array of Strings, each element of which has
368 * an environment variable settings in format <em>key=value</em>.
369 */
370 public void setEnvironment(String[] env) {
371 this.env = env;
372 }
373
374 /**
375 * Sets the working directory of the process to execute.
376 *
377 * <p>This is emulated using the antRun scripts unless the OS is
378 * Windows NT in which case a cmd.exe is spawned,
379 * or MRJ and setting user.dir works, or JDK 1.3 and there is
380 * official support in java.lang.Runtime.
381 *
382 * @param wd the working directory of the process.
383 */
384 public void setWorkingDirectory(File wd) {
385 workingDirectory =
386 (wd == null || wd.getAbsolutePath().equals(antWorkingDirectory))
387 ? null : wd;
388 }
389
390 /**
391 * Return the working directory.
392 * @return the directory as a File.
393 * @since Ant 1.7
394 */
395 public File getWorkingDirectory() {
396 return workingDirectory == null ? new File(antWorkingDirectory)
397 : workingDirectory;
398 }
399
400 /**
401 * Set the name of the antRun script using the project's value.
402 *
403 * @param project the current project.
404 *
405 * @throws BuildException not clear when it is going to throw an exception, but
406 * it is the method's signature.
407 */
408 public void setAntRun(Project project) throws BuildException {
409 this.project = project;
410 }
411
412 /**
413 * Launch this execution through the VM, where possible, rather than through
414 * the OS's shell. In some cases and operating systems using the shell will
415 * allow the shell to perform additional processing such as associating an
416 * executable with a script, etc.
417 *
418 * @param useVMLauncher true if exec should launch through the VM,
419 * false if the shell should be used to launch the
420 * command.
421 */
422 public void setVMLauncher(boolean useVMLauncher) {
423 this.useVMLauncher = useVMLauncher;
424 }
425
426 /**
427 * Creates a process that runs a command.
428 *
429 * @param project the Project, only used for logging purposes, may be null.
430 * @param command the command to run.
431 * @param env the environment for the command.
432 * @param dir the working directory for the command.
433 * @param useVM use the built-in exec command for JDK 1.3 if available.
434 * @return the process started.
435 * @throws IOException forwarded from the particular launcher used.
436 *
437 * @since Ant 1.5
438 */
439 public static Process launch(Project project, String[] command,
440 String[] env, File dir, boolean useVM)
441 throws IOException {
442 if (dir != null && !dir.exists()) {
443 throw new BuildException(dir + " doesn't exist.");
444 }
445 CommandLauncher launcher
446 = ((useVM && vmLauncher != null) ? vmLauncher : shellLauncher);
447 return launcher.exec(project, command, env, dir);
448 }
449
450 /**
451 * Runs a process defined by the command line and returns its exit status.
452 *
453 * @return the exit status of the subprocess or <code>INVALID</code>.
454 * @exception java.io.IOException The exception is thrown, if launching
455 * of the subprocess failed.
456 */
457 public int execute() throws IOException {
458 if (workingDirectory != null && !workingDirectory.exists()) {
459 throw new BuildException(workingDirectory + " doesn't exist.");
460 }
461 final Process process = launch(project, getCommandline(),
462 getEnvironment(), workingDirectory,
463 useVMLauncher);
464 try {
465 streamHandler.setProcessInputStream(process.getOutputStream());
466 streamHandler.setProcessOutputStream(process.getInputStream());
467 streamHandler.setProcessErrorStream(process.getErrorStream());
468 } catch (IOException e) {
469 process.destroy();
470 throw e;
471 }
472 streamHandler.start();
473
474 try {
475 // add the process to the list of those to destroy if the VM exits
476 //
477 processDestroyer.add(process);
478
479 if (watchdog != null) {
480 watchdog.start(process);
481 }
482 waitFor(process);
483
484 if (watchdog != null) {
485 watchdog.stop();
486 }
487 streamHandler.stop();
488 closeStreams(process);
489
490 if (watchdog != null) {
491 watchdog.checkException();
492 }
493 return getExitValue();
494 } catch (ThreadDeath t) {
495 // #31928: forcibly kill it before continuing.
496 process.destroy();
497 throw t;
498 } finally {
499 // remove the process to the list of those to destroy if
500 // the VM exits
501 //
502 processDestroyer.remove(process);
503 }
504 }
505
506 /**
507 * Starts a process defined by the command line.
508 * Ant will not wait for this process, nor log its output.
509 *
510 * @throws java.io.IOException The exception is thrown, if launching
511 * of the subprocess failed.
512 * @since Ant 1.6
513 */
514 public void spawn() throws IOException {
515 if (workingDirectory != null && !workingDirectory.exists()) {
516 throw new BuildException(workingDirectory + " doesn't exist.");
517 }
518 final Process process = launch(project, getCommandline(),
519 getEnvironment(), workingDirectory,
520 useVMLauncher);
521 if (Os.isFamily("windows")) {
522 try {
523 Thread.sleep(ONE_SECOND);
524 } catch (InterruptedException e) {
525 project.log("interruption in the sleep after having spawned a"
526 + " process", Project.MSG_VERBOSE);
527 }
528 }
529 OutputStream dummyOut = new OutputStream() {
530 public void write(int b) throws IOException {
531 }
532 };
533
534 ExecuteStreamHandler handler = new PumpStreamHandler(dummyOut);
535 handler.setProcessErrorStream(process.getErrorStream());
536 handler.setProcessOutputStream(process.getInputStream());
537 handler.start();
538 process.getOutputStream().close();
539
540 project.log("spawned process " + process.toString(),
541 Project.MSG_VERBOSE);
542 }
543
544 /**
545 * Wait for a given process.
546 *
547 * @param process the process one wants to wait for.
548 */
549 protected void waitFor(Process process) {
550 try {
551 process.waitFor();
552 setExitValue(process.exitValue());
553 } catch (InterruptedException e) {
554 process.destroy();
555 }
556 }
557
558 /**
559 * Set the exit value.
560 *
561 * @param value exit value of the process.
562 */
563 protected void setExitValue(int value) {
564 exitValue = value;
565 }
566
567 /**
568 * Query the exit value of the process.
569 * @return the exit value or Execute.INVALID if no exit value has
570 * been received.
571 */
572 public int getExitValue() {
573 return exitValue;
574 }
575
576 /**
577 * Checks whether <code>exitValue</code> signals a failure on the current
578 * system (OS specific).
579 *
580 * <p><b>Note</b> that this method relies on the conventions of
581 * the OS, it will return false results if the application you are
582 * running doesn't follow these conventions. One notable
583 * exception is the Java VM provided by HP for OpenVMS - it will
584 * return 0 if successful (like on any other platform), but this
585 * signals a failure on OpenVMS. So if you execute a new Java VM
586 * on OpenVMS, you cannot trust this method.</p>
587 *
588 * @param exitValue the exit value (return code) to be checked.
589 * @return <code>true</code> if <code>exitValue</code> signals a failure.
590 */
591 public static boolean isFailure(int exitValue) {
592 //on openvms even exit value signals failure;
593 // for other platforms nonzero exit value signals failure
594 return Os.isFamily("openvms")
595 ? (exitValue % 2 == 0) : (exitValue != 0);
596 }
597
598 /**
599 * Did this execute return in a failure.
600 * @see #isFailure(int)
601 * @return true if and only if the exit code is interpreted as a failure
602 * @since Ant1.7
603 */
604 public boolean isFailure() {
605 return isFailure(getExitValue());
606 }
607
608 /**
609 * Test for an untimely death of the process.
610 * @return true if a watchdog had to kill the process.
611 * @since Ant 1.5
612 */
613 public boolean killedProcess() {
614 return watchdog != null && watchdog.killedProcess();
615 }
616
617 /**
618 * Patch the current environment with the new values from the user.
619 * @return the patched environment.
620 */
621 private String[] patchEnvironment() {
622 // On OpenVMS Runtime#exec() doesn't support the environment array,
623 // so we only return the new values which then will be set in
624 // the generated DCL script, inheriting the parent process environment
625 if (Os.isFamily("openvms")) {
626 return env;
627 }
628 Vector osEnv = (Vector) getProcEnvironment().clone();
629 for (int i = 0; i < env.length; i++) {
630 String keyValue = env[i];
631 // Get key including "="
632 String key = keyValue.substring(0, keyValue.indexOf('=') + 1);
633 if (environmentCaseInSensitive) {
634 // Nb: using default locale as key is a env name
635 key = key.toLowerCase();
636 }
637 int size = osEnv.size();
638 // Find the key in the current enviroment copy
639 // and remove it.
640 for (int j = 0; j < size; j++) {
641 String osEnvItem = (String) osEnv.elementAt(j);
642 String convertedItem = environmentCaseInSensitive
643 ? osEnvItem.toLowerCase() : osEnvItem;
644 if (convertedItem.startsWith(key)) {
645 osEnv.removeElementAt(j);
646 if (environmentCaseInSensitive) {
647 // Use the original casiness of the key
648 keyValue = osEnvItem.substring(0, key.length())
649 + keyValue.substring(key.length());
650 }
651 break;
652 }
653 }
654 // Add the key to the enviromnent copy
655 osEnv.addElement(keyValue);
656 }
657 return (String[]) (osEnv.toArray(new String[osEnv.size()]));
658 }
659
660 /**
661 * A utility method that runs an external command. Writes the output and
662 * error streams of the command to the project log.
663 *
664 * @param task The task that the command is part of. Used for logging
665 * @param cmdline The command to execute.
666 *
667 * @throws BuildException if the command does not exit successfully.
668 */
669 public static void runCommand(Task task, String[] cmdline)
670 throws BuildException {
671 try {
672 task.log(Commandline.describeCommand(cmdline),
673 Project.MSG_VERBOSE);
674 Execute exe = new Execute(
675 new LogStreamHandler(task, Project.MSG_INFO, Project.MSG_ERR));
676 exe.setAntRun(task.getProject());
677 exe.setCommandline(cmdline);
678 int retval = exe.execute();
679 if (isFailure(retval)) {
680 throw new BuildException(cmdline[0]
681 + " failed with return code " + retval, task.getLocation());
682 }
683 } catch (java.io.IOException exc) {
684 throw new BuildException("Could not launch " + cmdline[0] + ": "
685 + exc, task.getLocation());
686 }
687 }
688
689 /**
690 * Close the streams belonging to the given Process.
691 * @param process the <code>Process</code>.
692 */
693 public static void closeStreams(Process process) {
694 FileUtils.close(process.getInputStream());
695 FileUtils.close(process.getOutputStream());
696 FileUtils.close(process.getErrorStream());
697 }
698
699 /**
700 * This method is VMS specific and used by getProcEnvironment().
701 *
702 * Parses VMS logicals from <code>in</code> and adds them to
703 * <code>environment</code>. <code>in</code> is expected to be the
704 * output of "SHOW LOGICAL". The method takes care of parsing the output
705 * correctly as well as making sure that a logical defined in multiple
706 * tables only gets added from the highest order table. Logicals with
707 * multiple equivalence names are mapped to a variable with multiple
708 * values separated by a comma (,).
709 */
710 private static Vector addVMSLogicals(Vector environment, BufferedReader in)
711 throws IOException {
712 HashMap logicals = new HashMap();
713 String logName = null, logValue = null, newLogName;
714 String line = null;
715 // CheckStyle:MagicNumber OFF
716 while ((line = in.readLine()) != null) {
717 // parse the VMS logicals into required format ("VAR=VAL[,VAL2]")
718 if (line.startsWith("\t=")) {
719 // further equivalence name of previous logical
720 if (logName != null) {
721 logValue += "," + line.substring(4, line.length() - 1);
722 }
723 } else if (line.startsWith(" \"")) {
724 // new logical?
725 if (logName != null) {
726 logicals.put(logName, logValue);
727 }
728 int eqIndex = line.indexOf('=');
729 newLogName = line.substring(3, eqIndex - 2);
730 if (logicals.containsKey(newLogName)) {
731 // already got this logical from a higher order table
732 logName = null;
733 } else {
734 logName = newLogName;
735 logValue = line.substring(eqIndex + 3, line.length() - 1);
736 }
737 }
738 }
739 // CheckStyle:MagicNumber ON
740 // Since we "look ahead" before adding, there's one last env var.
741 if (logName != null) {
742 logicals.put(logName, logValue);
743 }
744 for (Iterator i = logicals.keySet().iterator(); i.hasNext();) {
745 String logical = (String) i.next();
746 environment.add(logical + "=" + logicals.get(logical));
747 }
748 return environment;
749 }
750
751 /**
752 * A command launcher for a particular JVM/OS platform. This class is
753 * a general purpose command launcher which can only launch commands in
754 * the current working directory.
755 */
756 private static class CommandLauncher {
757 /**
758 * Launches the given command in a new process.
759 *
760 * @param project The project that the command is part of.
761 * @param cmd The command to execute.
762 * @param env The environment for the new process. If null,
763 * the environment of the current process is used.
764 * @return the created Process.
765 * @throws IOException if attempting to run a command in a
766 * specific directory.
767 */
768 public Process exec(Project project, String[] cmd, String[] env)
769 throws IOException {
770 if (project != null) {
771 project.log("Execute:CommandLauncher: "
772 + Commandline.describeCommand(cmd), Project.MSG_DEBUG);
773 }
774 return Runtime.getRuntime().exec(cmd, env);
775 }
776
777 /**
778 * Launches the given command in a new process, in the given working
779 * directory.
780 *
781 * @param project The project that the command is part of.
782 * @param cmd The command to execute.
783 * @param env The environment for the new process. If null,
784 * the environment of the current process is used.
785 * @param workingDir The directory to start the command in. If null,
786 * the current directory is used.
787 * @return the created Process.
788 * @throws IOException if trying to change directory.
789 */
790 public Process exec(Project project, String[] cmd, String[] env,
791 File workingDir) throws IOException {
792 if (workingDir == null) {
793 return exec(project, cmd, env);
794 }
795 throw new IOException("Cannot execute a process in different "
796 + "directory under this JVM");
797 }
798 }
799
800 /**
801 * A command launcher for JDK/JRE 1.3 (and higher). Uses the built-in
802 * Runtime.exec() command.
803 */
804 private static class Java13CommandLauncher extends CommandLauncher {
805 private Method myExecWithCWD;
806
807 public Java13CommandLauncher() throws NoSuchMethodException {
808 // Locate method Runtime.exec(String[] cmdarray,
809 // String[] envp, File dir)
810 myExecWithCWD = Runtime.class.getMethod("exec",
811 new Class[] {String[].class, String[].class, File.class});
812 }
813
814 /**
815 * Launches the given command in a new process, in the given working
816 * directory.
817 * @param project the Ant project.
818 * @param cmd the command line to execute as an array of strings.
819 * @param env the environment to set as an array of strings.
820 * @param workingDir the working directory where the command
821 * should run.
822 * @return the created Process.
823 * @throws IOException probably forwarded from Runtime#exec.
824 */
825 public Process exec(Project project, String[] cmd, String[] env,
826 File workingDir) throws IOException {
827 try {
828 if (project != null) {
829 project.log("Execute:Java13CommandLauncher: "
830 + Commandline.describeCommand(cmd), Project.MSG_DEBUG);
831 }
832 return (Process) myExecWithCWD.invoke(Runtime.getRuntime(),
833 /* the arguments: */ new Object[] {cmd, env, workingDir});
834 } catch (InvocationTargetException exc) {
835 Throwable realexc = exc.getTargetException();
836 if (realexc instanceof ThreadDeath) {
837 throw (ThreadDeath) realexc;
838 } else if (realexc instanceof IOException) {
839 throw (IOException) realexc;
840 } else {
841 throw new BuildException("Unable to execute command",
842 realexc);
843 }
844 } catch (Exception exc) {
845 // IllegalAccess, IllegalArgument, ClassCast
846 throw new BuildException("Unable to execute command", exc);
847 }
848 }
849 }
850
851 /**
852 * A command launcher that proxies another command launcher.
853 *
854 * Sub-classes override exec(args, env, workdir).
855 */
856 private static class CommandLauncherProxy extends CommandLauncher {
857 private CommandLauncher myLauncher;
858
859 CommandLauncherProxy(CommandLauncher launcher) {
860 myLauncher = launcher;
861 }
862
863 /**
864 * Launches the given command in a new process. Delegates this
865 * method to the proxied launcher.
866 * @param project the Ant project.
867 * @param cmd the command line to execute as an array of strings.
868 * @param env the environment to set as an array of strings.
869 * @return the created Process.
870 * @throws IOException forwarded from the exec method of the
871 * command launcher.
872 */
873 public Process exec(Project project, String[] cmd, String[] env)
874 throws IOException {
875 return myLauncher.exec(project, cmd, env);
876 }
877 }
878
879 /**
880 * A command launcher for OS/2 that uses 'cmd.exe' when launching
881 * commands in directories other than the current working
882 * directory.
883 *
884 * <p>Unlike Windows NT and friends, OS/2's cd doesn't support the
885 * /d switch to change drives and directories in one go.</p>
886 */
887 private static class OS2CommandLauncher extends CommandLauncherProxy {
888 OS2CommandLauncher(CommandLauncher launcher) {
889 super(launcher);
890 }
891
892 /**
893 * Launches the given command in a new process, in the given working
894 * directory.
895 * @param project the Ant project.
896 * @param cmd the command line to execute as an array of strings.
897 * @param env the environment to set as an array of strings.
898 * @param workingDir working directory where the command should run.
899 * @return the created Process.
900 * @throws IOException forwarded from the exec method of the
901 * command launcher.
902 */
903 public Process exec(Project project, String[] cmd, String[] env,
904 File workingDir) throws IOException {
905 File commandDir = workingDir;
906 if (workingDir == null) {
907 if (project != null) {
908 commandDir = project.getBaseDir();
909 } else {
910 return exec(project, cmd, env);
911 }
912 }
913 // Use cmd.exe to change to the specified drive and
914 // directory before running the command
915 final int preCmdLength = 7;
916 final String cmdDir = commandDir.getAbsolutePath();
917 String[] newcmd = new String[cmd.length + preCmdLength];
918 // CheckStyle:MagicNumber OFF - do not bother
919 newcmd[0] = "cmd";
920 newcmd[1] = "/c";
921 newcmd[2] = cmdDir.substring(0, 2);
922 newcmd[3] = "&&";
923 newcmd[4] = "cd";
924 newcmd[5] = cmdDir.substring(2);
925 newcmd[6] = "&&";
926 // CheckStyle:MagicNumber ON
927 System.arraycopy(cmd, 0, newcmd, preCmdLength, cmd.length);
928
929 return exec(project, newcmd, env);
930 }
931 }
932
933 /**
934 * A command launcher for Windows XP/2000/NT that uses 'cmd.exe' when
935 * launching commands in directories other than the current working
936 * directory.
937 */
938 private static class WinNTCommandLauncher extends CommandLauncherProxy {
939 WinNTCommandLauncher(CommandLauncher launcher) {
940 super(launcher);
941 }
942
943 /**
944 * Launches the given command in a new process, in the given working
945 * directory.
946 * @param project the Ant project.
947 * @param cmd the command line to execute as an array of strings.
948 * @param env the environment to set as an array of strings.
949 * @param workingDir working directory where the command should run.
950 * @return the created Process.
951 * @throws IOException forwarded from the exec method of the
952 * command launcher.
953 */
954 public Process exec(Project project, String[] cmd, String[] env,
955 File workingDir) throws IOException {
956 File commandDir = workingDir;
957 if (workingDir == null) {
958 if (project != null) {
959 commandDir = project.getBaseDir();
960 } else {
961 return exec(project, cmd, env);
962 }
963 }
964 // Use cmd.exe to change to the specified directory before running
965 // the command
966 final int preCmdLength = 6;
967 String[] newcmd = new String[cmd.length + preCmdLength];
968 // CheckStyle:MagicNumber OFF - do not bother
969 newcmd[0] = "cmd";
970 newcmd[1] = "/c";
971 newcmd[2] = "cd";
972 newcmd[3] = "/d";
973 newcmd[4] = commandDir.getAbsolutePath();
974 newcmd[5] = "&&";
975 // CheckStyle:MagicNumber ON
976 System.arraycopy(cmd, 0, newcmd, preCmdLength, cmd.length);
977
978 return exec(project, newcmd, env);
979 }
980 }
981
982 /**
983 * A command launcher for Mac that uses a dodgy mechanism to change
984 * working directory before launching commands.
985 */
986 private static class MacCommandLauncher extends CommandLauncherProxy {
987 MacCommandLauncher(CommandLauncher launcher) {
988 super(launcher);
989 }
990
991 /**
992 * Launches the given command in a new process, in the given working
993 * directory.
994 * @param project the Ant project.
995 * @param cmd the command line to execute as an array of strings.
996 * @param env the environment to set as an array of strings.
997 * @param workingDir working directory where the command should run.
998 * @return the created Process.
999 * @throws IOException forwarded from the exec method of the
1000 * command launcher.
1001 */
1002 public Process exec(Project project, String[] cmd, String[] env,
1003 File workingDir) throws IOException {
1004 if (workingDir == null) {
1005 return exec(project, cmd, env);
1006 }
1007 System.getProperties().put("user.dir", workingDir.getAbsolutePath());
1008 try {
1009 return exec(project, cmd, env);
1010 } finally {
1011 System.getProperties().put("user.dir", antWorkingDirectory);
1012 }
1013 }
1014 }
1015
1016 /**
1017 * A command launcher that uses an auxiliary script to launch commands
1018 * in directories other than the current working directory.
1019 */
1020 private static class ScriptCommandLauncher extends CommandLauncherProxy {
1021 ScriptCommandLauncher(String script, CommandLauncher launcher) {
1022 super(launcher);
1023 myScript = script;
1024 }
1025
1026 /**
1027 * Launches the given command in a new process, in the given working
1028 * directory.
1029 * @param project the Ant project.
1030 * @param cmd the command line to execute as an array of strings.
1031 * @param env the environment to set as an array of strings.
1032 * @param workingDir working directory where the command should run.
1033 * @return the created Process.
1034 * @throws IOException forwarded from the exec method of the
1035 * command launcher.
1036 */
1037 public Process exec(Project project, String[] cmd, String[] env,
1038 File workingDir) throws IOException {
1039 if (project == null) {
1040 if (workingDir == null) {
1041 return exec(project, cmd, env);
1042 }
1043 throw new IOException("Cannot locate antRun script: "
1044 + "No project provided");
1045 }
1046 // Locate the auxiliary script
1047 String antHome = project.getProperty(MagicNames.ANT_HOME);
1048 if (antHome == null) {
1049 throw new IOException("Cannot locate antRun script: "
1050 + "Property '" + MagicNames.ANT_HOME + "' not found");
1051 }
1052 String antRun =
1053 FILE_UTILS.resolveFile(project.getBaseDir(),
1054 antHome + File.separator + myScript).toString();
1055
1056 // Build the command
1057 File commandDir = workingDir;
1058 if (workingDir == null) {
1059 commandDir = project.getBaseDir();
1060 }
1061 String[] newcmd = new String[cmd.length + 2];
1062 newcmd[0] = antRun;
1063 newcmd[1] = commandDir.getAbsolutePath();
1064 System.arraycopy(cmd, 0, newcmd, 2, cmd.length);
1065
1066 return exec(project, newcmd, env);
1067 }
1068
1069 private String myScript;
1070 }
1071
1072 /**
1073 * A command launcher that uses an auxiliary perl script to launch commands
1074 * in directories other than the current working directory.
1075 */
1076 private static class PerlScriptCommandLauncher
1077 extends CommandLauncherProxy {
1078 private String myScript;
1079
1080 PerlScriptCommandLauncher(String script, CommandLauncher launcher) {
1081 super(launcher);
1082 myScript = script;
1083 }
1084
1085 /**
1086 * Launches the given command in a new process, in the given working
1087 * directory.
1088 * @param project the Ant project.
1089 * @param cmd the command line to execute as an array of strings.
1090 * @param env the environment to set as an array of strings.
1091 * @param workingDir working directory where the command should run.
1092 * @return the created Process.
1093 * @throws IOException forwarded from the exec method of the
1094 * command launcher.
1095 */
1096 public Process exec(Project project, String[] cmd, String[] env,
1097 File workingDir) throws IOException {
1098 if (project == null) {
1099 if (workingDir == null) {
1100 return exec(project, cmd, env);
1101 }
1102 throw new IOException("Cannot locate antRun script: "
1103 + "No project provided");
1104 }
1105 // Locate the auxiliary script
1106 String antHome = project.getProperty(MagicNames.ANT_HOME);
1107 if (antHome == null) {
1108 throw new IOException("Cannot locate antRun script: "
1109 + "Property '" + MagicNames.ANT_HOME + "' not found");
1110 }
1111 String antRun =
1112 FILE_UTILS.resolveFile(project.getBaseDir(),
1113 antHome + File.separator + myScript).toString();
1114
1115 // Build the command
1116 File commandDir = workingDir;
1117 if (workingDir == null) {
1118 commandDir = project.getBaseDir();
1119 }
1120 // CheckStyle:MagicNumber OFF
1121 String[] newcmd = new String[cmd.length + 3];
1122 newcmd[0] = "perl";
1123 newcmd[1] = antRun;
1124 newcmd[2] = commandDir.getAbsolutePath();
1125 System.arraycopy(cmd, 0, newcmd, 3, cmd.length);
1126 // CheckStyle:MagicNumber ON
1127
1128 return exec(project, newcmd, env);
1129 }
1130 }
1131
1132 /**
1133 * A command launcher for VMS that writes the command to a temporary DCL
1134 * script before launching commands. This is due to limitations of both
1135 * the DCL interpreter and the Java VM implementation.
1136 */
1137 private static class VmsCommandLauncher extends Java13CommandLauncher {
1138
1139 public VmsCommandLauncher() throws NoSuchMethodException {
1140 super();
1141 }
1142
1143 /**
1144 * Launches the given command in a new process.
1145 * @param project the Ant project.
1146 * @param cmd the command line to execute as an array of strings.
1147 * @param env the environment to set as an array of strings.
1148 * @return the created Process.
1149 * @throws IOException forwarded from the exec method of the
1150 * command launcher.
1151 */
1152 public Process exec(Project project, String[] cmd, String[] env)
1153 throws IOException {
1154 File cmdFile = createCommandFile(cmd, env);
1155 Process p
1156 = super.exec(project, new String[] {cmdFile.getPath()}, env);
1157 deleteAfter(cmdFile, p);
1158 return p;
1159 }
1160
1161 /**
1162 * Launches the given command in a new process, in the given working
1163 * directory. Note that under Java 1.3.1, 1.4.0 and 1.4.1 on VMS this
1164 * method only works if <code>workingDir</code> is null or the logical
1165 * JAVA$FORK_SUPPORT_CHDIR needs to be set to TRUE.
1166 * @param project the Ant project.
1167 * @param cmd the command line to execute as an array of strings.
1168 * @param env the environment to set as an array of strings.
1169 * @param workingDir working directory where the command should run.
1170 * @return the created Process.
1171 * @throws IOException forwarded from the exec method of the
1172 * command launcher.
1173 */
1174 public Process exec(Project project, String[] cmd, String[] env,
1175 File workingDir) throws IOException {
1176 File cmdFile = createCommandFile(cmd, env);
1177 Process p = super.exec(project, new String[] {cmdFile.getPath()},
1178 env, workingDir);
1179 deleteAfter(cmdFile, p);
1180 return p;
1181 }
1182
1183 /*
1184 * Writes the command into a temporary DCL script and returns the
1185 * corresponding File object. The script will be deleted on exit.
1186 * @param cmd the command line to execute as an array of strings.
1187 * @param env the environment to set as an array of strings.
1188 * @return the command File.
1189 * @throws IOException if errors are encountered creating the file.
1190 */
1191 private File createCommandFile(String[] cmd, String[] env)
1192 throws IOException {
1193 File script = FILE_UTILS.createTempFile("ANT", ".COM", null, true, true);
1194 PrintWriter out = null;
1195 try {
1196 out = new PrintWriter(new FileWriter(script));
1197
1198 // add the environment as logicals to the DCL script
1199 if (env != null) {
1200 int eqIndex;
1201 for (int i = 0; i < env.length; i++) {
1202 eqIndex = env[i].indexOf('=');
1203 if (eqIndex != -1) {
1204 out.print("$ DEFINE/NOLOG ");
1205 out.print(env[i].substring(0, eqIndex));
1206 out.print(" \"");
1207 out.print(env[i].substring(eqIndex + 1));
1208 out.println('\"');
1209 }
1210 }
1211 }
1212 out.print("$ " + cmd[0]);
1213 for (int i = 1; i < cmd.length; i++) {
1214 out.println(" -");
1215 out.print(cmd[i]);
1216 }
1217 } finally {
1218 if (out != null) {
1219 out.close();
1220 }
1221 }
1222 return script;
1223 }
1224
1225 private void deleteAfter(final File f, final Process p) {
1226 new Thread() {
1227 public void run() {
1228 try {
1229 p.waitFor();
1230 } catch (InterruptedException e) {
1231 //ignore
1232 }
1233 FileUtils.delete(f);
1234 }
1235 }
1236 .start();
1237 }
1238 }
1239 }