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