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