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.util.Queue;
26  import java.util.Scanner;
27  import java.util.StringTokenizer;
28  import java.util.concurrent.ConcurrentLinkedQueue;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import static java.lang.Long.parseLong;
33  import static java.util.concurrent.TimeUnit.DAYS;
34  import static java.util.concurrent.TimeUnit.HOURS;
35  import static java.util.concurrent.TimeUnit.MINUTES;
36  import static java.util.regex.Pattern.compile;
37  import static org.apache.commons.io.IOUtils.closeQuietly;
38  import static org.apache.commons.lang3.StringUtils.isNotBlank;
39  import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
40  import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
41  import static org.apache.maven.surefire.booter.ProcessInfo.ERR_PROCESS_INFO;
42  import static org.apache.maven.surefire.booter.ProcessInfo.INVALID_PROCESS_INFO;
43  
44  /**
45   * Recognizes PID of Plugin process and determines lifetime.
46   *
47   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
48   * @since 2.20.1
49   */
50  final class PpidChecker
51  {
52      private static final String WMIC_CREATION_DATE = "CreationDate";
53      private static final String WINDOWS_SYSTEM_ROOT_ENV = "SystemRoot";
54      private static final String RELATIVE_PATH_TO_WMIC = "System32\\Wbem";
55      private static final String SYSTEM_PATH_TO_WMIC =
56              "%" + WINDOWS_SYSTEM_ROOT_ENV + "%\\" + RELATIVE_PATH_TO_WMIC + "\\";
57  
58      private final Queue<Process> destroyableCommands = new ConcurrentLinkedQueue<Process>();
59  
60      /**
61       * The etime is in the form of [[dd-]hh:]mm:ss on Unix like systems.
62       * See the workaround https://issues.apache.org/jira/browse/SUREFIRE-1451.
63       */
64      static final Pattern UNIX_CMD_OUT_PATTERN = compile( "^(((\\d+)-)?(\\d{1,2}):)?(\\d{1,2}):(\\d{1,2})$" );
65  
66      private final long pluginPid;
67  
68      private volatile ProcessInfo pluginProcessInfo;
69      private volatile boolean stopped;
70  
71      PpidChecker( long pluginPid )
72      {
73          this.pluginPid = pluginPid;
74          //todo WARN logger (after new logger is finished) that (IS_OS_UNIX && canExecuteUnixPs()) is false
75      }
76  
77      boolean canUse()
78      {
79          return pluginProcessInfo == null
80                         ? IS_OS_WINDOWS || IS_OS_UNIX && canExecuteUnixPs()
81                         : pluginProcessInfo.isValid() && !pluginProcessInfo.isError();
82      }
83  
84      /**
85       * This method can be called only after {@link #canUse()} has returned {@code true}.
86       *
87       * @return {@code true} if parent process is alive; {@code false} otherwise
88       * @throws IllegalStateException if {@link #canUse()} returns {@code false}
89       *                               or the object has been {@link #destroyActiveCommands() destroyed}
90       */
91      @SuppressWarnings( "unchecked" )
92      boolean isProcessAlive()
93      {
94          if ( !canUse() )
95          {
96              throw new IllegalStateException( "irrelevant to call isProcessAlive()" );
97          }
98  
99          if ( IS_OS_WINDOWS )
100         {
101             ProcessInfo previousPluginProcessInfo = pluginProcessInfo;
102             pluginProcessInfo = windows();
103             if ( isStopped() || pluginProcessInfo.isError() )
104             {
105                 throw new IllegalStateException( "error to read process" );
106             }
107             // let's compare creation time, should be same unless killed or PID is reused by OS into another process
108             return pluginProcessInfo.isValid()
109                            && ( previousPluginProcessInfo == null
110                                         || pluginProcessInfo.isTimeEqualTo( previousPluginProcessInfo ) );
111         }
112         else if ( IS_OS_UNIX )
113         {
114             ProcessInfo previousPluginProcessInfo = pluginProcessInfo;
115             pluginProcessInfo = unix();
116             if ( isStopped() || pluginProcessInfo.isError() )
117             {
118                 throw new IllegalStateException( "error to read process" );
119             }
120             // let's compare elapsed time, should be greater or equal if parent process is the same and still alive
121             return pluginProcessInfo.isValid()
122                            && ( previousPluginProcessInfo == null
123                                         || pluginProcessInfo.isTimeEqualTo( previousPluginProcessInfo )
124                                         || pluginProcessInfo.isTimeAfter( previousPluginProcessInfo ) );
125         }
126 
127         throw new IllegalStateException();
128     }
129 
130     // https://www.freebsd.org/cgi/man.cgi?ps(1)
131     // etimes elapsed running time, in decimal integer seconds
132 
133     // http://manpages.ubuntu.com/manpages/xenial/man1/ps.1.html
134     // etimes elapsed time since the process was started, in seconds.
135 
136     // http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/test/java/lang/ProcessBuilder/Basic.java#L167
137     ProcessInfo unix()
138     {
139         ProcessInfoConsumer reader = new ProcessInfoConsumer( Charset.defaultCharset().name() )
140         {
141             @Override
142             ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo )
143             {
144                 if ( !previousProcessInfo.isValid() )
145                 {
146                     Matcher matcher = UNIX_CMD_OUT_PATTERN.matcher( line );
147                     if ( matcher.matches() )
148                     {
149                         long pidUptime = fromDays( matcher )
150                                                  + fromHours( matcher )
151                                                  + fromMinutes( matcher )
152                                                  + fromSeconds( matcher );
153                         return ProcessInfo.unixProcessInfo( pluginPid, pidUptime );
154                     }
155                 }
156                 return previousProcessInfo;
157             }
158         };
159 
160         return reader.execute( "/bin/sh", "-c", unixPathToPS() + " -o etime= -p " + pluginPid );
161     }
162 
163     ProcessInfo windows()
164     {
165         ProcessInfoConsumer reader = new ProcessInfoConsumer( "US-ASCII" )
166         {
167             private boolean hasHeader;
168 
169             @Override
170             ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo )
171             {
172                 if ( !previousProcessInfo.isValid() )
173                 {
174                     StringTokenizer args = new StringTokenizer( line );
175                     if ( args.countTokens() == 1 )
176                     {
177                         if ( hasHeader )
178                         {
179                             String startTimestamp = args.nextToken();
180                             return ProcessInfo.windowsProcessInfo( pluginPid, startTimestamp );
181                         }
182                         else
183                         {
184                             hasHeader = WMIC_CREATION_DATE.equals( args.nextToken() );
185                         }
186                     }
187                 }
188                 return previousProcessInfo;
189             }
190         };
191         String pid = String.valueOf( pluginPid );
192         String wmicPath = hasWmicStandardSystemPath() ? SYSTEM_PATH_TO_WMIC : "";
193         return reader.execute( "CMD", "/A", "/X", "/C",
194                 wmicPath + "wmic process where (ProcessId=" + pid + ") get " + WMIC_CREATION_DATE
195         );
196     }
197 
198     void destroyActiveCommands()
199     {
200         stopped = true;
201         for ( Process p = destroyableCommands.poll(); p != null; p = destroyableCommands.poll() )
202         {
203             p.destroy();
204         }
205     }
206 
207     private boolean isStopped()
208     {
209         return stopped;
210     }
211 
212     static String unixPathToPS()
213     {
214         return canExecuteLocalUnixPs() ? "/usr/bin/ps" : "/bin/ps";
215     }
216 
217     static boolean canExecuteUnixPs()
218     {
219         return canExecuteLocalUnixPs() || canExecuteStandardUnixPs();
220     }
221 
222     private static boolean canExecuteLocalUnixPs()
223     {
224         return new File( "/usr/bin/ps" ).canExecute();
225     }
226 
227     private static boolean canExecuteStandardUnixPs()
228     {
229         return new File( "/bin/ps" ).canExecute();
230     }
231 
232     private static boolean hasWmicStandardSystemPath()
233     {
234         String systemRoot = System.getenv( WINDOWS_SYSTEM_ROOT_ENV );
235         return isNotBlank( systemRoot ) && new File( systemRoot, RELATIVE_PATH_TO_WMIC + "\\wmic.exe" ).isFile();
236     }
237 
238     static long fromDays( Matcher matcher )
239     {
240         String s = matcher.group( 3 );
241         return s == null ? 0L : DAYS.toSeconds( parseLong( s ) );
242     }
243 
244     static long fromHours( Matcher matcher )
245     {
246         String s = matcher.group( 4 );
247         return s == null ? 0L : HOURS.toSeconds( parseLong( s ) );
248     }
249 
250     static long fromMinutes( Matcher matcher )
251     {
252         String s = matcher.group( 5 );
253         return s == null ? 0L : MINUTES.toSeconds( parseLong( s ) );
254     }
255 
256     static long fromSeconds( Matcher matcher )
257     {
258         String s = matcher.group( 6 );
259         return s == null ? 0L : parseLong( s );
260     }
261 
262     private static void checkValid( Scanner scanner )
263             throws IOException
264     {
265         IOException exception = scanner.ioException();
266         if ( exception != null )
267         {
268             throw exception;
269         }
270     }
271 
272     /**
273      * Reads standard output from {@link Process}.
274      * <br>
275      * The artifact maven-shared-utils has non-daemon Threads which is an issue in Surefire to satisfy System.exit.
276      * This implementation is taylor made without using any Thread.
277      * It's easy to destroy Process from other Thread.
278      */
279     private abstract class ProcessInfoConsumer
280     {
281         private final String charset;
282 
283         ProcessInfoConsumer( String charset )
284         {
285             this.charset = charset;
286         }
287 
288         abstract ProcessInfo consumeLine( String line, ProcessInfo previousProcessInfo );
289 
290         ProcessInfo execute( String... command )
291         {
292             ProcessBuilder processBuilder = new ProcessBuilder( command );
293             processBuilder.redirectErrorStream( true );
294             Process process = null;
295             ProcessInfo processInfo = INVALID_PROCESS_INFO;
296             try
297             {
298                 process = processBuilder.start();
299                 destroyableCommands.add( process );
300                 Scanner scanner = new Scanner( process.getInputStream(), charset );
301                 while ( scanner.hasNextLine() )
302                 {
303                     String line = scanner.nextLine().trim();
304                     processInfo = consumeLine( line, processInfo );
305                 }
306                 checkValid( scanner );
307                 int exitCode = process.waitFor();
308                 return exitCode == 0 ? processInfo : INVALID_PROCESS_INFO;
309             }
310             catch ( IOException e )
311             {
312                 DumpErrorSingleton.getSingleton().dumpException( e );
313                 return ERR_PROCESS_INFO;
314             }
315             catch ( InterruptedException e )
316             {
317                 DumpErrorSingleton.getSingleton().dumpException( e );
318                 return ERR_PROCESS_INFO;
319             }
320             finally
321             {
322                 if ( process != null )
323                 {
324                     destroyableCommands.remove( process );
325                     process.destroy();
326                     closeQuietly( process.getInputStream() );
327                     closeQuietly( process.getErrorStream() );
328                     closeQuietly( process.getOutputStream() );
329                 }
330             }
331         }
332     }
333 
334 }