src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/CommandExecutor.java
changeset 47216 71c04702a3d5
parent 41422 97eda72f53b6
equal deleted inserted replaced
47215:4ebc2e2fb97c 47216:71c04702a3d5
       
     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 }