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