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