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