View Javadoc
1   package org.apache.maven.surefire.booter;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.nio.charset.Charset;
25  import java.text.SimpleDateFormat;
26  import java.util.Queue;
27  import java.util.Scanner;
28  import java.util.TimeZone;
29  import java.util.concurrent.ConcurrentLinkedQueue;
30  import java.util.regex.Matcher;
31  import java.util.regex.Pattern;
32  
33  import static java.lang.Integer.parseInt;
34  import static java.lang.Long.parseLong;
35  import static java.util.concurrent.TimeUnit.DAYS;
36  import static java.util.concurrent.TimeUnit.HOURS;
37  import static java.util.concurrent.TimeUnit.MINUTES;
38  import static java.util.regex.Pattern.compile;
39  import static org.apache.commons.io.IOUtils.closeQuietly;
40  import static org.apache.commons.lang3.StringUtils.isNotBlank;
41  import static org.apache.commons.lang3.SystemUtils.IS_OS_HP_UX;
42  import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
43  import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
44  import static org.apache.maven.surefire.booter.ProcessInfo.unixProcessInfo;
45  import static org.apache.maven.surefire.booter.ProcessInfo.windowsProcessInfo;
46  import static org.apache.maven.surefire.booter.ProcessInfo.ERR_PROCESS_INFO;
47  import static org.apache.maven.surefire.booter.ProcessInfo.INVALID_PROCESS_INFO;
48  
49  /**
50   * Recognizes PID of Plugin process and determines lifetime.
51   *
52   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
53   * @since 2.20.1
54   */
55  final class PpidChecker
56  {
57      private static final int MINUTES_TO_MILLIS = 60 * 1000;
58      // 25 chars https://superuser.com/questions/937380/get-creation-time-of-file-in-milliseconds/937401#937401
59      private static final int WMIC_CREATION_DATE_VALUE_LENGTH = 25;
60      private static final int WMIC_CREATION_DATE_TIMESTAMP_LENGTH = 18;
61      private static final SimpleDateFormat WMIC_CREATION_DATE_FORMAT =
62              IS_OS_WINDOWS ? createWindowsCreationDateFormat() : null;
63      private static final String WMIC_CREATION_DATE = "CreationDate";
64      private static final String WINDOWS_SYSTEM_ROOT_ENV = "SystemRoot";
65      private static final String RELATIVE_PATH_TO_WMIC = "System32\\Wbem";
66      private static final String SYSTEM_PATH_TO_WMIC =
67              "%" + WINDOWS_SYSTEM_ROOT_ENV + "%\\" + RELATIVE_PATH_TO_WMIC + "\\";
68  
69      private final Queue<Process> destroyableCommands = new ConcurrentLinkedQueue<>();
70  
71      /**
72       * The etime is in the form of [[dd-]hh:]mm:ss on Unix like systems.
73       * See the workaround https://issues.apache.org/jira/browse/SUREFIRE-1451.
74       */
75      static final Pattern UNIX_CMD_OUT_PATTERN = compile( "^(((\\d+)-)?(\\d{1,2}):)?(\\d{1,2}):(\\d{1,2})$" );
76  
77      private final long ppid;
78  
79      private volatile ProcessInfo parentProcessInfo;
80      private volatile boolean stopped;
81  
82      PpidChecker( long ppid )
83      {
84          this.ppid = ppid;
85          //todo WARN logger (after new logger is finished) that (IS_OS_UNIX && canExecuteUnixPs()) is false
86      }
87  
88      boolean canUse()
89      {
90          final ProcessInfo ppi = parentProcessInfo;
91          return ppi == null ? IS_OS_WINDOWS || IS_OS_UNIX && canExecuteUnixPs() : ppi.canUse();
92      }
93  
94      /**
95       * This method can be called only after {@link #canUse()} has returned {@code true}.
96       *
97       * @return {@code true} if parent process is alive; {@code false} otherwise
98       * @throws IllegalStateException if {@link #canUse()} returns {@code false}, error to read process
99       *                               or this object has been {@link #destroyActiveCommands() destroyed}
100      * @throws NullPointerException if extracted e-time is null
101      */
102     @SuppressWarnings( "unchecked" )
103     boolean isProcessAlive()
104     {
105         if ( !canUse() )
106         {
107             throw new IllegalStateException( "irrelevant to call isProcessAlive()" );
108         }
109 
110         final ProcessInfo previousInfo = parentProcessInfo;
111         try
112         {
113             if ( IS_OS_WINDOWS )
114             {
115                 parentProcessInfo = windows();
116                 checkProcessInfo();
117 
118                 // let's compare creation time, should be same unless killed or PID is reused by OS into another process
119                 return previousInfo == null || parentProcessInfo.isTimeEqualTo( previousInfo );
120             }
121             else if ( IS_OS_UNIX )
122             {
123                 parentProcessInfo = unix();
124                 checkProcessInfo();
125 
126                 // let's compare elapsed time, should be greater or equal if parent process is the same and still alive
127                 return previousInfo == null || !parentProcessInfo.isTimeBefore( previousInfo );
128             }
129 
130             throw new IllegalStateException( "unknown platform or you did not call canUse() before isProcessAlive()" );
131         }
132         finally
133         {
134             if ( parentProcessInfo == null )
135             {
136                 parentProcessInfo = INVALID_PROCESS_INFO;
137             }
138         }
139     }
140 
141     private void checkProcessInfo()
142     {
143         if ( isStopped() )
144         {
145             throw new IllegalStateException( "error [STOPPED] to read process " + ppid );
146         }
147 
148         if ( parentProcessInfo.isError() )
149         {
150             throw new IllegalStateException( "error to read process " + ppid );
151         }
152 
153         if ( !parentProcessInfo.canUse() )
154         {
155             throw new IllegalStateException( "Cannot use PPID " + ppid + " process information. "
156                     + "Going to use NOOP events." );
157         }
158     }
159 
160     // https://www.freebsd.org/cgi/man.cgi?ps(1)
161     // etimes elapsed running time, in decimal integer seconds
162 
163     // http://manpages.ubuntu.com/manpages/xenial/man1/ps.1.html
164     // etimes elapsed time since the process was started, in seconds.
165 
166     // http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/test/java/lang/ProcessBuilder/Basic.java#L167
167     ProcessInfo unix()
168     {
169         ProcessInfoConsumer reader = new ProcessInfoConsumer( Charset.defaultCharset().name() )
170         {
171             @Override
172             ProcessInfo consumeLine( String line, ProcessInfo previousOutputLine )
173             {
174                 if ( previousOutputLine.isInvalid() )
175                 {
176                     Matcher matcher = UNIX_CMD_OUT_PATTERN.matcher( line );
177                     if ( matcher.matches() )
178                     {
179                         long pidUptime = fromDays( matcher )
180                                                  + fromHours( matcher )
181                                                  + fromMinutes( matcher )
182                                                  + fromSeconds( matcher );
183                         return unixProcessInfo( ppid, pidUptime );
184                     }
185                 }
186                 return previousOutputLine;
187             }
188         };
189 
190         return reader.execute( "/bin/sh", "-c", unixPathToPS() + " -o etime= -p " + ppid );
191     }
192 
193     ProcessInfo windows()
194     {
195         ProcessInfoConsumer reader = new ProcessInfoConsumer( "US-ASCII" )
196         {
197             private boolean hasHeader;
198 
199             @Override
200             ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo ) throws Exception
201             {
202                 if ( previousProcessInfo.isInvalid() && !line.isEmpty() )
203                 {
204                     if ( hasHeader )
205                     {
206                         // now the line is CreationDate, e.g. 20180406142327.741074+120
207                         if ( line.length() != WMIC_CREATION_DATE_VALUE_LENGTH )
208                         {
209                             throw new IllegalStateException( "WMIC CreationDate should have 25 characters "
210                                     + line );
211                         }
212                         String startTimestamp = line.substring( 0, WMIC_CREATION_DATE_TIMESTAMP_LENGTH );
213                         int indexOfTimeZone = WMIC_CREATION_DATE_VALUE_LENGTH - 4;
214                         long startTimestampMillisUTC =
215                                 WMIC_CREATION_DATE_FORMAT.parse( startTimestamp ).getTime()
216                                         - parseInt( line.substring( indexOfTimeZone ) ) * MINUTES_TO_MILLIS;
217                         return windowsProcessInfo( ppid, startTimestampMillisUTC );
218                     }
219                     else
220                     {
221                         hasHeader = WMIC_CREATION_DATE.equals( line );
222                     }
223                 }
224                 return previousProcessInfo;
225             }
226         };
227         String wmicPath = hasWmicStandardSystemPath() ? SYSTEM_PATH_TO_WMIC : "";
228         return reader.execute( "CMD", "/A", "/X", "/C",
229                 wmicPath + "wmic process where (ProcessId=" + ppid + ") get " + WMIC_CREATION_DATE
230         );
231     }
232 
233     void destroyActiveCommands()
234     {
235         stopped = true;
236         for ( Process p = destroyableCommands.poll(); p != null; p = destroyableCommands.poll() )
237         {
238             p.destroy();
239         }
240     }
241 
242     private boolean isStopped()
243     {
244         return stopped;
245     }
246 
247     private static String unixPathToPS()
248     {
249         return canExecuteLocalUnixPs() ? "/usr/bin/ps" : "/bin/ps";
250     }
251 
252     static boolean canExecuteUnixPs()
253     {
254         return canExecuteLocalUnixPs() || canExecuteStandardUnixPs();
255     }
256 
257     private static boolean canExecuteLocalUnixPs()
258     {
259         return new File( "/usr/bin/ps" ).canExecute();
260     }
261 
262     private static boolean canExecuteStandardUnixPs()
263     {
264         return new File( "/bin/ps" ).canExecute();
265     }
266 
267     private static boolean hasWmicStandardSystemPath()
268     {
269         String systemRoot = System.getenv( WINDOWS_SYSTEM_ROOT_ENV );
270         return isNotBlank( systemRoot ) && new File( systemRoot, RELATIVE_PATH_TO_WMIC + "\\wmic.exe" ).isFile();
271     }
272 
273     static long fromDays( Matcher matcher )
274     {
275         String s = matcher.group( 3 );
276         return s == null ? 0L : DAYS.toSeconds( parseLong( s ) );
277     }
278 
279     static long fromHours( Matcher matcher )
280     {
281         String s = matcher.group( 4 );
282         return s == null ? 0L : HOURS.toSeconds( parseLong( s ) );
283     }
284 
285     static long fromMinutes( Matcher matcher )
286     {
287         String s = matcher.group( 5 );
288         return s == null ? 0L : MINUTES.toSeconds( parseLong( s ) );
289     }
290 
291     static long fromSeconds( Matcher matcher )
292     {
293         String s = matcher.group( 6 );
294         return s == null ? 0L : parseLong( s );
295     }
296 
297     private static void checkValid( Scanner scanner )
298             throws IOException
299     {
300         IOException exception = scanner.ioException();
301         if ( exception != null )
302         {
303             throw exception;
304         }
305     }
306 
307     /**
308      * The beginning part of Windows WMIC format yyyymmddHHMMSS.xxx <br>
309      * https://technet.microsoft.com/en-us/library/ee198928.aspx <br>
310      * We use UTC time zone which avoids DST changes, see SUREFIRE-1512.
311      *
312      * @return Windows WMIC format yyyymmddHHMMSS.xxx
313      */
314     private static SimpleDateFormat createWindowsCreationDateFormat()
315     {
316         SimpleDateFormat formatter = new SimpleDateFormat( "yyyyMMddHHmmss'.'SSS" );
317         formatter.setTimeZone( TimeZone.getTimeZone( "UTC" ) );
318         return formatter;
319     }
320 
321     /**
322      * Reads standard output from {@link Process}.
323      * <br>
324      * The artifact maven-shared-utils has non-daemon Threads which is an issue in Surefire to satisfy System.exit.
325      * This implementation is taylor made without using any Thread.
326      * It's easy to destroy Process from other Thread.
327      */
328     private abstract class ProcessInfoConsumer
329     {
330         private final String charset;
331 
332         ProcessInfoConsumer( String charset )
333         {
334             this.charset = charset;
335         }
336 
337         abstract ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo ) throws Exception;
338 
339         ProcessInfo execute( String... command )
340         {
341             ProcessBuilder processBuilder = new ProcessBuilder( command );
342             processBuilder.redirectErrorStream( true );
343             Process process = null;
344             ProcessInfo processInfo = INVALID_PROCESS_INFO;
345             try
346             {
347                 if ( IS_OS_HP_UX ) // force to run shell commands in UNIX Standard mode on HP-UX
348                 {
349                     processBuilder.environment().put( "UNIX95", "1" );
350                 }
351                 process = processBuilder.start();
352                 destroyableCommands.add( process );
353                 Scanner scanner = new Scanner( process.getInputStream(), charset );
354                 while ( scanner.hasNextLine() )
355                 {
356                     String line = scanner.nextLine().trim();
357                     processInfo = consumeLine( line, processInfo );
358                 }
359                 checkValid( scanner );
360                 int exitCode = process.waitFor();
361                 return exitCode == 0 ? processInfo : INVALID_PROCESS_INFO;
362             }
363             catch ( Exception e )
364             {
365                 DumpErrorSingleton.getSingleton()
366                         .dumpException( e );
367 
368                 return ERR_PROCESS_INFO;
369             }
370             finally
371             {
372                 if ( process != null )
373                 {
374                     destroyableCommands.remove( process );
375                     process.destroy();
376                     closeQuietly( process.getInputStream() );
377                     closeQuietly( process.getErrorStream() );
378                     closeQuietly( process.getOutputStream() );
379                 }
380             }
381         }
382     }
383 
384 }