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