1 package org.apache.maven.surefire.booter;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
46
47
48
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
62
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
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
86
87
88
89
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
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
121 return pluginProcessInfo.isValid()
122 && ( previousPluginProcessInfo == null
123 || pluginProcessInfo.isTimeEqualTo( previousPluginProcessInfo )
124 || pluginProcessInfo.isTimeAfter( previousPluginProcessInfo ) );
125 }
126
127 throw new IllegalStateException();
128 }
129
130
131
132
133
134
135
136
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
274
275
276
277
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 }