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