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