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.nio.charset.Charset;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28 import java.util.Properties;
29 import java.util.StringTokenizer;
30
31 import javax.annotation.Nonnull;
32 import javax.annotation.Nullable;
33
34 import org.apache.maven.shared.utils.Os;
35 import org.apache.maven.shared.utils.StringUtils;
36
37 /**
38 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugstøl </a>
39 * @version $Id$
40 */
41 public abstract class CommandLineUtils
42 {
43
44 /**
45 * A {@code StreamConsumer} providing consumed lines as a {@code String}.
46 *
47 * @see #getOutput()
48 */
49 public static class StringStreamConsumer
50 implements StreamConsumer
51 {
52
53 private final StringBuffer string = new StringBuffer();
54
55 private static final String LS = System.getProperty( "line.separator", "\n" );
56
57 /**
58 * {@inheritDoc}
59 */
60 @Override
61 public void consumeLine( String line )
62 {
63 string.append( line ).append( LS );
64 }
65
66 /**
67 * @return The output.
68 */
69 public String getOutput()
70 {
71 return string.toString();
72 }
73
74 }
75
76 /**
77 * Number of milliseconds per second.
78 */
79 private static final long MILLIS_PER_SECOND = 1000L;
80
81 /**
82 * Number of nanoseconds per second.
83 */
84 private static final long NANOS_PER_SECOND = 1000000000L;
85
86 /**
87 * @param cl The command line {@link Commandline}
88 * @param systemOut {@link StreamConsumer}
89 * @param systemErr {@link StreamConsumer}
90 * @return return code.
91 * @throws CommandLineException in case of a problem.
92 */
93 public static int executeCommandLine( @Nonnull Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr )
94 throws CommandLineException
95 {
96 return executeCommandLine( cl, null, systemOut, systemErr, 0 );
97 }
98
99 /**
100 * @param cl The command line {@link Commandline}
101 * @param systemOut {@link StreamConsumer}
102 * @param systemErr {@link StreamConsumer}
103 * @param timeoutInSeconds The timeout.
104 * @return return code.
105 * @throws CommandLineException in case of a problem.
106 */
107 public static int executeCommandLine( @Nonnull Commandline cl, StreamConsumer systemOut, StreamConsumer systemErr,
108 int timeoutInSeconds )
109 throws CommandLineException
110 {
111 return executeCommandLine( cl, null, systemOut, systemErr, timeoutInSeconds );
112 }
113
114 /**
115 * @param cl The command line {@link Commandline}
116 * @param systemIn {@link StreamConsumer}
117 * @param systemOut {@link StreamConsumer}
118 * @param systemErr {@link StreamConsumer}
119 * @return return code.
120 * @throws CommandLineException in case of a problem.
121 */
122 public static int executeCommandLine( @Nonnull Commandline cl, InputStream systemIn, StreamConsumer systemOut,
123 StreamConsumer systemErr )
124 throws CommandLineException
125 {
126 return executeCommandLine( cl, systemIn, systemOut, systemErr, 0 );
127 }
128
129 /**
130 * @param cl The command line to execute
131 * @param systemIn The input to read from, must be thread safe
132 * @param systemOut A consumer that receives output, must be thread safe
133 * @param systemErr A consumer that receives system error stream output, must be thread safe
134 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
135 * @return A return value, see {@link Process#exitValue()}
136 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
137 */
138 public static int executeCommandLine( @Nonnull Commandline cl, InputStream systemIn, StreamConsumer systemOut,
139 StreamConsumer systemErr, int timeoutInSeconds )
140 throws CommandLineException
141 {
142 return executeCommandLine( cl, systemIn, systemOut, systemErr, timeoutInSeconds, null );
143 }
144
145 /**
146 * @param cl The command line to execute
147 * @param systemIn The input to read from, must be thread safe
148 * @param systemOut A consumer that receives output, must be thread safe
149 * @param systemErr A consumer that receives system error stream output, must be thread safe
150 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
151 * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
152 * exceeded, but before waiting on the stream feeder and pumpers to finish.
153 * @return A return value, see {@link Process#exitValue()}
154 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
155 */
156 public static int executeCommandLine( @Nonnull Commandline cl, InputStream systemIn, StreamConsumer systemOut,
157 StreamConsumer systemErr, int timeoutInSeconds,
158 @Nullable Runnable runAfterProcessTermination )
159 throws CommandLineException
160 {
161 return executeCommandLine( cl, systemIn, systemOut, systemErr, timeoutInSeconds, runAfterProcessTermination,
162 null );
163 }
164
165 /**
166 * @param cl The command line to execute
167 * @param systemIn The input to read from, must be thread safe
168 * @param systemOut A consumer that receives output, must be thread safe
169 * @param systemErr A consumer that receives system error stream output, must be thread safe
170 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
171 * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
172 * exceeded, but before waiting on the stream feeder and pumpers to finish.
173 * @param streamCharset Charset to use for reading streams
174 * @return A return value, see {@link Process#exitValue()}
175 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
176 */
177 public static int executeCommandLine( @Nonnull Commandline cl, InputStream systemIn, StreamConsumer systemOut,
178 StreamConsumer systemErr, int timeoutInSeconds,
179 @Nullable Runnable runAfterProcessTermination,
180 @Nullable final Charset streamCharset )
181 throws CommandLineException
182 {
183 final CommandLineCallable future =
184 executeCommandLineAsCallable( cl, systemIn, systemOut, systemErr, timeoutInSeconds,
185 runAfterProcessTermination, streamCharset );
186 return future.call();
187 }
188
189 /**
190 * Immediately forks a process, returns a callable that will block until process is complete.
191 *
192 * @param cl The command line to execute
193 * @param systemIn The input to read from, must be thread safe
194 * @param systemOut A consumer that receives output, must be thread safe
195 * @param systemErr A consumer that receives system error stream output, must be thread safe
196 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
197 * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
198 * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call"
199 * must be called on this to be sure the forked process has terminated, no guarantees is made about
200 * any internal state before after the completion of the call statements
201 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
202 */
203 public static CommandLineCallable executeCommandLineAsCallable( @Nonnull final Commandline cl,
204 @Nullable final InputStream systemIn,
205 final StreamConsumer systemOut,
206 final StreamConsumer systemErr,
207 final int timeoutInSeconds,
208 @Nullable final Runnable runAfterProcessTermination )
209 throws CommandLineException
210 {
211 return executeCommandLineAsCallable( cl, systemIn, systemOut, systemErr, timeoutInSeconds,
212 runAfterProcessTermination, null );
213 }
214
215 /**
216 * Immediately forks a process, returns a callable that will block until process is complete.
217 *
218 * @param cl The command line to execute
219 * @param systemIn The input to read from, must be thread safe
220 * @param systemOut A consumer that receives output, must be thread safe
221 * @param systemErr A consumer that receives system error stream output, must be thread safe
222 * @param timeoutInSeconds Positive integer to specify timeout, zero and negative integers for no timeout.
223 * @param runAfterProcessTermination Optional callback to run after the process terminated or the the timeout was
224 * @param streamCharset Charset to use for reading streams
225 * @return A CommandLineCallable that provides the process return value, see {@link Process#exitValue()}. "call"
226 * must be called on this to be sure the forked process has terminated, no guarantees is made about
227 * any internal state before after the completion of the call statements
228 * @throws CommandLineException or CommandLineTimeOutException if time out occurs
229 */
230 public static CommandLineCallable executeCommandLineAsCallable( @Nonnull final Commandline cl,
231 @Nullable final InputStream systemIn,
232 final StreamConsumer systemOut,
233 final StreamConsumer systemErr,
234 final int timeoutInSeconds,
235 @Nullable final Runnable runAfterProcessTermination,
236 @Nullable final Charset streamCharset )
237 throws CommandLineException
238 {
239 //noinspection ConstantConditions
240 if ( cl == null )
241 {
242 throw new IllegalArgumentException( "cl cannot be null." );
243 }
244
245 final Process p = cl.execute();
246
247 final Thread processHook = new Thread()
248 {
249
250 {
251 this.setName( "CommandLineUtils process shutdown hook" );
252 this.setContextClassLoader( null );
253 }
254
255 @Override
256 public void run()
257 {
258 p.destroy();
259 }
260
261 };
262
263 ShutdownHookUtils.addShutDownHook( processHook );
264
265 return new CommandLineCallable()
266 {
267
268 @Override
269 public Integer call()
270 throws CommandLineException
271 {
272 StreamFeeder inputFeeder = null;
273 StreamPumper outputPumper = null;
274 StreamPumper errorPumper = null;
275 try
276 {
277 if ( systemIn != null )
278 {
279 inputFeeder = new StreamFeeder( systemIn, p.getOutputStream() );
280 inputFeeder.start();
281 }
282
283 outputPumper = new StreamPumper( p.getInputStream(), systemOut );
284 outputPumper.start();
285
286 errorPumper = new StreamPumper( p.getErrorStream(), systemErr );
287 errorPumper.start();
288
289 int returnValue;
290 if ( timeoutInSeconds <= 0 )
291 {
292 returnValue = p.waitFor();
293 }
294 else
295 {
296 final long now = System.nanoTime();
297 final long timeout = now + NANOS_PER_SECOND * timeoutInSeconds;
298 while ( isAlive( p ) && ( System.nanoTime() < timeout ) )
299 {
300 // The timeout is specified in seconds. Therefore we must not sleep longer than one second
301 // but we should sleep as long as possible to reduce the number of iterations performed.
302 Thread.sleep( MILLIS_PER_SECOND - 1L );
303 }
304
305 if ( isAlive( p ) )
306 {
307 throw new InterruptedException( String.format( "Process timed out after %d seconds.",
308 timeoutInSeconds ) );
309
310 }
311
312 returnValue = p.exitValue();
313 }
314
315 // TODO Find out if waitUntilDone needs to be called using a try-finally construct. The method may throw an
316 // InterruptedException so that calls to waitUntilDone may be skipped.
317 // try
318 // {
319 // if ( inputFeeder != null )
320 // {
321 // inputFeeder.waitUntilDone();
322 // }
323 // }
324 // finally
325 // {
326 // try
327 // {
328 // outputPumper.waitUntilDone();
329 // }
330 // finally
331 // {
332 // errorPumper.waitUntilDone();
333 // }
334 // }
335 if ( inputFeeder != null )
336 {
337 inputFeeder.waitUntilDone();
338 }
339
340 outputPumper.waitUntilDone();
341 errorPumper.waitUntilDone();
342
343 if ( inputFeeder != null )
344 {
345 inputFeeder.close();
346
347 if ( inputFeeder.getException() != null )
348 {
349 throw new CommandLineException( "Failure processing stdin.", inputFeeder.getException() );
350 }
351 }
352
353 if ( outputPumper.getException() != null )
354 {
355 throw new CommandLineException( "Failure processing stdout.", outputPumper.getException() );
356 }
357
358 if ( errorPumper.getException() != null )
359 {
360 throw new CommandLineException( "Failure processing stderr.", errorPumper.getException() );
361 }
362
363 return returnValue;
364 }
365 catch ( InterruptedException ex )
366 {
367 throw new CommandLineTimeOutException( "Error while executing external command, process killed.",
368 ex );
369
370 }
371 finally
372 {
373 if ( inputFeeder != null )
374 {
375 inputFeeder.disable();
376 }
377 if ( outputPumper != null )
378 {
379 outputPumper.disable();
380 }
381 if ( errorPumper != null )
382 {
383 errorPumper.disable();
384 }
385
386 try
387 {
388 if ( runAfterProcessTermination != null )
389 {
390 runAfterProcessTermination.run();
391 }
392 }
393 finally
394 {
395 ShutdownHookUtils.removeShutdownHook( processHook );
396
397 try
398 {
399 processHook.run();
400 }
401 finally
402 {
403 if ( inputFeeder != null )
404 {
405 inputFeeder.close();
406 }
407 }
408 }
409 }
410 }
411
412 };
413 }
414
415 /**
416 * Gets the shell environment variables for this process. Note that the returned mapping from variable names to
417 * values will always be case-sensitive regardless of the platform, i.e. <code>getSystemEnvVars().get("path")</code>
418 * and <code>getSystemEnvVars().get("PATH")</code> will in general return different values. However, on platforms
419 * with case-insensitive environment variables like Windows, all variable names will be normalized to upper case.
420 *
421 * @return The shell environment variables, can be empty but never <code>null</code>.
422 * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result
423 * <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
424 */
425 public static Properties getSystemEnvVars()
426 {
427 return getSystemEnvVars( !Os.isFamily( Os.FAMILY_WINDOWS ) );
428 }
429
430 /**
431 * Return the shell environment variables. If <code>caseSensitive == true</code>, then envar
432 * keys will all be upper-case.
433 *
434 * @param caseSensitive Whether environment variable keys should be treated case-sensitively.
435 * @return Properties object of (possibly modified) envar keys mapped to their values.
436 * @see System#getenv() System.getenv() API, new in JDK 5.0, to get the same result
437 * <b>since 2.0.2 System#getenv() will be used if available in the current running jvm.</b>
438 */
439 public static Properties getSystemEnvVars( boolean caseSensitive )
440 {
441 Map<String, String> envs = System.getenv();
442 return ensureCaseSensitivity( envs, caseSensitive );
443 }
444
445 private static boolean isAlive( Process p )
446 {
447 if ( p == null )
448 {
449 return false;
450 }
451
452 try
453 {
454 p.exitValue();
455 return false;
456 }
457 catch ( IllegalThreadStateException e )
458 {
459 return true;
460 }
461 }
462
463 /**
464 * @param toProcess The command line to translate.
465 * @return The array of translated parts.
466 * @throws CommandLineException in case of unbalanced quotes.
467 */
468 public static String[] translateCommandline( String toProcess ) throws CommandLineException
469 {
470 if ( ( toProcess == null ) || ( toProcess.length() == 0 ) )
471 {
472 return new String[0];
473 }
474
475 // parse with a simple finite state machine
476
477 final int normal = 0;
478 final int inQuote = 1;
479 final int inDoubleQuote = 2;
480 int state = normal;
481 StringTokenizer tok = new StringTokenizer( toProcess, "\"\' ", true );
482 List<String> tokens = new ArrayList<String>();
483 StringBuilder current = new StringBuilder();
484
485 while ( tok.hasMoreTokens() )
486 {
487 String nextTok = tok.nextToken();
488 switch ( state )
489 {
490 case inQuote:
491 if ( "\'".equals( nextTok ) )
492 {
493 state = normal;
494 }
495 else
496 {
497 current.append( nextTok );
498 }
499 break;
500 case inDoubleQuote:
501 if ( "\"".equals( nextTok ) )
502 {
503 state = normal;
504 }
505 else
506 {
507 current.append( nextTok );
508 }
509 break;
510 default:
511 if ( "\'".equals( nextTok ) )
512 {
513 state = inQuote;
514 }
515 else if ( "\"".equals( nextTok ) )
516 {
517 state = inDoubleQuote;
518 }
519 else if ( " ".equals( nextTok ) )
520 {
521 if ( current.length() != 0 )
522 {
523 tokens.add( current.toString() );
524 current.setLength( 0 );
525 }
526 }
527 else
528 {
529 current.append( nextTok );
530 }
531 break;
532 }
533 }
534
535 if ( current.length() != 0 )
536 {
537 tokens.add( current.toString() );
538 }
539
540 if ( ( state == inQuote ) || ( state == inDoubleQuote ) )
541 {
542 throw new CommandLineException( "unbalanced quotes in " + toProcess );
543 }
544
545 return tokens.toArray( new String[tokens.size()] );
546 }
547
548 /**
549 * @param line The line
550 * @return The concatenate lines.
551 */
552 public static String toString( String... line )
553 {
554 // empty path return empty string
555 if ( ( line == null ) || ( line.length == 0 ) )
556 {
557 return "";
558 }
559
560 // path containing one or more elements
561 final StringBuilder result = new StringBuilder();
562 for ( int i = 0; i < line.length; i++ )
563 {
564 if ( i > 0 )
565 {
566 result.append( ' ' );
567 }
568 try
569 {
570 result.append( StringUtils.quoteAndEscape( line[i], '\"' ) );
571 }
572 catch ( Exception e )
573 {
574 System.err.println( "Error quoting argument: " + e.getMessage() );
575 }
576 }
577 return result.toString();
578 }
579
580 static Properties ensureCaseSensitivity( Map<String, String> envs, boolean preserveKeyCase )
581 {
582 Properties envVars = new Properties();
583 for ( Map.Entry<String, String> entry : envs.entrySet() )
584 {
585 envVars.put( !preserveKeyCase ? entry.getKey().toUpperCase( Locale.ENGLISH ) : entry.getKey(),
586 entry.getValue() );
587 }
588 return envVars;
589 }
590 }