1 /*
   2  * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.internal.runtime;
  27 
  28 import java.io.ByteArrayInputStream;
  29 import java.io.ByteArrayOutputStream;
  30 import java.io.File;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.OutputStream;
  34 import java.io.StreamTokenizer;
  35 import java.io.StringReader;
  36 import java.lang.ProcessBuilder.Redirect;
  37 import java.nio.file.Path;
  38 import java.nio.file.Paths;
  39 import java.security.AccessController;
  40 import java.security.PrivilegedAction;
  41 import java.util.ArrayList;
  42 import java.util.HashMap;
  43 import java.util.Iterator;
  44 import java.util.List;
  45 import java.util.Map;
  46 import java.util.concurrent.TimeUnit;
  47 
  48 import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*;
  49 import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
  50 
  51 /**
  52  * The CommandExecutor class provides support for Nashorn's $EXEC
  53  * builtin function. CommandExecutor provides support for command parsing,
  54  * I/O redirection, piping, completion timeouts, # comments, and simple
  55  * environment variable management (cd, setenv, and unsetenv).
  56  */
  57 class CommandExecutor {
  58     // Size of byte buffers used for piping.
  59     private static final int BUFFER_SIZE = 1024;
  60 
  61     // Test to see if running on Windows.
  62     private static final boolean IS_WINDOWS =
  63         AccessController.doPrivileged((PrivilegedAction<Boolean>)() -> {
  64         return System.getProperty("os.name").contains("Windows");
  65     });
  66 
  67     // Cygwin drive alias prefix.
  68     private static final String CYGDRIVE = "/cygdrive/";
  69 
  70     // User's home directory
  71     private static final String HOME_DIRECTORY =
  72         AccessController.doPrivileged((PrivilegedAction<String>)() -> {
  73         return System.getProperty("user.home");
  74     });
  75 
  76     // Various types of standard redirects.
  77     enum RedirectType {
  78         NO_REDIRECT,
  79         REDIRECT_INPUT,
  80         REDIRECT_OUTPUT,
  81         REDIRECT_OUTPUT_APPEND,
  82         REDIRECT_ERROR,
  83         REDIRECT_ERROR_APPEND,
  84         REDIRECT_OUTPUT_ERROR_APPEND,
  85         REDIRECT_ERROR_TO_OUTPUT
  86     };
  87 
  88     // Prefix strings to standard redirects.
  89     private static final String[] redirectPrefixes = new String[] {
  90         "<",
  91         "0<",
  92         ">",
  93         "1>",
  94         ">>",
  95         "1>>",
  96         "2>",
  97         "2>>",
  98         "&>",
  99         "2>&1"
 100     };
 101 
 102     // Map from redirectPrefixes to RedirectType.
 103     private static final RedirectType[] redirects = new RedirectType[] {
 104         REDIRECT_INPUT,
 105         REDIRECT_INPUT,
 106         REDIRECT_OUTPUT,
 107         REDIRECT_OUTPUT,
 108         REDIRECT_OUTPUT_APPEND,
 109         REDIRECT_OUTPUT_APPEND,
 110         REDIRECT_ERROR,
 111         REDIRECT_ERROR_APPEND,
 112         REDIRECT_OUTPUT_ERROR_APPEND,
 113         REDIRECT_ERROR_TO_OUTPUT
 114     };
 115 
 116     /**
 117      * The RedirectInfo class handles checking the next token in a command
 118      * to see if it contains a redirect.  If the redirect file does not butt
 119      * against the prefix, then the next token is consumed.
 120      */
 121     private static class RedirectInfo {
 122         // true if a redirect was encountered on the current command.
 123         private boolean hasRedirects;
 124         // Redirect.PIPE or an input redirect from the command line.
 125         private Redirect inputRedirect;
 126         // Redirect.PIPE or an output redirect from the command line.
 127         private Redirect outputRedirect;
 128         // Redirect.PIPE or an error redirect from the command line.
 129         private Redirect errorRedirect;
 130         // true if the error stream should be merged with output.
 131         private boolean mergeError;
 132 
 133         RedirectInfo() {
 134             this.hasRedirects = false;
 135             this.inputRedirect = Redirect.PIPE;
 136             this.outputRedirect = Redirect.PIPE;
 137             this.errorRedirect = Redirect.PIPE;
 138             this.mergeError = false;
 139         }
 140 
 141         /**
 142          * check - tests to see if the current token contains a redirect
 143          * @param token    current command line token
 144          * @param iterator current command line iterator
 145          * @param cwd      current working directory
 146          * @return true if token is consumed
 147          */
 148         boolean check(String token, final Iterator<String> iterator, final String cwd) {
 149             // Iterate through redirect prefixes to file a match.
 150             for (int i = 0; i < redirectPrefixes.length; i++) {
 151                String prefix = redirectPrefixes[i];
 152 
 153                // If a match is found.
 154                 if (token.startsWith(prefix)) {
 155                     // Indicate we have at least one redirect (efficiency.)
 156                     hasRedirects = true;
 157                     // Map prefix to RedirectType.
 158                     RedirectType redirect = redirects[i];
 159                     // Strip prefix from token
 160                     token = token.substring(prefix.length());
 161 
 162                     // Get file from either current or next token.
 163                     File file = null;
 164                     if (redirect != REDIRECT_ERROR_TO_OUTPUT) {
 165                         // Nothing left of current token.
 166                         if (token.length() == 0) {
 167                             if (iterator.hasNext()) {
 168                                 // Use next token.
 169                                 token = iterator.next();
 170                             } else {
 171                                 // Send to null device if not provided.
 172                                 token = IS_WINDOWS ? "NUL:" : "/dev/null";
 173                             }
 174                         }
 175 
 176                         // Redirect file.
 177                         file = resolvePath(cwd, token).toFile();
 178                     }
 179 
 180                     // Define redirect based on prefix.
 181                     switch (redirect) {
 182                         case REDIRECT_INPUT:
 183                             inputRedirect = Redirect.from(file);
 184                             break;
 185                         case REDIRECT_OUTPUT:
 186                             outputRedirect = Redirect.to(file);
 187                             break;
 188                         case REDIRECT_OUTPUT_APPEND:
 189                             outputRedirect = Redirect.appendTo(file);
 190                             break;
 191                         case REDIRECT_ERROR:
 192                             errorRedirect = Redirect.to(file);
 193                             break;
 194                         case REDIRECT_ERROR_APPEND:
 195                             errorRedirect = Redirect.appendTo(file);
 196                             break;
 197                         case REDIRECT_OUTPUT_ERROR_APPEND:
 198                             outputRedirect = Redirect.to(file);
 199                             errorRedirect = Redirect.to(file);
 200                             mergeError = true;
 201                             break;
 202                         case REDIRECT_ERROR_TO_OUTPUT:
 203                             mergeError = true;
 204                             break;
 205                         default:
 206                             return false;
 207                     }
 208 
 209                     // Indicate token is consumed.
 210                     return true;
 211                 }
 212             }
 213 
 214             // No redirect found.
 215             return false;
 216         }
 217 
 218         /**
 219          * apply - apply the redirects to the current ProcessBuilder.
 220          * @param pb current ProcessBuilder
 221          */
 222         void apply(final ProcessBuilder pb) {
 223             // Only if there was redirects (saves new structure in ProcessBuilder.)
 224             if (hasRedirects) {
 225                 // If output and error are the same file then merge.
 226                 File outputFile = outputRedirect.file();
 227                 File errorFile = errorRedirect.file();
 228 
 229                 if (outputFile != null && outputFile.equals(errorFile)) {
 230                     mergeError = true;
 231                 }
 232 
 233                 // Apply redirects.
 234                 pb.redirectInput(inputRedirect);
 235                 pb.redirectOutput(outputRedirect);
 236                 pb.redirectError(errorRedirect);
 237                 pb.redirectErrorStream(mergeError);
 238             }
 239         }
 240     }
 241 
 242     /**
 243      * The Piper class is responsible for copying from an InputStream to an
 244      * OutputStream without blocking the current thread.
 245      */
 246     private static class Piper implements java.lang.Runnable {
 247         // Stream to copy from.
 248         private final InputStream input;
 249         // Stream to copy to.
 250         private final OutputStream output;
 251 
 252         Piper(final InputStream input, final OutputStream output) {
 253             this.input = input;
 254             this.output = output;
 255         }
 256 
 257         /**
 258          * start - start the Piper in a new daemon thread
 259          */
 260         void start() {
 261             Thread thread = new Thread(this, "$EXEC Piper");
 262             thread.setDaemon(true);
 263             thread.start();
 264         }
 265 
 266         /**
 267          * run - thread action
 268          */
 269         @Override
 270         public void run() {
 271             try {
 272                 // Buffer for copying.
 273                 byte[] b = new byte[BUFFER_SIZE];
 274                 // Read from the InputStream until EOF.
 275                 int read;
 276                 while (-1 < (read = input.read(b, 0, b.length))) {
 277                     // Write available date to OutputStream.
 278                     output.write(b, 0, read);
 279                 }
 280             } catch (Exception e) {
 281                 // Assume the worst.
 282                 throw new RuntimeException("Broken pipe", e);
 283             } finally {
 284                 // Make sure the streams are closed.
 285                 try {
 286                     input.close();
 287                 } catch (IOException e) {
 288                     // Don't care.
 289                 }
 290                 try {
 291                     output.close();
 292                 } catch (IOException e) {
 293                     // Don't care.
 294                 }
 295             }
 296         }
 297 
 298         // Exit thread.
 299     }
 300 
 301     // Process exit statuses.
 302     static final int EXIT_SUCCESS  =  0;
 303     static final int EXIT_FAILURE  =  1;
 304 
 305     // Copy of environment variables used by all processes.
 306     private  Map<String, String> environment;
 307     // Input string if provided on CommandExecutor call.
 308     private String inputString;
 309     // Output string if required from CommandExecutor call.
 310     private String outputString;
 311     // Error string if required from CommandExecutor call.
 312     private String errorString;
 313     // Last process exit code.
 314     private int exitCode;
 315 
 316     // Input stream if provided on CommandExecutor call.
 317     private InputStream inputStream;
 318     // Output stream if provided on CommandExecutor call.
 319     private OutputStream outputStream;
 320     // Error stream if provided on CommandExecutor call.
 321     private OutputStream errorStream;
 322 
 323     // Ordered collection of current or piped ProcessBuilders.
 324     private List<ProcessBuilder> processBuilders = new ArrayList<>();
 325 
 326     CommandExecutor() {
 327         this.environment = new HashMap<>();
 328         this.inputString = "";
 329         this.outputString = "";
 330         this.errorString = "";
 331         this.exitCode = EXIT_SUCCESS;
 332         this.inputStream = null;
 333         this.outputStream = null;
 334         this.errorStream = null;
 335         this.processBuilders = new ArrayList<>();
 336     }
 337 
 338     /**
 339      * envVarValue - return the value of the environment variable key, or
 340      * deflt if not found.
 341      * @param key   name of environment variable
 342      * @param deflt value to return if not found
 343      * @return value of the environment variable
 344      */
 345     private String envVarValue(final String key, final String deflt) {
 346         return environment.getOrDefault(key, deflt);
 347     }
 348 
 349     /**
 350      * envVarLongValue - return the value of the environment variable key as a
 351      * long value.
 352      * @param key name of environment variable
 353      * @return long value of the environment variable
 354      */
 355     private long envVarLongValue(final String key) {
 356         try {
 357             return Long.parseLong(envVarValue(key, "0"));
 358         } catch (NumberFormatException ex) {
 359             return 0L;
 360         }
 361     }
 362 
 363     /**
 364      * envVarBooleanValue - return the value of the environment variable key as a
 365      * boolean value.  true if the value was non-zero, false otherwise.
 366      * @param key name of environment variable
 367      * @return boolean value of the environment variable
 368      */
 369     private boolean envVarBooleanValue(final String key) {
 370         return envVarLongValue(key) != 0;
 371     }
 372 
 373     /**
 374      * stripQuotes - strip quotes from token if present. Quoted tokens kept
 375      * quotes to prevent search for redirects.
 376      * @param token token to strip
 377      * @return stripped token
 378      */
 379     private static String stripQuotes(String token) {
 380         if ((token.startsWith("\"") && token.endsWith("\"")) ||
 381              token.startsWith("\'") && token.endsWith("\'")) {
 382             token = token.substring(1, token.length() - 1);
 383         }
 384         return token;
 385     }
 386 
 387     /**
 388      * resolvePath - resolves a path against a current working directory.
 389      * @param cwd      current working directory
 390      * @param fileName name of file or directory
 391      * @return resolved Path to file
 392      */
 393     private static Path resolvePath(final String cwd, final String fileName) {
 394         return Paths.get(sanitizePath(cwd)).resolve(fileName).normalize();
 395     }
 396 
 397     /**
 398      * builtIn - checks to see if the command is a builtin and performs
 399      * appropriate action.
 400      * @param cmd current command
 401      * @param cwd current working directory
 402      * @return true if was a builtin command
 403      */
 404     private boolean builtIn(final List<String> cmd, final String cwd) {
 405         switch (cmd.get(0)) {
 406             // Set current working directory.
 407             case "cd":
 408                 final boolean cygpath = IS_WINDOWS && cwd.startsWith(CYGDRIVE);
 409                 // If zero args then use home directory as cwd else use first arg.
 410                 final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
 411                 // Normalize the cwd
 412                 final Path cwdPath = resolvePath(cwd, newCWD);
 413 
 414                 // Check if is a directory.
 415                 final File file = cwdPath.toFile();
 416                 if (!file.exists()) {
 417                     reportError("file.not.exist", file.toString());
 418                     return true;
 419                 } else if (!file.isDirectory()) {
 420                     reportError("not.directory", file.toString());
 421                     return true;
 422                 }
 423 
 424                 // Set PWD environment variable to be picked up as cwd.
 425                 // Make sure Cygwin paths look like Unix paths.
 426                 String scwd = cwdPath.toString();
 427                 if (cygpath && scwd.length() >= 2 &&
 428                         Character.isLetter(scwd.charAt(0)) && scwd.charAt(1) == ':') {
 429                     scwd = CYGDRIVE + Character.toLowerCase(scwd.charAt(0)) + "/" + scwd.substring(2);
 430                 }
 431                 environment.put("PWD", scwd);
 432                 return true;
 433 
 434             // Set an environment variable.
 435             case "setenv":
 436                 if (3 <= cmd.size()) {
 437                     final String key = cmd.get(1);
 438                     final String value = cmd.get(2);
 439                     environment.put(key, value);
 440                 }
 441 
 442                 return true;
 443 
 444             // Unset an environment variable.
 445             case "unsetenv":
 446                 if (2 <= cmd.size()) {
 447                     final String key = cmd.get(1);
 448                     environment.remove(key);
 449                 }
 450 
 451                 return true;
 452         }
 453 
 454         return false;
 455     }
 456 
 457     /**
 458      * preprocessCommand - scan the command for redirects, and sanitize the
 459      * executable path
 460      * @param tokens       command tokens
 461      * @param cwd          current working directory
 462      * @param redirectInfo redirection information
 463      * @return tokens remaining for actual command
 464      */
 465     private List<String>  preprocessCommand(final List<String> tokens,
 466             final String cwd, final RedirectInfo redirectInfo) {
 467         // Tokens remaining for actual command.
 468         final List<String> command = new ArrayList<>();
 469 
 470         // iterate through all tokens.
 471         final Iterator<String> iterator = tokens.iterator();
 472         while (iterator.hasNext()) {
 473             String token = iterator.next();
 474 
 475             // Check if is a redirect.
 476             if (redirectInfo.check(token, iterator, cwd)) {
 477                 // Don't add to the command.
 478                 continue;
 479             }
 480 
 481             // Strip quotes and add to command.
 482             command.add(stripQuotes(token));
 483         }
 484 
 485         if (command.size() > 0) {
 486             command.set(0, sanitizePath(command.get(0)));
 487         }
 488 
 489         return command;
 490     }
 491 
 492     /**
 493      * Sanitize a path in case the underlying platform is Cygwin. In that case,
 494      * convert from the {@code /cygdrive/x} drive specification to the usual
 495      * Windows {@code X:} format.
 496      *
 497      * @param d a String representing a path
 498      * @return a String representing the same path in a form that can be
 499      *         processed by the underlying platform
 500      */
 501     private static String sanitizePath(final String d) {
 502         if (!IS_WINDOWS || (IS_WINDOWS && !d.startsWith(CYGDRIVE))) {
 503             return d;
 504         }
 505         final String pd = d.substring(CYGDRIVE.length());
 506         if (pd.length() >= 2 && pd.charAt(1) == '/') {
 507             // drive letter plus / -> convert /cygdrive/x/... to X:/...
 508             return pd.charAt(0) + ":" + pd.substring(1);
 509         } else if (pd.length() == 1) {
 510             // just drive letter -> convert /cygdrive/x to X:
 511             return pd.charAt(0) + ":";
 512         }
 513         // remaining case: /cygdrive/ -> can't convert
 514         return d;
 515     }
 516 
 517     /**
 518      * createProcessBuilder - create a ProcessBuilder for the command.
 519      * @param command      command tokens
 520      * @param cwd          current working directory
 521      * @param redirectInfo redirect information
 522      */
 523     private void createProcessBuilder(final List<String> command,
 524             final String cwd, final RedirectInfo redirectInfo) {
 525         // Create new ProcessBuilder.
 526         final ProcessBuilder pb = new ProcessBuilder(command);
 527         // Set current working directory.
 528         pb.directory(new File(sanitizePath(cwd)));
 529 
 530         // Map environment variables.
 531         final Map<String, String> processEnvironment = pb.environment();
 532         processEnvironment.clear();
 533         processEnvironment.putAll(environment);
 534 
 535         // Apply redirects.
 536         redirectInfo.apply(pb);
 537         // Add to current list of commands.
 538         processBuilders.add(pb);
 539     }
 540 
 541     /**
 542      * command - process the command
 543      * @param tokens  tokens of the command
 544      * @param isPiped true if the output of this command should be piped to the next
 545      */
 546     private void command(final List<String> tokens, boolean isPiped) {
 547         // Test to see if we should echo the command to output.
 548         if (envVarBooleanValue("JJS_ECHO")) {
 549             System.out.println(String.join(" ", tokens));
 550         }
 551 
 552         // Get the current working directory.
 553         final String cwd = envVarValue("PWD", HOME_DIRECTORY);
 554         // Preprocess the command for redirects.
 555         final RedirectInfo redirectInfo = new RedirectInfo();
 556         final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
 557 
 558         // Skip if empty or a built in.
 559         if (command.isEmpty() || builtIn(command, cwd)) {
 560             return;
 561         }
 562 
 563         // Create ProcessBuilder with cwd and redirects set.
 564         createProcessBuilder(command, cwd, redirectInfo);
 565 
 566         // If piped, wait for the next command.
 567         if (isPiped) {
 568             return;
 569         }
 570 
 571         // Fetch first and last ProcessBuilder.
 572         final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
 573         final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
 574 
 575         // Determine which streams have not be redirected from pipes.
 576         boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
 577         boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
 578         boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
 579         boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
 580 
 581         // If not redirected and inputStream is current processes' input.
 582         if (inputIsPipe && (inheritIO || inputStream == System.in)) {
 583             // Inherit current processes' input.
 584             firstProcessBuilder.redirectInput(Redirect.INHERIT);
 585             inputIsPipe = false;
 586         }
 587 
 588         // If not redirected and outputStream is current processes' output.
 589         if (outputIsPipe && (inheritIO || outputStream == System.out)) {
 590             // Inherit current processes' output.
 591             lastProcessBuilder.redirectOutput(Redirect.INHERIT);
 592             outputIsPipe = false;
 593         }
 594 
 595         // If not redirected and errorStream is current processes' error.
 596         if (errorIsPipe && (inheritIO || errorStream == System.err)) {
 597             // Inherit current processes' error.
 598             lastProcessBuilder.redirectError(Redirect.INHERIT);
 599             errorIsPipe = false;
 600         }
 601 
 602         // Start the processes.
 603         final List<Process> processes = new ArrayList<>();
 604         for (ProcessBuilder pb : processBuilders) {
 605             try {
 606                 processes.add(pb.start());
 607             } catch (IOException ex) {
 608                 reportError("unknown.command", String.join(" ", pb.command()));
 609                 return;
 610             }
 611         }
 612 
 613         // Clear processBuilders for next command.
 614         processBuilders.clear();
 615 
 616         // Get first and last process.
 617         final Process firstProcess = processes.get(0);
 618         final Process lastProcess = processes.get(processes.size() - 1);
 619 
 620         // Prepare for string based i/o if no redirection or provided streams.
 621         ByteArrayOutputStream byteOutputStream = null;
 622         ByteArrayOutputStream byteErrorStream = null;
 623 
 624         // If input is not redirected.
 625         if (inputIsPipe) {
 626             // If inputStream other than System.in is provided.
 627             if (inputStream != null) {
 628                 // Pipe inputStream to first process output stream.
 629                 new Piper(inputStream, firstProcess.getOutputStream()).start();
 630             } else {
 631                 // Otherwise assume an input string has been provided.
 632                 new Piper(new ByteArrayInputStream(inputString.getBytes()), firstProcess.getOutputStream()).start();
 633             }
 634         }
 635 
 636         // If output is not redirected.
 637         if (outputIsPipe) {
 638             // If outputStream other than System.out is provided.
 639             if (outputStream != null ) {
 640                 // Pipe outputStream from last process input stream.
 641                 new Piper(lastProcess.getInputStream(), outputStream).start();
 642             } else {
 643                 // Otherwise assume an output string needs to be prepared.
 644                 byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
 645                 new Piper(lastProcess.getInputStream(), byteOutputStream).start();
 646             }
 647         }
 648 
 649         // If error is not redirected.
 650         if (errorIsPipe) {
 651             // If errorStream other than System.err is provided.
 652             if (errorStream != null) {
 653                 new Piper(lastProcess.getErrorStream(), errorStream).start();
 654             } else {
 655                 // Otherwise assume an error string needs to be prepared.
 656                 byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE);
 657                 new Piper(lastProcess.getErrorStream(), byteErrorStream).start();
 658             }
 659         }
 660 
 661         // Pipe commands in between.
 662         for (int i = 0, n = processes.size() - 1; i < n; i++) {
 663             final Process prev = processes.get(i);
 664             final Process next = processes.get(i + 1);
 665             new Piper(prev.getInputStream(), next.getOutputStream()).start();
 666         }
 667 
 668         // Wind up processes.
 669         try {
 670             // Get the user specified timeout.
 671             long timeout = envVarLongValue("JJS_TIMEOUT");
 672 
 673             // If user specified timeout (milliseconds.)
 674             if (timeout != 0) {
 675                 // Wait for last process, with timeout.
 676                 if (lastProcess.waitFor(timeout, TimeUnit.MILLISECONDS)) {
 677                     // Get exit code of last process.
 678                     exitCode = lastProcess.exitValue();
 679                 } else {
 680                     reportError("timeout", Long.toString(timeout));
 681                  }
 682             } else {
 683                 // Wait for last process and get exit code.
 684                 exitCode = lastProcess.waitFor();
 685             }
 686 
 687             // Accumulate the output and error streams.
 688             outputString += byteOutputStream != null ? byteOutputStream.toString() : "";
 689             errorString += byteErrorStream != null ? byteErrorStream.toString() : "";
 690         } catch (InterruptedException ex) {
 691             // Kill any living processes.
 692             processes.stream().forEach(p -> {
 693                 if (p.isAlive()) {
 694                     p.destroy();
 695                 }
 696 
 697                 // Get the first error code.
 698                 exitCode = exitCode == 0 ? p.exitValue() : exitCode;
 699             });
 700         }
 701 
 702         // If we got a non-zero exit code then possibly throw an exception.
 703         if (exitCode != 0 && envVarBooleanValue("JJS_THROW_ON_EXIT")) {
 704             throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exitCode));
 705         }
 706     }
 707 
 708     /**
 709      * createTokenizer - build up StreamTokenizer for the command script
 710      * @param script command script to parsed
 711      * @return StreamTokenizer for command script
 712      */
 713     private static StreamTokenizer createTokenizer(final String script) {
 714         final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(script));
 715         tokenizer.resetSyntax();
 716         // Default all characters to word.
 717         tokenizer.wordChars(0, 255);
 718         // Spaces and special characters are white spaces.
 719         tokenizer.whitespaceChars(0, ' ');
 720         // Ignore # comments.
 721         tokenizer.commentChar('#');
 722         // Handle double and single quote strings.
 723         tokenizer.quoteChar('"');
 724         tokenizer.quoteChar('\'');
 725         // Need to recognize the end of a command.
 726         tokenizer.eolIsSignificant(true);
 727         // Command separator.
 728         tokenizer.ordinaryChar(';');
 729         // Pipe separator.
 730         tokenizer.ordinaryChar('|');
 731 
 732         return tokenizer;
 733     }
 734 
 735     /**
 736      * process - process a command string
 737      * @param script command script to parsed
 738      */
 739     void process(final String script) {
 740         // Build up StreamTokenizer for the command script.
 741         final StreamTokenizer tokenizer = createTokenizer(script);
 742 
 743         // Prepare to accumulate command tokens.
 744         final List<String> command = new ArrayList<>();
 745         // Prepare to acumulate partial tokens joined with "\ ".
 746         final StringBuilder sb = new StringBuilder();
 747 
 748         try {
 749             // Fetch next token until end of script.
 750             while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
 751                 // Next word token.
 752                 String token = tokenizer.sval;
 753 
 754                 // If special token.
 755                 if (token == null) {
 756                     // Flush any partial token.
 757                     if (sb.length() != 0) {
 758                         command.add(sb.append(token).toString());
 759                         sb.setLength(0);
 760                     }
 761 
 762                     // Process a completed command.
 763                     // Will be either ';' (command end) or '|' (pipe), true if '|'.
 764                     command(command, tokenizer.ttype == '|');
 765 
 766                     if (exitCode != EXIT_SUCCESS) {
 767                         return;
 768                     }
 769 
 770                     // Start with a new set of tokens.
 771                     command.clear();
 772                 } else if (token.endsWith("\\")) {
 773                     // Backslash followed by space.
 774                     sb.append(token.substring(0, token.length() - 1)).append(' ');
 775                 } else if (sb.length() == 0) {
 776                     // If not a word then must be a quoted string.
 777                     if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
 778                         // Quote string, sb is free to use (empty.)
 779                         sb.append((char)tokenizer.ttype);
 780                         sb.append(token);
 781                         sb.append((char)tokenizer.ttype);
 782                         token = sb.toString();
 783                         sb.setLength(0);
 784                     }
 785 
 786                     command.add(token);
 787                 } else {
 788                     // Partial token pending.
 789                     command.add(sb.append(token).toString());
 790                     sb.setLength(0);
 791                 }
 792             }
 793         } catch (final IOException ex) {
 794             // Do nothing.
 795         }
 796 
 797         // Partial token pending.
 798         if (sb.length() != 0) {
 799             command.add(sb.toString());
 800         }
 801 
 802         // Process last command.
 803         command(command, false);
 804     }
 805 
 806     /**
 807      * process - process a command array of strings
 808      * @param tokens command script to be processed
 809      */
 810     void process(final List<String> tokens) {
 811         // Prepare to accumulate command tokens.
 812         final List<String> command = new ArrayList<>();
 813 
 814         // Iterate through tokens.
 815         final Iterator<String> iterator = tokens.iterator();
 816         while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
 817             // Next word token.
 818             String token = iterator.next();
 819 
 820             if (token == null) {
 821                 continue;
 822             }
 823 
 824             switch (token) {
 825                 case "|":
 826                     // Process as a piped command.
 827                     command(command, true);
 828                     // Start with a new set of tokens.
 829                     command.clear();
 830 
 831                     continue;
 832                 case ";":
 833                     // Process as a normal command.
 834                     command(command, false);
 835                     // Start with a new set of tokens.
 836                     command.clear();
 837 
 838                     continue;
 839             }
 840 
 841             command.add(token);
 842         }
 843 
 844         // Process last command.
 845         command(command, false);
 846     }
 847 
 848     void reportError(final String msg, final String object) {
 849         errorString += ECMAErrors.getMessage("range.error.exec." + msg, object);
 850         exitCode = EXIT_FAILURE;
 851     }
 852 
 853     String getOutputString() {
 854         return outputString;
 855     }
 856 
 857     String getErrorString() {
 858         return errorString;
 859     }
 860 
 861     int getExitCode() {
 862         return exitCode;
 863     }
 864 
 865     void setEnvironment(Map<String, String> environment) {
 866         this.environment = environment;
 867     }
 868 
 869     void setInputStream(InputStream inputStream) {
 870         this.inputStream = inputStream;
 871     }
 872 
 873     void setInputString(String inputString) {
 874         this.inputString = inputString;
 875     }
 876 
 877     void setOutputStream(OutputStream outputStream) {
 878         this.outputStream = outputStream;
 879     }
 880 
 881     void setErrorStream(OutputStream errorStream) {
 882         this.errorStream = errorStream;
 883     }
 884 }