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