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