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