View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.utils.cli;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  
24  import java.io.InputStream;
25  import java.nio.charset.Charset;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Properties;
31  import java.util.StringTokenizer;
32  import java.util.concurrent.TimeUnit;
33  
34  import org.apache.maven.shared.utils.Os;
35  import org.apache.maven.shared.utils.StringUtils;
36  
37  /**
38   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l </a>
39   */
40  public abstract class CommandLineUtils {
41  
42      /**
43       * A {@code StreamConsumer} providing consumed lines as a {@code String}.
44       *
45       * @see #getOutput()
46       */
47      public static class StringStreamConsumer implements StreamConsumer {
48  
49          private final StringBuilder string = new StringBuilder();
50  
51          private static final String LS = System.getProperty("line.separator", "\n");
52  
53          /**
54           * {@inheritDoc}
55           */
56          @Override
57          public void consumeLine(String line) {
58              string.append(line).append(LS);
59          }
60  
61          /**
62           * @return The output.
63           */
64          public String getOutput() {
65              return string.toString();
66          }
67      }
68  
69      /**
70       * @param cl The command line {@link Commandline}
71       * @param systemOut {@link StreamConsumer}
72       * @param systemErr {@link StreamConsumer}
73       * @return return code.
74       * @throws CommandLineException in case of a problem.
75       */
76      public static int executeCommandLine(@Nonnull Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr)
77              throws CommandLineException {
78          return executeCommandLine(cl, null, systemOut, systemErr, 0);
79      }
80  
81      /**
82       * @param cl The command line {@link Commandline}
83       * @param systemOut {@link StreamConsumer}
84       * @param systemErr {@link StreamConsumer}
85       * @param timeoutInSeconds The timeout.
86       * @return return code.
87       * @throws CommandLineException in case of a problem.
88       */
89      public static int executeCommandLine(
90              @Nonnull Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr, int timeoutInSeconds)
91              throws CommandLineException {
92          return executeCommandLine(cl, null, systemOut, systemErr, timeoutInSeconds);
93      }
94  
95      /**
96       * @param cl The command line {@link Commandline}
97       * @param systemIn {@link StreamConsumer}
98       * @param systemOut {@link StreamConsumer}
99       * @param systemErr {@link StreamConsumer}
100      * @return return code.
101      * @throws CommandLineException in case of a problem.
102      */
103     public static int executeCommandLine(
104             @Nonnull Commandline cl, InputStream systemIn, StreamConsumer systemOut, StreamConsumer systemErr)
105             throws CommandLineException {
106         return executeCommandLine(cl, systemIn, systemOut, systemErr, 0);
107     }
108 
109     /**
110      * @param cl               The command line to execute
111      * @param systemIn         The input to read from, must be thread safe
112      * @param systemOut        A consumer that receives output, must be thread safe
113      * @param systemErr        A consumer that receives system error stream output, must be thread safe
114      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
115      * @return A return value, see {@link Process#exitValue()}
116      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
117      */
118     public static int executeCommandLine(
119             @Nonnull Commandline cl,
120             InputStream systemIn,
121             StreamConsumer systemOut,
122             StreamConsumer systemErr,
123             int timeoutInSeconds)
124             throws CommandLineException {
125         return executeCommandLine(cl, systemIn, systemOut, systemErr, timeoutInSeconds, null);
126     }
127 
128     /**
129      * @param cl               The command line to execute
130      * @param systemIn         The input to read from, must be thread safe
131      * @param systemOut        A consumer that receives output, must be thread safe
132      * @param systemErr        A consumer that receives system error stream output, must be thread safe
133      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
134      * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
135      *  exceeded, but before waiting on the stream feeder and pumpers to finish.
136      * @return A return value, see {@link Process#exitValue()}
137      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
138      */
139     public static int executeCommandLine(
140             @Nonnull Commandline cl,
141             InputStream systemIn,
142             StreamConsumer systemOut,
143             StreamConsumer systemErr,
144             int timeoutInSeconds,
145             @Nullable Runnable runAfterProcessTermination)
146             throws CommandLineException {
147         return executeCommandLine(
148                 cl, systemIn, systemOut, systemErr, timeoutInSeconds, runAfterProcessTermination, null);
149     }
150 
151     /**
152      * @param cl               The command line to execute
153      * @param systemIn         The input to read from, must be thread safe
154      * @param systemOut        A consumer that receives output, must be thread safe
155      * @param systemErr        A consumer that receives system error stream output, must be thread safe
156      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
157      * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
158      *  exceeded, but before waiting on the stream feeder and pumpers to finish.
159      * @param streamCharset    Charset to use for reading streams
160      * @return A return value, see {@link Process#exitValue()}
161      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
162      */
163     public static int executeCommandLine(
164             @Nonnull Commandline cl,
165             InputStream systemIn,
166             StreamConsumer systemOut,
167             StreamConsumer systemErr,
168             int timeoutInSeconds,
169             @Nullable Runnable runAfterProcessTermination,
170             @Nullable final Charset streamCharset)
171             throws CommandLineException {
172         final CommandLineCallable future = executeCommandLineAsCallable(
173                 cl, systemIn, systemOut, systemErr, timeoutInSeconds, runAfterProcessTermination, streamCharset);
174         return future.call();
175     }
176 
177     /**
178      * Immediately forks a process, returns a callable that will block until process is complete.
179      *
180      * @param cl               The command line to execute
181      * @param systemIn         The input to read from, must be thread safe
182      * @param systemOut        A consumer that receives output, must be thread safe
183      * @param systemErr        A consumer that receives system error stream output, must be thread safe
184      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
185      * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
186      * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call"
187      *         must be called on this to be sure the forked process has terminated, no guarantees is made about
188      *         any internal state before after the completion of the call statements
189      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
190      */
191     public static CommandLineCallable executeCommandLineAsCallable(
192             @Nonnull final Commandline cl,
193             @Nullable final InputStream systemIn,
194             final StreamConsumer systemOut,
195             final StreamConsumer systemErr,
196             final int timeoutInSeconds,
197             @Nullable final Runnable runAfterProcessTermination)
198             throws CommandLineException {
199         return executeCommandLineAsCallable(
200                 cl, systemIn, systemOut, systemErr, timeoutInSeconds, runAfterProcessTermination, null);
201     }
202 
203     /**
204      * Immediately forks a process, returns a callable that will block until process is complete.
205      *
206      * @param cl               The command line to execute
207      * @param systemIn         The input to read from, must be thread safe
208      * @param systemOut        A consumer that receives output, must be thread safe
209      * @param systemErr        A consumer that receives system error stream output, must be thread safe
210      * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
211      * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
212      * @param streamCharset    Charset to use for reading streams
213      * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call"
214      *         must be called on this to be sure the forked process has terminated, no guarantees is made about
215      *         any internal state before after the completion of the call statements
216      * @throws CommandLineException or CommandLineTimeOutException if time out occurs
217      */
218     public static CommandLineCallable executeCommandLineAsCallable(
219             @Nonnull final Commandline cl,
220             @Nullable final InputStream systemIn,
221             final StreamConsumer systemOut,
222             final StreamConsumer systemErr,
223             final int timeoutInSeconds,
224             @Nullable final Runnable runAfterProcessTermination,
225             @Nullable final Charset streamCharset)
226             throws CommandLineException {
227         //noinspection ConstantConditions
228         if (cl == null) {
229             throw new IllegalArgumentException("cl cannot be null.");
230         }
231 
232         final Process p = cl.execute();
233 
234         final Thread processHook = new Thread() {
235 
236             {
237                 this.setName("CommandLineUtils process shutdown hook");
238                 this.setContextClassLoader(null);
239             }
240 
241             @Override
242             public void run() {
243                 p.destroy();
244             }
245         };
246 
247         ShutdownHookUtils.addShutDownHook(processHook);
248 
249         return new CommandLineCallable() {
250 
251             @Override
252             public Integer call() throws CommandLineException {
253                 StreamPollFeeder inputFeeder = null;
254                 StreamPumper outputPumper = null;
255                 StreamPumper errorPumper = null;
256                 try {
257                     if (systemIn != null) {
258                         inputFeeder = new StreamPollFeeder(systemIn, p.getOutputStream());
259                         inputFeeder.setName("StreamPollFeeder-systemIn");
260                         inputFeeder.start();
261                     }
262 
263                     outputPumper = new StreamPumper(p.getInputStream(), systemOut);
264                     outputPumper.setName("StreamPumper-systemOut");
265                     outputPumper.start();
266 
267                     errorPumper = new StreamPumper(p.getErrorStream(), systemErr);
268                     errorPumper.setName("StreamPumper-systemErr");
269                     errorPumper.start();
270 
271                     if (timeoutInSeconds > 0 && !p.waitFor(timeoutInSeconds, TimeUnit.SECONDS)) {
272                         throw new CommandLineTimeOutException(
273                                 String.format("Process timed out after %d seconds.", timeoutInSeconds));
274                     }
275 
276                     int returnValue = p.waitFor();
277 
278                     // TODO Find out if waitUntilDone needs to be called using a try-finally construct. The method may
279                     // throw an
280                     //      InterruptedException so that calls to waitUntilDone may be skipped.
281                     //                    try
282                     //                    {
283                     //                        if ( inputFeeder != null )
284                     //                        {
285                     //                            inputFeeder.waitUntilDone();
286                     //                        }
287                     //                    }
288                     //                    finally
289                     //                    {
290                     //                        try
291                     //                        {
292                     //                            outputPumper.waitUntilDone();
293                     //                        }
294                     //                        finally
295                     //                        {
296                     //                            errorPumper.waitUntilDone();
297                     //                        }
298                     //                    }
299                     if (inputFeeder != null) {
300                         inputFeeder.waitUntilDone();
301                     }
302 
303                     outputPumper.waitUntilDone();
304                     errorPumper.waitUntilDone();
305 
306                     if (inputFeeder != null && inputFeeder.getException() != null) {
307                         throw new CommandLineException("Failure processing stdin.", inputFeeder.getException());
308                     }
309 
310                     if (outputPumper.getException() != null) {
311                         throw new CommandLineException("Failure processing stdout.", outputPumper.getException());
312                     }
313 
314                     if (errorPumper.getException() != null) {
315                         throw new CommandLineException("Failure processing stderr.", errorPumper.getException());
316                     }
317 
318                     return returnValue;
319                 } catch (InterruptedException ex) {
320                     Thread.currentThread().interrupt();
321                     throw new CommandLineTimeOutException(
322                             "Error while executing external command, process killed.", ex);
323                 } finally {
324                     if (outputPumper != null) {
325                         outputPumper.disable();
326                     }
327                     if (errorPumper != null) {
328                         errorPumper.disable();
329                     }
330 
331                     try {
332                         if (runAfterProcessTermination != null) {
333                             runAfterProcessTermination.run();
334                         }
335                     } finally {
336                         ShutdownHookUtils.removeShutdownHook(processHook);
337                         processHook.run();
338                     }
339                 }
340             }
341         };
342     }
343 
344     /**
345      * Gets the shell environment variables for this process. Note that the returned mapping from variable names to
346      * values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>
347      * and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms
348      * with case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
349      *
350      * @return The shell environment variables, can be empty but never <code>null</code>.
351      * @deprecated use System#getenv()
352      */
353     @Deprecated
354     public static Properties getSystemEnvVars() {
355         return getSystemEnvVars(!Os.isFamily(Os.FAMILY_WINDOWS));
356     }
357 
358     /**
359      * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar
360      * keys will all be upper-case.
361      *
362      * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
363      * @return Properties object of (possibly modified) envar keys mapped to their values.
364      * @deprecated use System#getenv()
365      */
366     @Deprecated
367     public static Properties getSystemEnvVars(boolean caseSensitive) {
368         Map<String, String> envs = System.getenv();
369         return ensureCaseSensitivity(envs, caseSensitive);
370     }
371 
372     /**
373      * @param toProcess The command line to translate.
374      * @return The array of translated parts.
375      * @throws CommandLineException in case of unbalanced quotes.
376      */
377     public static String[] translateCommandline(String toProcess) throws CommandLineException {
378         if ((toProcess == null) || (toProcess.length() == 0)) {
379             return new String[0];
380         }
381 
382         // parse with a simple finite state machine
383 
384         final int normal = 0;
385         final int inQuote = 1;
386         final int inDoubleQuote = 2;
387         boolean inEscape = false;
388         int state = normal;
389         final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' \\", true);
390         List<String> tokens = new ArrayList<>();
391         StringBuilder current = new StringBuilder();
392 
393         while (tok.hasMoreTokens()) {
394             String nextTok = tok.nextToken();
395             switch (state) {
396                 case inQuote:
397                     if ("\'".equals(nextTok)) {
398                         if (inEscape) {
399                             current.append(nextTok);
400                             inEscape = false;
401                         } else {
402                             state = normal;
403                         }
404                     } else {
405                         current.append(nextTok);
406                         inEscape = "\\".equals(nextTok);
407                     }
408                     break;
409                 case inDoubleQuote:
410                     if ("\"".equals(nextTok)) {
411                         if (inEscape) {
412                             current.append(nextTok);
413                             inEscape = false;
414                         } else {
415                             state = normal;
416                         }
417                     } else {
418                         current.append(nextTok);
419                         inEscape = "\\".equals(nextTok);
420                     }
421                     break;
422                 default:
423                     if ("\'".equals(nextTok)) {
424                         if (inEscape) {
425                             inEscape = false;
426                             current.append(nextTok);
427                         } else {
428                             state = inQuote;
429                         }
430                     } else if ("\"".equals(nextTok)) {
431                         if (inEscape) {
432                             inEscape = false;
433                             current.append(nextTok);
434                         } else {
435                             state = inDoubleQuote;
436                         }
437                     } else if (" ".equals(nextTok)) {
438                         if (current.length() != 0) {
439                             tokens.add(current.toString());
440                             current.setLength(0);
441                         }
442                     } else {
443                         current.append(nextTok);
444                         inEscape = "\\".equals(nextTok);
445                     }
446                     break;
447             }
448         }
449 
450         if (current.length() != 0) {
451             tokens.add(current.toString());
452         }
453 
454         if ((state == inQuote) || (state == inDoubleQuote)) {
455             throw new CommandLineException("unbalanced quotes in " + toProcess);
456         }
457 
458         return tokens.toArray(new String[tokens.size()]);
459     }
460 
461     /**
462      * @param line the lines
463      * @return the concatenated lines, quoted and escaped, separated by spaces
464      */
465     public static String toString(String... line) {
466         // empty path return empty string
467         if ((line == null) || (line.length == 0)) {
468             return "";
469         }
470 
471         final StringBuilder result = new StringBuilder();
472         for (int i = 0; i < line.length; i++) {
473             if (i > 0) {
474                 result.append(' ');
475             }
476             result.append(StringUtils.quoteAndEscape(line[i], '\"'));
477         }
478         return result.toString();
479     }
480 
481     static Properties ensureCaseSensitivity(Map<String, String> envs, boolean preserveKeyCase) {
482         Properties envVars = new Properties();
483         for (Map.Entry<String, String> entry : envs.entrySet()) {
484             envVars.put(
485                     !preserveKeyCase ? entry.getKey().toUpperCase(Locale.ENGLISH) : entry.getKey(), entry.getValue());
486         }
487         return envVars;
488     }
489 }