1 package org.apache.maven.surefire.booter;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import javax.annotation.Nonnull;
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.charset.Charset;
26 import java.text.SimpleDateFormat;
27 import java.util.Queue;
28 import java.util.Scanner;
29 import java.util.TimeZone;
30 import java.util.concurrent.ConcurrentLinkedQueue;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33
34 import static java.lang.Integer.parseInt;
35 import static java.lang.Long.parseLong;
36 import static java.util.concurrent.TimeUnit.DAYS;
37 import static java.util.concurrent.TimeUnit.HOURS;
38 import static java.util.concurrent.TimeUnit.MINUTES;
39 import static java.util.regex.Pattern.compile;
40 import static org.apache.commons.io.IOUtils.closeQuietly;
41 import static org.apache.commons.lang3.StringUtils.isNotBlank;
42 import static org.apache.commons.lang3.SystemUtils.IS_OS_HP_UX;
43 import static org.apache.commons.lang3.SystemUtils.IS_OS_LINUX;
44 import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
45 import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
46 import static org.apache.maven.surefire.booter.ProcessInfo.unixProcessInfo;
47 import static org.apache.maven.surefire.booter.ProcessInfo.windowsProcessInfo;
48 import static org.apache.maven.surefire.booter.ProcessInfo.ERR_PROCESS_INFO;
49 import static org.apache.maven.surefire.booter.ProcessInfo.INVALID_PROCESS_INFO;
50 import static org.apache.maven.surefire.util.internal.StringUtils.NL;
51
52
53
54
55
56
57
58 final class PpidChecker
59 {
60 private static final int MINUTES_TO_MILLIS = 60 * 1000;
61
62 private static final int WMIC_CREATION_DATE_VALUE_LENGTH = 25;
63 private static final int WMIC_CREATION_DATE_TIMESTAMP_LENGTH = 18;
64 private static final SimpleDateFormat WMIC_CREATION_DATE_FORMAT =
65 IS_OS_WINDOWS ? createWindowsCreationDateFormat() : null;
66 private static final String WMIC_CREATION_DATE = "CreationDate";
67 private static final String WINDOWS_SYSTEM_ROOT_ENV = "SystemRoot";
68 private static final String RELATIVE_PATH_TO_WMIC = "System32\\Wbem";
69 private static final String SYSTEM_PATH_TO_WMIC =
70 "%" + WINDOWS_SYSTEM_ROOT_ENV + "%\\" + RELATIVE_PATH_TO_WMIC + "\\";
71 private static final String PS_ETIME_HEADER = "ELAPSED";
72 private static final String PS_PID_HEADER = "PID";
73
74 private final Queue<Process> destroyableCommands = new ConcurrentLinkedQueue<>();
75
76
77
78
79
80 static final Pattern UNIX_CMD_OUT_PATTERN = compile( "^(((\\d+)-)?(\\d{1,2}):)?(\\d{1,2}):(\\d{1,2})\\s+(\\d+)$" );
81
82 static final Pattern BUSYBOX_CMD_OUT_PATTERN = compile( "^(\\d+)[hH](\\d{1,2})\\s+(\\d+)$" );
83
84 private final String ppid;
85
86 private volatile ProcessInfo parentProcessInfo;
87 private volatile boolean stopped;
88
89 PpidChecker( @Nonnull String ppid )
90 {
91 this.ppid = ppid;
92 }
93
94 boolean canUse()
95 {
96 if ( isStopped() )
97 {
98 return false;
99 }
100 final ProcessInfo ppi = parentProcessInfo;
101 return ppi == null ? IS_OS_WINDOWS || IS_OS_UNIX && canExecuteUnixPs() : ppi.canUse();
102 }
103
104
105
106
107
108
109
110
111
112 boolean isProcessAlive()
113 {
114 if ( !canUse() )
115 {
116 throw new IllegalStateException( "irrelevant to call isProcessAlive()" );
117 }
118
119 final ProcessInfo previousInfo = parentProcessInfo;
120 if ( IS_OS_WINDOWS )
121 {
122 parentProcessInfo = windows();
123 checkProcessInfo();
124
125
126 return !parentProcessInfo.isInvalid()
127 && ( previousInfo == null || parentProcessInfo.isTimeEqualTo( previousInfo ) );
128 }
129 else if ( IS_OS_UNIX )
130 {
131 parentProcessInfo = unix();
132 checkProcessInfo();
133
134
135 return !parentProcessInfo.isInvalid()
136 && ( previousInfo == null || !parentProcessInfo.isTimeBefore( previousInfo ) );
137 }
138 parentProcessInfo = ERR_PROCESS_INFO;
139 throw new IllegalStateException( "unknown platform or you did not call canUse() before isProcessAlive()" );
140 }
141
142 private void checkProcessInfo()
143 {
144 if ( isStopped() )
145 {
146 throw new IllegalStateException( "error [STOPPED] to read process " + ppid );
147 }
148
149 if ( !parentProcessInfo.canUse() )
150 {
151 throw new IllegalStateException( "Cannot use PPID " + ppid + " process information. "
152 + "Going to use NOOP events." );
153 }
154 }
155
156
157
158
159
160
161
162
163 ProcessInfo unix()
164 {
165 ProcessInfoConsumer reader = new ProcessInfoConsumer( Charset.defaultCharset().name() )
166 {
167 @Override
168 @Nonnull
169 ProcessInfo consumeLine( String line, ProcessInfo previousOutputLine )
170 {
171 if ( previousOutputLine.isInvalid() )
172 {
173 if ( hasHeader )
174 {
175 Matcher matcher = UNIX_CMD_OUT_PATTERN.matcher( line );
176 if ( matcher.matches() && ppid.equals( fromPID( matcher ) ) )
177 {
178 long pidUptime = fromDays( matcher )
179 + fromHours( matcher )
180 + fromMinutes( matcher )
181 + fromSeconds( matcher );
182 return unixProcessInfo( ppid, pidUptime );
183 }
184 matcher = BUSYBOX_CMD_OUT_PATTERN.matcher( line );
185 if ( matcher.matches() && ppid.equals( fromBusyboxPID( matcher ) ) )
186 {
187 long pidUptime = fromBusyboxHours( matcher ) + fromBusyboxMinutes( matcher );
188 return unixProcessInfo( ppid, pidUptime );
189 }
190 }
191 else
192 {
193 hasHeader = line.contains( PS_ETIME_HEADER ) && line.contains( PS_PID_HEADER );
194 }
195 }
196 return previousOutputLine;
197 }
198 };
199 String cmd = unixPathToPS() + " -o etime,pid " + ( IS_OS_LINUX ? "" : "-p " ) + ppid;
200 return reader.execute( "/bin/sh", "-c", cmd );
201 }
202
203 ProcessInfo windows()
204 {
205 ProcessInfoConsumer reader = new ProcessInfoConsumer( "US-ASCII" )
206 {
207 @Override
208 @Nonnull
209 ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo ) throws Exception
210 {
211 if ( previousProcessInfo.isInvalid() && !line.isEmpty() )
212 {
213 if ( hasHeader )
214 {
215
216 if ( line.length() != WMIC_CREATION_DATE_VALUE_LENGTH )
217 {
218 throw new IllegalStateException( "WMIC CreationDate should have 25 characters "
219 + line );
220 }
221 String startTimestamp = line.substring( 0, WMIC_CREATION_DATE_TIMESTAMP_LENGTH );
222 int indexOfTimeZone = WMIC_CREATION_DATE_VALUE_LENGTH - 4;
223 long startTimestampMillisUTC =
224 WMIC_CREATION_DATE_FORMAT.parse( startTimestamp ).getTime()
225 - parseInt( line.substring( indexOfTimeZone ) ) * MINUTES_TO_MILLIS;
226 return windowsProcessInfo( ppid, startTimestampMillisUTC );
227 }
228 else
229 {
230 hasHeader = WMIC_CREATION_DATE.equals( line );
231 }
232 }
233 return previousProcessInfo;
234 }
235 };
236 String wmicPath = hasWmicStandardSystemPath() ? SYSTEM_PATH_TO_WMIC : "";
237 return reader.execute( "CMD", "/A", "/X", "/C",
238 wmicPath + "wmic process where (ProcessId=" + ppid + ") get " + WMIC_CREATION_DATE
239 );
240 }
241
242 void destroyActiveCommands()
243 {
244 stopped = true;
245 for ( Process p = destroyableCommands.poll(); p != null; p = destroyableCommands.poll() )
246 {
247 p.destroy();
248 }
249 }
250
251 private boolean isStopped()
252 {
253 return stopped;
254 }
255
256 private static String unixPathToPS()
257 {
258 return canExecuteLocalUnixPs() ? "/usr/bin/ps" : "/bin/ps";
259 }
260
261 static boolean canExecuteUnixPs()
262 {
263 return canExecuteLocalUnixPs() || canExecuteStandardUnixPs();
264 }
265
266 private static boolean canExecuteLocalUnixPs()
267 {
268 try
269 {
270 return new File( "/usr/bin/ps" ).canExecute();
271 }
272 catch ( SecurityException e )
273 {
274 return false;
275 }
276 }
277
278 private static boolean canExecuteStandardUnixPs()
279 {
280 try
281 {
282 return new File( "/bin/ps" ).canExecute();
283 }
284 catch ( SecurityException e )
285 {
286 return false;
287 }
288 }
289
290 private static boolean hasWmicStandardSystemPath()
291 {
292 String systemRoot = System.getenv( WINDOWS_SYSTEM_ROOT_ENV );
293 return isNotBlank( systemRoot ) && new File( systemRoot, RELATIVE_PATH_TO_WMIC + "\\wmic.exe" ).isFile();
294 }
295
296 static long fromDays( Matcher matcher )
297 {
298 String s = matcher.group( 3 );
299 return s == null ? 0L : DAYS.toSeconds( parseLong( s ) );
300 }
301
302 static long fromHours( Matcher matcher )
303 {
304 String s = matcher.group( 4 );
305 return s == null ? 0L : HOURS.toSeconds( parseLong( s ) );
306 }
307
308 static long fromMinutes( Matcher matcher )
309 {
310 String s = matcher.group( 5 );
311 return s == null ? 0L : MINUTES.toSeconds( parseLong( s ) );
312 }
313
314 static long fromSeconds( Matcher matcher )
315 {
316 String s = matcher.group( 6 );
317 return s == null ? 0L : parseLong( s );
318 }
319
320 static String fromPID( Matcher matcher )
321 {
322 return matcher.group( 7 );
323 }
324
325 static long fromBusyboxHours( Matcher matcher )
326 {
327 String s = matcher.group( 1 );
328 return s == null ? 0L : HOURS.toSeconds( parseLong( s ) );
329 }
330
331 static long fromBusyboxMinutes( Matcher matcher )
332 {
333 String s = matcher.group( 2 );
334 return s == null ? 0L : MINUTES.toSeconds( parseLong( s ) );
335 }
336
337 static String fromBusyboxPID( Matcher matcher )
338 {
339 return matcher.group( 3 );
340 }
341
342 private static void checkValid( Scanner scanner )
343 throws IOException
344 {
345 IOException exception = scanner.ioException();
346 if ( exception != null )
347 {
348 throw exception;
349 }
350 }
351
352
353
354
355
356
357
358
359 private static SimpleDateFormat createWindowsCreationDateFormat()
360 {
361 SimpleDateFormat formatter = new SimpleDateFormat( "yyyyMMddHHmmss'.'SSS" );
362 formatter.setTimeZone( TimeZone.getTimeZone( "UTC" ) );
363 return formatter;
364 }
365
366 public void stop()
367 {
368 stopped = true;
369 }
370
371
372
373
374
375
376
377
378 private abstract class ProcessInfoConsumer
379 {
380 private final String charset;
381
382 boolean hasHeader;
383
384 ProcessInfoConsumer( String charset )
385 {
386 this.charset = charset;
387 }
388
389 abstract @Nonnull ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo ) throws Exception;
390
391 ProcessInfo execute( String... command )
392 {
393 ProcessBuilder processBuilder = new ProcessBuilder( command );
394 processBuilder.redirectErrorStream( true );
395 Process process = null;
396 ProcessInfo processInfo = INVALID_PROCESS_INFO;
397 StringBuilder out = new StringBuilder( 64 );
398 try
399 {
400 if ( IS_OS_HP_UX )
401 {
402 processBuilder.environment().put( "UNIX95", "1" );
403 }
404 process = processBuilder.start();
405 destroyableCommands.add( process );
406 Scanner scanner = new Scanner( process.getInputStream(), charset );
407 while ( scanner.hasNextLine() )
408 {
409 String line = scanner.nextLine();
410 out.append( line ).append( NL );
411 processInfo = consumeLine( line.trim(), processInfo );
412 }
413 checkValid( scanner );
414 int exitCode = process.waitFor();
415 if ( exitCode != 0 || isStopped() )
416 {
417 out.append( "<<exit>> <<" ).append( exitCode ).append( ">>" ).append( NL );
418 DumpErrorSingleton.getSingleton()
419 .dumpText( out.toString() );
420 }
421 return isStopped() ? ERR_PROCESS_INFO : exitCode == 0 ? processInfo : INVALID_PROCESS_INFO;
422 }
423 catch ( Exception e )
424 {
425 if ( !( e instanceof InterruptedException ) && !( e.getCause() instanceof InterruptedException ) )
426 {
427 DumpErrorSingleton.getSingleton()
428 .dumpText( out.toString() );
429
430 DumpErrorSingleton.getSingleton()
431 .dumpException( e );
432 }
433
434 return ERR_PROCESS_INFO;
435 }
436 finally
437 {
438 if ( process != null )
439 {
440 destroyableCommands.remove( process );
441 process.destroy();
442 closeQuietly( process.getInputStream() );
443 closeQuietly( process.getErrorStream() );
444 closeQuietly( process.getOutputStream() );
445 }
446 }
447 }
448 }
449
450 @Override
451 public String toString()
452 {
453 String args = "ppid=" + ppid
454 + ", stopped=" + stopped;
455
456 ProcessInfo processInfo = parentProcessInfo;
457 if ( processInfo != null )
458 {
459 args += ", invalid=" + processInfo.isInvalid()
460 + ", error=" + processInfo.isError();
461 }
462
463 if ( IS_OS_UNIX )
464 {
465 args += ", canExecuteLocalUnixPs=" + canExecuteLocalUnixPs()
466 + ", canExecuteStandardUnixPs=" + canExecuteStandardUnixPs();
467 }
468
469 return "PpidChecker{" + args + '}';
470 }
471 }