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