View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * Recognizes PID of Plugin process and determines lifetime.
61   *
62   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
63   * @since 2.20.1
64   */
65  final class PpidChecker {
66      private static final long MINUTES_TO_MILLIS = 60L * 1000L;
67      // 25 chars https://superuser.com/questions/937380/get-creation-time-of-file-in-milliseconds/937401#937401
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       * The etime is in the form of [[dd-]hh:]mm:ss on Unix like systems.
84       * See the workaround https://issues.apache.org/jira/browse/SUREFIRE-1451.
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      * This method can be called only after {@link #canUse()} has returned {@code true}.
109      *
110      * @return {@code true} if parent process is alive; {@code false} otherwise
111      * @throws IllegalStateException if {@link #canUse()} returns {@code false}, error to read process
112      *                               or this object has been {@link #destroyActiveCommands() destroyed}
113      * @throws NullPointerException if extracted e-time is null
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             // let's compare creation time, should be same unless killed or PID is reused by OS into another process
126             return !parentProcessInfo.isInvalid()
127                     && (previousInfo == null || parentProcessInfo.isTimeEqualTo(previousInfo));
128         } else if (IS_OS_UNIX) {
129             parentProcessInfo = unix();
130             checkProcessInfo();
131 
132             // let's compare elapsed time, should be greater or equal if parent process is the same and still alive
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     // https://www.freebsd.org/cgi/man.cgi?ps(1)
152     // etimes elapsed running time, in decimal integer seconds
153 
154     // http://manpages.ubuntu.com/manpages/xenial/man1/ps.1.html
155     // etimes elapsed time since the process was started, in seconds.
156 
157     // http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/test/java/lang/ProcessBuilder/Basic.java#L167
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                         // now the line is CreationDate, e.g. 20180406142327.741074+120
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      * The beginning part of Windows WMIC format yyyymmddHHMMSS.xxx <br>
310      * https://technet.microsoft.com/en-us/library/ee198928.aspx <br>
311      * We use UTC time zone which avoids DST changes, see SUREFIRE-1512.
312      *
313      * @return Windows WMIC format yyyymmddHHMMSS.xxx
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      * Reads standard output from {@link Process}.
327      * <br>
328      * The artifact maven-shared-utils has non-daemon Threads which is an issue in Surefire to satisfy System.exit.
329      * This implementation is taylor made without using any Thread.
330      * It's easy to destroy Process from other Thread.
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) // force to run shell commands in UNIX Standard mode on 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                 //noinspection ResultOfMethodCallIgnored
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                         // cannot do anything about it, the dump file writes would fail as well
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 }