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