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