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 org.apache.maven.surefire.util.ReflectionUtils;
23  
24  import java.io.BufferedReader;
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.FileReader;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.lang.management.ManagementFactory;
31  import java.lang.reflect.Method;
32  import java.math.BigDecimal;
33  import java.util.Properties;
34  import java.util.StringTokenizer;
35  
36  import static java.lang.Character.isDigit;
37  import static java.lang.Thread.currentThread;
38  import static org.apache.commons.io.IOUtils.closeQuietly;
39  import static org.apache.commons.lang3.StringUtils.isNumeric;
40  import static org.apache.commons.lang3.SystemUtils.IS_OS_FREE_BSD;
41  import static org.apache.commons.lang3.SystemUtils.IS_OS_LINUX;
42  import static org.apache.commons.lang3.SystemUtils.IS_OS_NET_BSD;
43  import static org.apache.commons.lang3.SystemUtils.IS_OS_OPEN_BSD;
44  import static org.apache.maven.surefire.util.ReflectionUtils.invokeMethodChain;
45  import static org.apache.maven.surefire.util.ReflectionUtils.tryLoadClass;
46  import static org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
47  
48  /**
49   * JDK 9 support.
50   *
51   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
52   * @since 2.20.1
53   */
54  public final class SystemUtils
55  {
56      public static final BigDecimal JAVA_SPECIFICATION_VERSION = getJavaSpecificationVersion();
57  
58      private static final BigDecimal JAVA_VERSION_7 = new BigDecimal( "1.7" ).stripTrailingZeros();
59  
60      private static final BigDecimal JIGSAW_JAVA_VERSION = new BigDecimal( 9 ).stripTrailingZeros();
61  
62      private static final int PROC_STATUS_PID_FIRST_CHARS = 20;
63  
64      private SystemUtils()
65      {
66          throw new IllegalStateException( "no instantiable constructor" );
67      }
68  
69      /**
70       * @param jvmExecPath    e.g. /jdk/bin/java, /jdk/jre/bin/java
71       * @return {@code true} if {@code jvmExecPath} is path to java binary executor
72       */
73      public static boolean endsWithJavaPath( String jvmExecPath )
74      {
75          File javaExec = new File( jvmExecPath ).getAbsoluteFile();
76          File bin = javaExec.getParentFile();
77          String exec = javaExec.getName();
78          return exec.startsWith( "java" ) && bin != null && bin.getName().equals( "bin" );
79      }
80  
81      /**
82       * If {@code jvmExecutable} is <tt>/jdk/bin/java</tt> (since jdk9) or <tt>/jdk/jre/bin/java</tt> (prior to jdk9)
83       * then the absolute path to JDK home is returned <tt>/jdk</tt>.
84       * <br>
85       * Null is returned if {@code jvmExecutable} is incorrect.
86       *
87       * @param jvmExecutable    /jdk/bin/java* or /jdk/jre/bin/java*
88       * @return path to jdk directory; or <tt>null</tt> if wrong path or directory layout of JDK installation.
89       */
90      public static File toJdkHomeFromJvmExec( String jvmExecutable )
91      {
92          File bin = new File( jvmExecutable ).getAbsoluteFile().getParentFile();
93          if ( "bin".equals( bin.getName() ) )
94          {
95              File parent = bin.getParentFile();
96              if ( "jre".equals( parent.getName() ) )
97              {
98                  File jdk = parent.getParentFile();
99                  return new File( jdk, "bin" ).isDirectory() ? jdk : null;
100             }
101             return parent;
102         }
103         return null;
104     }
105 
106     /**
107      * If system property <tt>java.home</tt> is <tt>/jdk</tt> (since jdk9) or <tt>/jdk/jre</tt> (prior to jdk9) then
108      * the absolute path to
109      * JDK home is returned <tt>/jdk</tt>.
110      *
111      * @return path to JDK
112      */
113     public static File toJdkHomeFromJre()
114     {
115         return toJdkHomeFromJre( System.getProperty( "java.home" ) );
116     }
117 
118     /**
119      * If {@code jreHome} is <tt>/jdk</tt> (since jdk9) or <tt>/jdk/jre</tt> (prior to jdk9) then
120      * the absolute path to JDK home is returned <tt>/jdk</tt>.
121      * <br>
122      * JRE home directory {@code jreHome} must be taken from system property <tt>java.home</tt>.
123      *
124      * @param jreHome    path to /jdk or /jdk/jre
125      * @return path to JDK
126      */
127     static File toJdkHomeFromJre( String jreHome )
128     {
129         File pathToJreOrJdk = new File( jreHome ).getAbsoluteFile();
130         return "jre".equals( pathToJreOrJdk.getName() ) ? pathToJreOrJdk.getParentFile() : pathToJreOrJdk;
131     }
132 
133     public static BigDecimal toJdkVersionFromReleaseFile( File jdkHome )
134     {
135         File release = new File( requireNonNull( jdkHome ).getAbsoluteFile(), "release" );
136         if ( !release.isFile() )
137         {
138             return null;
139         }
140         InputStream is = null;
141         try
142         {
143             Properties properties = new Properties();
144             is = new FileInputStream( release );
145             properties.load( is );
146             String javaVersion = properties.getProperty( "JAVA_VERSION" ).replace( "\"", "" );
147             StringTokenizer versions = new StringTokenizer( javaVersion, "._" );
148 
149             if ( versions.countTokens() == 1 )
150             {
151                 javaVersion = versions.nextToken();
152             }
153             else if ( versions.countTokens() >= 2 )
154             {
155                 String majorVersion = versions.nextToken();
156                 String minorVersion = versions.nextToken();
157                 javaVersion = isNumeric( minorVersion ) ? majorVersion + "." + minorVersion : majorVersion;
158             }
159             else
160             {
161                 return null;
162             }
163 
164             return new BigDecimal( javaVersion );
165         }
166         catch ( IOException e )
167         {
168             return null;
169         }
170         finally
171         {
172             closeQuietly( is );
173         }
174     }
175 
176     /**
177      * Safely extracts major and minor version as fractional number from
178      * <pre>
179      *     $MAJOR.$MINOR.$SECURITY
180      * </pre>.
181      * <br>
182      *     The security version is usually not needed to know.
183      *     It can be applied to not certified JRE.
184      *
185      * @return major.minor version derived from java specification version of <em>this</em> JVM, e.g. 1.8, 9, etc.
186      */
187     private static BigDecimal getJavaSpecificationVersion()
188     {
189         StringBuilder fractionalVersion = new StringBuilder( "0" );
190         for ( char c : org.apache.commons.lang3.SystemUtils.JAVA_SPECIFICATION_VERSION.toCharArray() )
191         {
192             if ( isDigit( c ) )
193             {
194                 fractionalVersion.append( c );
195             }
196             else if ( c == '.' )
197             {
198                 if ( fractionalVersion.indexOf( "." ) == -1 )
199                 {
200                     fractionalVersion.append( '.' );
201                 }
202                 else
203                 {
204                     break;
205                 }
206             }
207         }
208         String majorMinorVersion = fractionalVersion.toString();
209         return new BigDecimal( majorMinorVersion.endsWith( "." ) ? majorMinorVersion + "0" : majorMinorVersion )
210                 .stripTrailingZeros();
211     }
212 
213     public static boolean isJava9AtLeast( String jvmExecutablePath )
214     {
215         File externalJavaHome = toJdkHomeFromJvmExec( jvmExecutablePath );
216         File thisJavaHome = toJdkHomeFromJre();
217         if ( thisJavaHome.equals( externalJavaHome ) )
218         {
219             return isBuiltInJava9AtLeast();
220         }
221         else
222         {
223             BigDecimal releaseFileVersion =
224                     externalJavaHome == null ? null : toJdkVersionFromReleaseFile( externalJavaHome );
225             return isJava9AtLeast( releaseFileVersion );
226         }
227     }
228 
229     public static boolean isBuiltInJava9AtLeast()
230     {
231         return JAVA_SPECIFICATION_VERSION.compareTo( JIGSAW_JAVA_VERSION ) >= 0;
232     }
233 
234     public static boolean isBuiltInJava7AtLeast()
235     {
236         return JAVA_SPECIFICATION_VERSION.compareTo( JAVA_VERSION_7 ) >= 0;
237     }
238 
239     public static boolean isJava9AtLeast( BigDecimal version )
240     {
241         return version != null && version.compareTo( JIGSAW_JAVA_VERSION ) >= 0;
242     }
243 
244     public static ClassLoader platformClassLoader()
245     {
246         if ( isBuiltInJava9AtLeast() )
247         {
248             return reflectClassLoader( ClassLoader.class, "getPlatformClassLoader" );
249         }
250         return null;
251     }
252 
253     public static Long pid()
254     {
255         if ( isBuiltInJava9AtLeast() )
256         {
257             Long pid = pidOnJava9();
258             if ( pid != null )
259             {
260                 return pid;
261             }
262         }
263 
264         if ( IS_OS_LINUX )
265         {
266             try
267             {
268                 return pidStatusOnLinux();
269             }
270             catch ( Exception e )
271             {
272                 // examine PID via JMX
273             }
274         }
275         else if ( IS_OS_FREE_BSD || IS_OS_NET_BSD || IS_OS_OPEN_BSD )
276         {
277             try
278             {
279                 return pidStatusOnBSD();
280             }
281             catch ( Exception e )
282             {
283                 // examine PID via JMX
284             }
285         }
286 
287         return pidOnJMX();
288     }
289 
290     static Long pidOnJMX()
291     {
292         String processName = ManagementFactory.getRuntimeMXBean().getName();
293         if ( processName.contains( "@" ) )
294         {
295             String pid = processName.substring( 0, processName.indexOf( '@' ) ).trim();
296             try
297             {
298                 return Long.parseLong( pid );
299             }
300             catch ( NumberFormatException e )
301             {
302                 return null;
303             }
304         }
305 
306         return null;
307     }
308 
309     /**
310      * $ cat /proc/self/stat
311      * <br>
312      * 48982 (cat) R 9744 48982 9744 34818 48982 8192 185 0 0 0 0 0 0 0 20 0 1 0
313      * 137436614 103354368 134 18446744073709551615 4194304 4235780 140737488346592
314      * 140737488343784 252896458544 0 0 0 0 0 0 0 17 2 0 0 0 0 0
315      * <br>
316      * $ SELF_PID=$(cat /proc/self/stat)
317      * <br>
318      * $ echo $CPU_ID | gawk '{print $1}'
319      * <br>
320      * 48982
321      *
322      * @return self PID
323      * @throws Exception i/o and number format exc
324      */
325     static Long pidStatusOnLinux() throws Exception
326     {
327         return pidStatusOnLinux( "" );
328     }
329 
330     /**
331      * For testing purposes only.
332      *
333      * @param root    shifted to test-classes
334      * @return same as in {@link #pidStatusOnLinux()}
335      * @throws Exception same as in {@link #pidStatusOnLinux()}
336      */
337     static Long pidStatusOnLinux( String root ) throws Exception
338     {
339         FileReader input = new FileReader( root + "/proc/self/stat" );
340         try
341         {
342             // Reading and encoding 20 characters is bit faster than whole line.
343             // size of (long) = 19, + 1 space
344             char[] buffer = new char[PROC_STATUS_PID_FIRST_CHARS];
345             String startLine = new String( buffer, 0, input.read( buffer ) );
346             return Long.parseLong( startLine.substring( 0, startLine.indexOf( ' ' ) ) );
347         }
348         finally
349         {
350             input.close();
351         }
352     }
353 
354     /**
355      * The process status.  This file is read-only and returns a single
356      * line containing multiple space-separated fields.
357      * <br>
358      * See <a href="https://www.freebsd.org/cgi/man.cgi?query=procfs&sektion=5">procfs status</a>
359      * <br>
360      * # cat /proc/curproc/status
361      * <br>
362      * cat 60424 60386 60424 60386 5,0 ctty 972854153,236415 0,0 0,1043 nochan 0 0 0,0 prisoner
363      * <br>
364      * Fields are:
365      * <br>
366      * comm pid ppid pgid sid maj, min ctty, sldr start user/system time wmsg euid ruid rgid,egid,
367      * groups[1 .. NGROUPS] hostname
368      *
369      * @return current PID
370      * @throws Exception if could not read /proc/curproc/status
371      */
372     static Long pidStatusOnBSD() throws Exception
373     {
374         return pidStatusOnBSD( "" );
375     }
376 
377     /**
378      * For testing purposes only.
379      *
380      * @param root    shifted to test-classes
381      * @return same as in {@link #pidStatusOnBSD()}
382      * @throws Exception same as in {@link #pidStatusOnBSD()}
383      */
384     static Long pidStatusOnBSD( String root ) throws Exception
385     {
386         BufferedReader input = new BufferedReader( new FileReader( root + "/proc/curproc/status" ) );
387         try
388         {
389             String line = input.readLine();
390             int i1 = 1 + line.indexOf( ' ' );
391             int i2 = line.indexOf( ' ', i1 );
392             return Long.parseLong( line.substring( i1, i2 ) );
393         }
394         finally
395         {
396             input.close();
397         }
398     }
399 
400     static Long pidOnJava9()
401     {
402         ClassLoader classLoader = currentThread().getContextClassLoader();
403         Class<?> processHandle = tryLoadClass( classLoader, "java.lang.ProcessHandle" );
404         Class<?>[] classesChain = { processHandle, processHandle };
405         String[] methodChain = { "current", "pid" };
406         return (Long) invokeMethodChain( classesChain, methodChain, null );
407     }
408 
409     static ClassLoader reflectClassLoader( Class<?> target, String getterMethodName )
410     {
411         try
412         {
413             Method getter = ReflectionUtils.getMethod( target, getterMethodName );
414             return (ClassLoader) ReflectionUtils.invokeMethodWithArray( null, getter );
415         }
416         catch ( RuntimeException e )
417         {
418             return null;
419         }
420     }
421 }