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  import javax.annotation.Nonnull;
34  import javax.annotation.Nullable;
35  
36  /**
37   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l </a>
38   * @version $Id: CommandLineUtils.html 925654 2014-10-13 20:12:40Z 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( @Nonnull 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( @Nonnull 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( @Nonnull 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( @Nonnull 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( @Nonnull Commandline cl, InputStream systemIn, StreamConsumer systemOut,
135                                           StreamConsumer systemErr, int timeoutInSeconds,
136                                           @Nullable 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     public static CommandLineCallable executeCommandLineAsCallable( @Nonnull final Commandline cl,
160                                                                     @Nullable final InputStream systemIn,
161                                                                     final StreamConsumer systemOut,
162                                                                     final StreamConsumer systemErr,
163                                                                     final int timeoutInSeconds,
164                                                                     @Nullable final Runnable runAfterProcessTermination )
165         throws CommandLineException
166     {
167         //noinspection ConstantConditions
168         if ( cl == null )
169         {
170             throw new IllegalArgumentException( "cl cannot be null." );
171         }
172 
173         final Process p = cl.execute();
174 
175         final StreamFeeder inputFeeder = systemIn != null ? new StreamFeeder( systemIn, p.getOutputStream() ) : null;
176 
177         final StreamPumper outputPumper = new StreamPumper( p.getInputStream(), systemOut );
178 
179         final StreamPumper errorPumper = new StreamPumper( p.getErrorStream(), systemErr );
180 
181         if ( inputFeeder != null )
182         {
183             inputFeeder.start();
184         }
185 
186         outputPumper.start();
187 
188         errorPumper.start();
189 
190         final ProcessHook processHook = new ProcessHook( p );
191 
192         ShutdownHookUtils.addShutDownHook( processHook );
193 
194         return new CommandLineCallable()
195         {
196             public Integer call()
197                 throws CommandLineException
198             {
199                 try
200                 {
201                     int returnValue;
202                     if ( timeoutInSeconds <= 0 )
203                     {
204                         returnValue = p.waitFor();
205                     }
206                     else
207                     {
208                         long now = System.currentTimeMillis();
209                         long timeoutInMillis = 1000L * timeoutInSeconds;
210                         long finish = now + timeoutInMillis;
211                         while ( isAlive( p ) && ( System.currentTimeMillis() < finish ) )
212                         {
213                             Thread.sleep( 10 );
214                         }
215                         if ( isAlive( p ) )
216                         {
217                             throw new InterruptedException(
218                                 "Process timeout out after " + timeoutInSeconds + " seconds" );
219                         }
220 
221                         returnValue = p.exitValue();
222                     }
223 
224                     if ( runAfterProcessTermination != null )
225                     {
226                         runAfterProcessTermination.run();
227                     }
228 
229                     waitForAllPumpers( inputFeeder, outputPumper, errorPumper );
230 
231                     if ( outputPumper.getException() != null )
232                     {
233                         throw new CommandLineException( "Error inside systemOut parser", outputPumper.getException() );
234                     }
235 
236                     if ( errorPumper.getException() != null )
237                     {
238                         throw new CommandLineException( "Error inside systemErr parser", errorPumper.getException() );
239                     }
240 
241                     return returnValue;
242                 }
243                 catch ( InterruptedException ex )
244                 {
245                     if ( inputFeeder != null )
246                     {
247                         inputFeeder.disable();
248                     }
249 
250                     outputPumper.disable();
251                     errorPumper.disable();
252                     throw new CommandLineTimeOutException( "Error while executing external command, process killed.",
253                                                            ex );
254                 }
255                 finally
256                 {
257                     ShutdownHookUtils.removeShutdownHook( processHook );
258 
259                     processHook.run();
260 
261                     if ( inputFeeder != null )
262                     {
263                         inputFeeder.close();
264                     }
265 
266                     outputPumper.close();
267 
268                     errorPumper.close();
269                 }
270             }
271         };
272     }
273 
274     private static void waitForAllPumpers( @Nullable StreamFeeder inputFeeder, StreamPumper outputPumper,
275                                            StreamPumper errorPumper )
276         throws InterruptedException
277     {
278         if ( inputFeeder != null )
279         {
280             inputFeeder.waitUntilDone();
281         }
282 
283         outputPumper.waitUntilDone();
284         errorPumper.waitUntilDone();
285     }
286 
287     /**
288      * Gets the shell environment variables for this process. Note that the returned mapping from variable names to
289      * values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>
290      * and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms
291      * with case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
292      *
293      * @return The shell environment variables, can be empty but never <code>null</code>.
294      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result
295      *      <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
296      */
297     public static Properties getSystemEnvVars()
298     {
299         return getSystemEnvVars( !Os.isFamily( Os.FAMILY_WINDOWS ) );
300     }
301 
302     /**
303      * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar
304      * keys will all be upper-case.
305      *
306      * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
307      * @return Properties object of (possibly modified) envar keys mapped to their values.
308      * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result
309      *      <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
310      */
311     public static Properties getSystemEnvVars( boolean caseSensitive )
312     {
313         Map<String, String> envs = System.getenv();
314         return ensureCaseSensitivity( envs, caseSensitive );
315     }
316 
317     private static boolean isAlive( Process p )
318     {
319         if ( p == null )
320         {
321             return false;
322         }
323 
324         try
325         {
326             p.exitValue();
327             return false;
328         }
329         catch ( IllegalThreadStateException e )
330         {
331             return true;
332         }
333     }
334 
335     public static String[] translateCommandline( String toProcess )
336         throws Exception
337     {
338         if ( ( toProcess == null ) || ( toProcess.length() == 0 ) )
339         {
340             return new String[0];
341         }
342 
343         // parse with a simple finite state machine
344 
345         final int normal = 0;
346         final int inQuote = 1;
347         final int inDoubleQuote = 2;
348         int state = normal;
349         StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
350         List<String> tokens = new ArrayList<String>();
351         StringBuilder current = new StringBuilder();
352 
353         while ( tok.hasMoreTokens() )
354         {
355             String nextTok = tok.nextToken();
356             switch ( state )
357             {
358                 case inQuote:
359                     if ( "\'".equals( nextTok ) )
360                     {
361                         state = normal;
362                     }
363                     else
364                     {
365                         current.append( nextTok );
366                     }
367                     break;
368                 case inDoubleQuote:
369                     if ( "\"".equals( nextTok ) )
370                     {
371                         state = normal;
372                     }
373                     else
374                     {
375                         current.append( nextTok );
376                     }
377                     break;
378                 default:
379                     if ( "\'".equals( nextTok ) )
380                     {
381                         state = inQuote;
382                     }
383                     else if ( "\"".equals( nextTok ) )
384                     {
385                         state = inDoubleQuote;
386                     }
387                     else if ( " ".equals( nextTok ) )
388                     {
389                         if ( current.length() != 0 )
390                         {
391                             tokens.add( current.toString() );
392                             current.setLength( 0 );
393                         }
394                     }
395                     else
396                     {
397                         current.append( nextTok );
398                     }
399                     break;
400             }
401         }
402 
403         if ( current.length() != 0 )
404         {
405             tokens.add( current.toString() );
406         }
407 
408         if ( ( state == inQuote ) || ( state == inDoubleQuote ) )
409         {
410             throw new CommandLineException( "unbalanced quotes in " + toProcess );
411         }
412 
413         return tokens.toArray( new String[tokens.size()] );
414     }
415 
416     public static String toString( String... line )
417     {
418         // empty path return empty string
419         if ( ( line == null ) || ( line.length == 0 ) )
420         {
421             return "";
422         }
423 
424         // path containing one or more elements
425         final StringBuilder result = new StringBuilder();
426         for ( int i = 0; i < line.length; i++ )
427         {
428             if ( i > 0 )
429             {
430                 result.append( ' ' );
431             }
432             try
433             {
434                 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
435             }
436             catch ( Exception e )
437             {
438                 System.err.println( "Error quoting argument: " + e.getMessage() );
439             }
440         }
441         return result.toString();
442     }
443 
444     static Properties ensureCaseSensitivity( Map<String, String> envs, boolean preserveKeyCase )
445     {
446         Properties envVars = new Properties();
447         for ( Map.Entry<String, String> entry : envs.entrySet() )
448         {
449             envVars.put( !preserveKeyCase ? entry.getKey().toUpperCase( Locale.ENGLISH ) : entry.getKey(),
450                          entry.getValue() );
451         }
452         return envVars;
453     }
454 }