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ø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 }