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