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