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