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.util.Properties;
33  import java.util.StringTokenizer;
34  
35  import static java.lang.Thread.currentThread;
36  import static org.apache.commons.io.IOUtils.closeQuietly;
37  import static org.apache.commons.lang3.JavaVersion.JAVA_9;
38  import static org.apache.commons.lang3.JavaVersion.JAVA_RECENT;
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.commons.lang3.SystemUtils.isJavaVersionAtLeast;
45  import static org.apache.maven.surefire.util.ReflectionUtils.invokeMethodChain;
46  import static org.apache.maven.surefire.util.ReflectionUtils.tryLoadClass;
47  import static org.apache.maven.surefire.util.internal.ObjectUtils.requireNonNull;
48  
49  /**
50   * JDK 9 support.
51   *
52   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
53   * @since 2.20.1
54   */
55  public final class SystemUtils
56  {
57      private static final double JIGSAW_JAVA_VERSION = 9.0d;
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 Double toJdkVersionFromReleaseFile( File jdkHome )
131     {
132         File release = new File( requireNonNull( jdkHome ).getAbsoluteFile(), "release" );
133         if ( !release.isFile() )
134         {
135             return null;
136         }
137         InputStream is = null;
138         try
139         {
140             Properties properties = new Properties();
141             is = new FileInputStream( release );
142             properties.load( is );
143             String javaVersion = properties.getProperty( "JAVA_VERSION" ).replace( "\"", "" );
144             StringTokenizer versions = new StringTokenizer( javaVersion, "._" );
145 
146             if ( versions.countTokens() == 1 )
147             {
148                 javaVersion = versions.nextToken();
149             }
150             else if ( versions.countTokens() >= 2 )
151             {
152                 String majorVersion = versions.nextToken();
153                 String minorVersion = versions.nextToken();
154                 javaVersion = isNumeric( minorVersion ) ? majorVersion + "." + minorVersion : majorVersion;
155             }
156             else
157             {
158                 return null;
159             }
160 
161             return Double.valueOf( javaVersion );
162         }
163         catch ( IOException e )
164         {
165             return null;
166         }
167         finally
168         {
169             closeQuietly( is );
170         }
171     }
172 
173     public static boolean isJava9AtLeast( String jvmExecutablePath )
174     {
175         File externalJavaHome = toJdkHomeFromJvmExec( jvmExecutablePath );
176         File thisJavaHome = toJdkHomeFromJre();
177         if ( thisJavaHome.equals( externalJavaHome ) )
178         {
179             return isBuiltInJava9AtLeast();
180         }
181         Double releaseFileVersion = externalJavaHome == null ? null : toJdkVersionFromReleaseFile( externalJavaHome );
182         return SystemUtils.isJava9AtLeast( releaseFileVersion );
183     }
184 
185     static boolean isBuiltInJava9AtLeast()
186     {
187         return isJavaVersionAtLeast( JAVA_9 );
188     }
189 
190     public static boolean isJava9AtLeast( Double version )
191     {
192         return version != null && version >= JIGSAW_JAVA_VERSION;
193     }
194 
195     public static ClassLoader platformClassLoader()
196     {
197         if ( JAVA_RECENT.atLeast( JAVA_9 ) )
198         {
199             return reflectClassLoader( ClassLoader.class, "getPlatformClassLoader" );
200         }
201         return null;
202     }
203 
204     public static Long pid()
205     {
206         if ( JAVA_RECENT.atLeast( JAVA_9 ) )
207         {
208             Long pid = pidOnJava9();
209             if ( pid != null )
210             {
211                 return pid;
212             }
213         }
214 
215         if ( IS_OS_LINUX )
216         {
217             try
218             {
219                 return pidStatusOnLinux();
220             }
221             catch ( Exception e )
222             {
223                 // examine PID via JMX
224             }
225         }
226         else if ( IS_OS_FREE_BSD || IS_OS_NET_BSD || IS_OS_OPEN_BSD )
227         {
228             try
229             {
230                 return pidStatusOnBSD();
231             }
232             catch ( Exception e )
233             {
234                 // examine PID via JMX
235             }
236         }
237 
238         return pidOnJMX();
239     }
240 
241     static Long pidOnJMX()
242     {
243         String processName = ManagementFactory.getRuntimeMXBean().getName();
244         if ( processName.contains( "@" ) )
245         {
246             String pid = processName.substring( 0, processName.indexOf( '@' ) ).trim();
247             try
248             {
249                 return Long.parseLong( pid );
250             }
251             catch ( NumberFormatException e )
252             {
253                 return null;
254             }
255         }
256 
257         return null;
258     }
259 
260     static Long pidStatusOnLinux() throws Exception
261     {
262         FileReader input = new FileReader( "/proc/self/stat" );
263         try
264         {
265             // Reading and encoding 20 characters is bit faster than whole line.
266             // size of (long) = 19, + 1 space
267             char[] buffer = new char[PROC_STATUS_PID_FIRST_CHARS];
268             String startLine = new String( buffer, 0, input.read( buffer ) );
269             return Long.parseLong( startLine.substring( 0, startLine.indexOf( ' ' ) ) );
270         }
271         finally
272         {
273             input.close();
274         }
275     }
276 
277     /**
278      * The process status.  This file is read-only and returns a single
279      * line containing multiple space-separated fields.
280      * See <a href="https://www.freebsd.org/cgi/man.cgi?query=procfs&sektion=5">procfs status</a>
281      *
282      * @return current PID
283      * @throws Exception if could not read /proc/curproc/status
284      */
285     static Long pidStatusOnBSD() throws Exception
286     {
287         BufferedReader input = new BufferedReader( new FileReader( "/proc/curproc/status" ) );
288         try
289         {
290             String line = input.readLine();
291             int i1 = 1 + line.indexOf( ' ' );
292             int i2 = line.indexOf( ' ', i1 );
293             return Long.parseLong( line.substring( i1, i2 ) );
294         }
295         finally
296         {
297             input.close();
298         }
299     }
300 
301     static Long pidOnJava9()
302     {
303         ClassLoader classLoader = currentThread().getContextClassLoader();
304         Class<?> processHandle = tryLoadClass( classLoader, "java.lang.ProcessHandle" );
305         Class<?>[] classesChain = { processHandle, processHandle };
306         String[] methodChain = { "current", "pid" };
307         return (Long) invokeMethodChain( classesChain, methodChain, null );
308     }
309 
310     static ClassLoader reflectClassLoader( Class<?> target, String getterMethodName )
311     {
312         try
313         {
314             Method getter = ReflectionUtils.getMethod( target, getterMethodName );
315             return (ClassLoader) ReflectionUtils.invokeMethodWithArray( null, getter );
316         }
317         catch ( RuntimeException e )
318         {
319             return null;
320         }
321     }
322 }