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