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