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                final 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                     final 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                 final File outputFile = outputRedirect.file();
 227                 final 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         private final Thread thread;
 253 
 254         Piper(final InputStream input, final OutputStream output) {
 255             this.input = input;
 256             this.output = output;
 257             this.thread = new Thread(this, "$EXEC Piper");
 258         }
 259 
 260         /**
 261          * start - start the Piper in a new daemon thread
 262          * @return this Piper
 263          */
 264         Piper start() {
 265             thread.setDaemon(true);
 266             thread.start();
 267             return this;
 268         }
 269 
 270         /**
 271          * run - thread action
 272          */
 273         @Override
 274         public void run() {
 275             try {
 276                 // Buffer for copying.
 277                 final byte[] b = new byte[BUFFER_SIZE];
 278                 // Read from the InputStream until EOF.
 279                 int read;
 280                 while (-1 < (read = input.read(b, 0, b.length))) {
 281                     // Write available date to OutputStream.
 282                     output.write(b, 0, read);
 283                 }
 284             } catch (final Exception e) {
 285                 // Assume the worst.
 286                 throw new RuntimeException("Broken pipe", e);
 287             } finally {
 288                 // Make sure the streams are closed.
 289                 try {
 290                     input.close();
 291                 } catch (final IOException e) {
 292                     // Don't care.
 293                 }
 294                 try {
 295                     output.close();
 296                 } catch (final IOException e) {
 297                     // Don't care.
 298                 }
 299             }
 300         }
 301 
 302         public void join() throws InterruptedException {
 303             thread.join();
 304         }
 305 
 306         // Exit thread.
 307     }
 308 
 309     // Process exit statuses.
 310     static final int EXIT_SUCCESS  =  0;
 311     static final int EXIT_FAILURE  =  1;
 312 
 313     // Copy of environment variables used by all processes.
 314     private  Map<String, String> environment;
 315     // Input string if provided on CommandExecutor call.
 316     private String inputString;
 317     // Output string if required from CommandExecutor call.
 318     private String outputString;
 319     // Error string if required from CommandExecutor call.
 320     private String errorString;
 321     // Last process exit code.
 322     private int exitCode;
 323 
 324     // Input stream if provided on CommandExecutor call.
 325     private InputStream inputStream;
 326     // Output stream if provided on CommandExecutor call.
 327     private OutputStream outputStream;
 328     // Error stream if provided on CommandExecutor call.
 329     private OutputStream errorStream;
 330 
 331     // Ordered collection of current or piped ProcessBuilders.
 332     private List<ProcessBuilder> processBuilders = new ArrayList<>();
 333 
 334     CommandExecutor() {
 335         this.environment = new HashMap<>();
 336         this.inputString = "";
 337         this.outputString = "";
 338         this.errorString = "";
 339         this.exitCode = EXIT_SUCCESS;
 340         this.inputStream = null;
 341         this.outputStream = null;
 342         this.errorStream = null;
 343         this.processBuilders = new ArrayList<>();
 344     }
 345 
 346     /**
 347      * envVarValue - return the value of the environment variable key, or
 348      * deflt if not found.
 349      * @param key   name of environment variable
 350      * @param deflt value to return if not found
 351      * @return value of the environment variable
 352      */
 353     private String envVarValue(final String key, final String deflt) {
 354         return environment.getOrDefault(key, deflt);
 355     }
 356 
 357     /**
 358      * envVarLongValue - return the value of the environment variable key as a
 359      * long value.
 360      * @param key name of environment variable
 361      * @return long value of the environment variable
 362      */
 363     private long envVarLongValue(final String key) {
 364         try {
 365             return Long.parseLong(envVarValue(key, "0"));
 366         } catch (final NumberFormatException ex) {
 367             return 0L;
 368         }
 369     }
 370 
 371     /**
 372      * envVarBooleanValue - return the value of the environment variable key as a
 373      * boolean value.  true if the value was non-zero, false otherwise.
 374      * @param key name of environment variable
 375      * @return boolean value of the environment variable
 376      */
 377     private boolean envVarBooleanValue(final String key) {
 378         return envVarLongValue(key) != 0;
 379     }
 380 
 381     /**
 382      * stripQuotes - strip quotes from token if present. Quoted tokens kept
 383      * quotes to prevent search for redirects.
 384      * @param token token to strip
 385      * @return stripped token
 386      */
 387     private static String stripQuotes(String token) {
 388         if ((token.startsWith("\"") && token.endsWith("\"")) ||
 389              token.startsWith("\'") && token.endsWith("\'")) {
 390             token = token.substring(1, token.length() - 1);
 391         }
 392         return token;
 393     }
 394 
 395     /**
 396      * resolvePath - resolves a path against a current working directory.
 397      * @param cwd      current working directory
 398      * @param fileName name of file or directory
 399      * @return resolved Path to file
 400      */
 401     private static Path resolvePath(final String cwd, final String fileName) {
 402         return Paths.get(sanitizePath(cwd)).resolve(fileName).normalize();
 403     }
 404 
 405     /**
 406      * builtIn - checks to see if the command is a builtin and performs
 407      * appropriate action.
 408      * @param cmd current command
 409      * @param cwd current working directory
 410      * @return true if was a builtin command
 411      */
 412     private boolean builtIn(final List<String> cmd, final String cwd) {
 413         switch (cmd.get(0)) {
 414             // Set current working directory.
 415             case "cd":
 416                 final boolean cygpath = IS_WINDOWS && cwd.startsWith(CYGDRIVE);
 417                 // If zero args then use home directory as cwd else use first arg.
 418                 final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
 419                 // Normalize the cwd
 420                 final Path cwdPath = resolvePath(cwd, newCWD);
 421 
 422                 // Check if is a directory.
 423                 final File file = cwdPath.toFile();
 424                 if (!file.exists()) {
 425                     reportError("file.not.exist", file.toString());
 426                     return true;
 427                 } else if (!file.isDirectory()) {
 428                     reportError("not.directory", file.toString());
 429                     return true;
 430                 }
 431 
 432                 // Set PWD environment variable to be picked up as cwd.
 433                 // Make sure Cygwin paths look like Unix paths.
 434                 String scwd = cwdPath.toString();
 435                 if (cygpath && scwd.length() >= 2 &&
 436                         Character.isLetter(scwd.charAt(0)) && scwd.charAt(1) == ':') {
 437                     scwd = CYGDRIVE + Character.toLowerCase(scwd.charAt(0)) + "/" + scwd.substring(2);
 438                 }
 439                 environment.put("PWD", scwd);
 440                 return true;
 441 
 442             // Set an environment variable.
 443             case "setenv":
 444                 if (3 <= cmd.size()) {
 445                     final String key = cmd.get(1);
 446                     final String value = cmd.get(2);
 447                     environment.put(key, value);
 448                 }
 449 
 450                 return true;
 451 
 452             // Unset an environment variable.
 453             case "unsetenv":
 454                 if (2 <= cmd.size()) {
 455                     final String key = cmd.get(1);
 456                     environment.remove(key);
 457                 }
 458 
 459                 return true;
 460         }
 461 
 462         return false;
 463     }
 464 
 465     /**
 466      * preprocessCommand - scan the command for redirects, and sanitize the
 467      * executable path
 468      * @param tokens       command tokens
 469      * @param cwd          current working directory
 470      * @param redirectInfo redirection information
 471      * @return tokens remaining for actual command
 472      */
 473     private List<String>  preprocessCommand(final List<String> tokens,
 474             final String cwd, final RedirectInfo redirectInfo) {
 475         // Tokens remaining for actual command.
 476         final List<String> command = new ArrayList<>();
 477 
 478         // iterate through all tokens.
 479         final Iterator<String> iterator = tokens.iterator();
 480         while (iterator.hasNext()) {
 481             final String token = iterator.next();
 482 
 483             // Check if is a redirect.
 484             if (redirectInfo.check(token, iterator, cwd)) {
 485                 // Don't add to the command.
 486                 continue;
 487             }
 488 
 489             // Strip quotes and add to command.
 490             command.add(stripQuotes(token));
 491         }
 492 
 493         if (command.size() > 0) {
 494             command.set(0, sanitizePath(command.get(0)));
 495         }
 496 
 497         return command;
 498     }
 499 
 500     /**
 501      * Sanitize a path in case the underlying platform is Cygwin. In that case,
 502      * convert from the {@code /cygdrive/x} drive specification to the usual
 503      * Windows {@code X:} format.
 504      *
 505      * @param d a String representing a path
 506      * @return a String representing the same path in a form that can be
 507      *         processed by the underlying platform
 508      */
 509     private static String sanitizePath(final String d) {
 510         if (!IS_WINDOWS || (IS_WINDOWS && !d.startsWith(CYGDRIVE))) {
 511             return d;
 512         }
 513         final String pd = d.substring(CYGDRIVE.length());
 514         if (pd.length() >= 2 && pd.charAt(1) == '/') {
 515             // drive letter plus / -> convert /cygdrive/x/... to X:/...
 516             return pd.charAt(0) + ":" + pd.substring(1);
 517         } else if (pd.length() == 1) {
 518             // just drive letter -> convert /cygdrive/x to X:
 519             return pd.charAt(0) + ":";
 520         }
 521         // remaining case: /cygdrive/ -> can't convert
 522         return d;
 523     }
 524 
 525     /**
 526      * createProcessBuilder - create a ProcessBuilder for the command.
 527      * @param command      command tokens
 528      * @param cwd          current working directory
 529      * @param redirectInfo redirect information
 530      */
 531     private void createProcessBuilder(final List<String> command,
 532             final String cwd, final RedirectInfo redirectInfo) {
 533         // Create new ProcessBuilder.
 534         final ProcessBuilder pb = new ProcessBuilder(command);
 535         // Set current working directory.
 536         pb.directory(new File(sanitizePath(cwd)));
 537 
 538         // Map environment variables.
 539         final Map<String, String> processEnvironment = pb.environment();
 540         processEnvironment.clear();
 541         processEnvironment.putAll(environment);
 542 
 543         // Apply redirects.
 544         redirectInfo.apply(pb);
 545         // Add to current list of commands.
 546         processBuilders.add(pb);
 547     }
 548 
 549     /**
 550      * command - process the command
 551      * @param tokens  tokens of the command
 552      * @param isPiped true if the output of this command should be piped to the next
 553      */
 554     private void command(final List<String> tokens, final boolean isPiped) {
 555         // Test to see if we should echo the command to output.
 556         if (envVarBooleanValue("JJS_ECHO")) {
 557             System.out.println(String.join(" ", tokens));
 558         }
 559 
 560         // Get the current working directory.
 561         final String cwd = envVarValue("PWD", HOME_DIRECTORY);
 562         // Preprocess the command for redirects.
 563         final RedirectInfo redirectInfo = new RedirectInfo();
 564         final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
 565 
 566         // Skip if empty or a built in.
 567         if (command.isEmpty() || builtIn(command, cwd)) {
 568             return;
 569         }
 570 
 571         // Create ProcessBuilder with cwd and redirects set.
 572         createProcessBuilder(command, cwd, redirectInfo);
 573 
 574         // If piped, wait for the next command.
 575         if (isPiped) {
 576             return;
 577         }
 578 
 579         // Fetch first and last ProcessBuilder.
 580         final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
 581         final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
 582 
 583         // Determine which streams have not be redirected from pipes.
 584         boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
 585         boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
 586         boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
 587         final boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
 588 
 589         // If not redirected and inputStream is current processes' input.
 590         if (inputIsPipe && (inheritIO || inputStream == System.in)) {
 591             // Inherit current processes' input.
 592             firstProcessBuilder.redirectInput(Redirect.INHERIT);
 593             inputIsPipe = false;
 594         }
 595 
 596         // If not redirected and outputStream is current processes' output.
 597         if (outputIsPipe && (inheritIO || outputStream == System.out)) {
 598             // Inherit current processes' output.
 599             lastProcessBuilder.redirectOutput(Redirect.INHERIT);
 600             outputIsPipe = false;
 601         }
 602 
 603         // If not redirected and errorStream is current processes' error.
 604         if (errorIsPipe && (inheritIO || errorStream == System.err)) {
 605             // Inherit current processes' error.
 606             lastProcessBuilder.redirectError(Redirect.INHERIT);
 607             errorIsPipe = false;
 608         }
 609 
 610         // Start the processes.
 611         final List<Process> processes = new ArrayList<>();
 612         for (final ProcessBuilder pb : processBuilders) {
 613             try {
 614                 processes.add(pb.start());
 615             } catch (final IOException ex) {
 616                 reportError("unknown.command", String.join(" ", pb.command()));
 617                 return;
 618             }
 619         }
 620 
 621         // Clear processBuilders for next command.
 622         processBuilders.clear();
 623 
 624         // Get first and last process.
 625         final Process firstProcess = processes.get(0);
 626         final Process lastProcess = processes.get(processes.size() - 1);
 627 
 628         // Prepare for string based i/o if no redirection or provided streams.
 629         ByteArrayOutputStream byteOutputStream = null;
 630         ByteArrayOutputStream byteErrorStream = null;
 631 
 632         final List<Piper> piperThreads = new ArrayList<>();
 633 
 634         // If input is not redirected.
 635         if (inputIsPipe) {
 636             // If inputStream other than System.in is provided.
 637             if (inputStream != null) {
 638                 // Pipe inputStream to first process output stream.
 639                 piperThreads.add(new Piper(inputStream, firstProcess.getOutputStream()).start());
 640             } else {
 641                 // Otherwise assume an input string has been provided.
 642                 piperThreads.add(new Piper(new ByteArrayInputStream(inputString.getBytes()), firstProcess.getOutputStream()).start());
 643             }
 644         }
 645 
 646         // If output is not redirected.
 647         if (outputIsPipe) {
 648             // If outputStream other than System.out is provided.
 649             if (outputStream != null ) {
 650                 // Pipe outputStream from last process input stream.
 651                 piperThreads.add(new Piper(lastProcess.getInputStream(), outputStream).start());
 652             } else {
 653                 // Otherwise assume an output string needs to be prepared.
 654                 byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
 655                 piperThreads.add(new Piper(lastProcess.getInputStream(), byteOutputStream).start());
 656             }
 657         }
 658 
 659         // If error is not redirected.
 660         if (errorIsPipe) {
 661             // If errorStream other than System.err is provided.
 662             if (errorStream != null) {
 663                 piperThreads.add(new Piper(lastProcess.getErrorStream(), errorStream).start());
 664             } else {
 665                 // Otherwise assume an error string needs to be prepared.
 666                 byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE);
 667                 piperThreads.add(new Piper(lastProcess.getErrorStream(), byteErrorStream).start());
 668             }
 669         }
 670 
 671         // Pipe commands in between.
 672         for (int i = 0, n = processes.size() - 1; i < n; i++) {
 673             final Process prev = processes.get(i);
 674             final Process next = processes.get(i + 1);
 675             piperThreads.add(new Piper(prev.getInputStream(), next.getOutputStream()).start());
 676         }
 677 
 678         // Wind up processes.
 679         try {
 680             // Get the user specified timeout.
 681             final long timeout = envVarLongValue("JJS_TIMEOUT");
 682 
 683             // If user specified timeout (milliseconds.)
 684             if (timeout != 0) {
 685                 // Wait for last process, with timeout.
 686                 if (lastProcess.waitFor(timeout, TimeUnit.MILLISECONDS)) {
 687                     // Get exit code of last process.
 688                     exitCode = lastProcess.exitValue();
 689                 } else {
 690                     reportError("timeout", Long.toString(timeout));
 691                  }
 692             } else {
 693                 // Wait for last process and get exit code.
 694                 exitCode = lastProcess.waitFor();
 695             }
 696             // Wait for all piper threads to terminate
 697             for (final Piper piper : piperThreads) {
 698                 piper.join();
 699             }
 700 
 701             // Accumulate the output and error streams.
 702             outputString += byteOutputStream != null ? byteOutputStream.toString() : "";
 703             errorString += byteErrorStream != null ? byteErrorStream.toString() : "";
 704         } catch (final InterruptedException ex) {
 705             // Kill any living processes.
 706             processes.stream().forEach(p -> {
 707                 if (p.isAlive()) {
 708                     p.destroy();
 709                 }
 710 
 711                 // Get the first error code.
 712                 exitCode = exitCode == 0 ? p.exitValue() : exitCode;
 713             });
 714         }
 715 
 716         // If we got a non-zero exit code then possibly throw an exception.
 717         if (exitCode != 0 && envVarBooleanValue("JJS_THROW_ON_EXIT")) {
 718             throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exitCode));
 719         }
 720     }
 721 
 722     /**
 723      * createTokenizer - build up StreamTokenizer for the command script
 724      * @param script command script to parsed
 725      * @return StreamTokenizer for command script
 726      */
 727     private static StreamTokenizer createTokenizer(final String script) {
 728         final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(script));
 729         tokenizer.resetSyntax();
 730         // Default all characters to word.
 731         tokenizer.wordChars(0, 255);
 732         // Spaces and special characters are white spaces.
 733         tokenizer.whitespaceChars(0, ' ');
 734         // Ignore # comments.
 735         tokenizer.commentChar('#');
 736         // Handle double and single quote strings.
 737         tokenizer.quoteChar('"');
 738         tokenizer.quoteChar('\'');
 739         // Need to recognize the end of a command.
 740         tokenizer.eolIsSignificant(true);
 741         // Command separator.
 742         tokenizer.ordinaryChar(';');
 743         // Pipe separator.
 744         tokenizer.ordinaryChar('|');
 745 
 746         return tokenizer;
 747     }
 748 
 749     /**
 750      * process - process a command string
 751      * @param script command script to parsed
 752      */
 753     void process(final String script) {
 754         // Build up StreamTokenizer for the command script.
 755         final StreamTokenizer tokenizer = createTokenizer(script);
 756 
 757         // Prepare to accumulate command tokens.
 758         final List<String> command = new ArrayList<>();
 759         // Prepare to acumulate partial tokens joined with "\ ".
 760         final StringBuilder sb = new StringBuilder();
 761 
 762         try {
 763             // Fetch next token until end of script.
 764             while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
 765                 // Next word token.
 766                 String token = tokenizer.sval;
 767 
 768                 // If special token.
 769                 if (token == null) {
 770                     // Flush any partial token.
 771                     if (sb.length() != 0) {
 772                         command.add(sb.append(token).toString());
 773                         sb.setLength(0);
 774                     }
 775 
 776                     // Process a completed command.
 777                     // Will be either ';' (command end) or '|' (pipe), true if '|'.
 778                     command(command, tokenizer.ttype == '|');
 779 
 780                     if (exitCode != EXIT_SUCCESS) {
 781                         return;
 782                     }
 783 
 784                     // Start with a new set of tokens.
 785                     command.clear();
 786                 } else if (token.endsWith("\\")) {
 787                     // Backslash followed by space.
 788                     sb.append(token.substring(0, token.length() - 1)).append(' ');
 789                 } else if (sb.length() == 0) {
 790                     // If not a word then must be a quoted string.
 791                     if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
 792                         // Quote string, sb is free to use (empty.)
 793                         sb.append((char)tokenizer.ttype);
 794                         sb.append(token);
 795                         sb.append((char)tokenizer.ttype);
 796                         token = sb.toString();
 797                         sb.setLength(0);
 798                     }
 799 
 800                     command.add(token);
 801                 } else {
 802                     // Partial token pending.
 803                     command.add(sb.append(token).toString());
 804                     sb.setLength(0);
 805                 }
 806             }
 807         } catch (final IOException ex) {
 808             // Do nothing.
 809         }
 810 
 811         // Partial token pending.
 812         if (sb.length() != 0) {
 813             command.add(sb.toString());
 814         }
 815 
 816         // Process last command.
 817         command(command, false);
 818     }
 819 
 820     /**
 821      * process - process a command array of strings
 822      * @param tokens command script to be processed
 823      */
 824     void process(final List<String> tokens) {
 825         // Prepare to accumulate command tokens.
 826         final List<String> command = new ArrayList<>();
 827 
 828         // Iterate through tokens.
 829         final Iterator<String> iterator = tokens.iterator();
 830         while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
 831             // Next word token.
 832             final String token = iterator.next();
 833 
 834             if (token == null) {
 835                 continue;
 836             }
 837 
 838             switch (token) {
 839                 case "|":
 840                     // Process as a piped command.
 841                     command(command, true);
 842                     // Start with a new set of tokens.
 843                     command.clear();
 844 
 845                     continue;
 846                 case ";":
 847                     // Process as a normal command.
 848                     command(command, false);
 849                     // Start with a new set of tokens.
 850                     command.clear();
 851 
 852                     continue;
 853             }
 854 
 855             command.add(token);
 856         }
 857 
 858         // Process last command.
 859         command(command, false);
 860     }
 861 
 862     void reportError(final String msg, final String object) {
 863         errorString += ECMAErrors.getMessage("range.error.exec." + msg, object);
 864         exitCode = EXIT_FAILURE;
 865     }
 866 
 867     String getOutputString() {
 868         return outputString;
 869     }
 870 
 871     String getErrorString() {
 872         return errorString;
 873     }
 874 
 875     int getExitCode() {
 876         return exitCode;
 877     }
 878 
 879     void setEnvironment(final Map<String, String> environment) {
 880         this.environment = environment;
 881     }
 882 
 883     void setInputStream(final InputStream inputStream) {
 884         this.inputStream = inputStream;
 885     }
 886 
 887     void setInputString(final String inputString) {
 888         this.inputString = inputString;
 889     }
 890 
 891     void setOutputStream(final OutputStream outputStream) {
 892         this.outputStream = outputStream;
 893     }
 894 
 895     void setErrorStream(final OutputStream errorStream) {
 896         this.errorStream = errorStream;
 897     }
 898 }