View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.booter;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileReader;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.lang.management.ManagementFactory;
28  import java.lang.reflect.Method;
29  import java.math.BigDecimal;
30  import java.util.Properties;
31  import java.util.StringTokenizer;
32  
33  import org.apache.maven.surefire.api.util.ReflectionUtils;
34  
35  import static java.lang.Thread.currentThread;
36  import static java.util.Objects.requireNonNull;
37  import static org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodChain;
38  import static org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray;
39  import static org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass;
40  import static org.apache.maven.surefire.shared.lang3.JavaVersion.JAVA_9;
41  import static org.apache.maven.surefire.shared.lang3.JavaVersion.JAVA_RECENT;
42  import static org.apache.maven.surefire.shared.lang3.StringUtils.isNumeric;
43  import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_FREE_BSD;
44  import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_LINUX;
45  import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_NET_BSD;
46  import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_OPEN_BSD;
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      private static final BigDecimal JIGSAW_JAVA_VERSION = new BigDecimal(9).stripTrailingZeros();
56  
57      private static final int PROC_STATUS_PID_FIRST_CHARS = 20;
58  
59      private SystemUtils() {
60          throw new IllegalStateException("no instantiable constructor");
61      }
62  
63      /**
64       * @param jvmExecPath    e.g. /jdk/bin/java, /jdk/jre/bin/java
65       * @return {@code true} if {@code jvmExecPath} is path to java binary executor
66       */
67      public static boolean endsWithJavaPath(String jvmExecPath) {
68          File javaExec = new File(jvmExecPath).getAbsoluteFile();
69          File bin = javaExec.getParentFile();
70          String exec = javaExec.getName();
71          return exec.startsWith("java") && bin != null && bin.getName().equals("bin");
72      }
73  
74      /**
75       * If {@code jvmExecutable} is <code>/jdk/bin/java</code> (since jdk9) or <code>/jdk/jre/bin/java</code>
76       * (prior to jdk9) then the absolute path to JDK home is returned <code>/jdk</code>.
77       * <br>
78       * Null is returned if {@code jvmExecutable} is incorrect.
79       *
80       * @param jvmExecutable    /jdk/bin/java* or /jdk/jre/bin/java*
81       * @return path to jdk directory; or <code>null</code> if wrong path or directory layout of JDK installation.
82       */
83      public static File toJdkHomeFromJvmExec(String jvmExecutable) {
84          File bin = new File(jvmExecutable).getAbsoluteFile().getParentFile();
85          if ("bin".equals(bin.getName())) {
86              File parent = bin.getParentFile();
87              if ("jre".equals(parent.getName())) {
88                  File jdk = parent.getParentFile();
89                  return new File(jdk, "bin").isDirectory() ? jdk : null;
90              }
91              return parent;
92          }
93          return null;
94      }
95  
96      /**
97       * If system property <code>java.home</code> is <code>/jdk</code> (since jdk9) or <code>/jdk/jre</code>
98       * (prior to jdk9) then the absolute path to
99       * JDK home is returned <code>/jdk</code>.
100      *
101      * @return path to JDK
102      */
103     public static File toJdkHomeFromJre() {
104         return toJdkHomeFromJre(System.getProperty("java.home"));
105     }
106 
107     /**
108      * If {@code jreHome} is <code>/jdk</code> (since jdk9) or <code>/jdk/jre</code> (prior to jdk9) then
109      * the absolute path to JDK home is returned <code>/jdk</code>.
110      * <br>
111      * JRE home directory {@code jreHome} must be taken from system property <code>java.home</code>.
112      *
113      * @param jreHome    path to /jdk or /jdk/jre
114      * @return path to JDK
115      */
116     static File toJdkHomeFromJre(String jreHome) {
117         File pathToJreOrJdk = new File(jreHome).getAbsoluteFile();
118         return "jre".equals(pathToJreOrJdk.getName()) ? pathToJreOrJdk.getParentFile() : pathToJreOrJdk;
119     }
120 
121     public static BigDecimal toJdkVersionFromReleaseFile(File jdkHome) {
122         File release = new File(requireNonNull(jdkHome).getAbsoluteFile(), "release");
123         if (!release.isFile()) {
124             return null;
125         }
126         Properties properties = new Properties();
127         try (InputStream is = new FileInputStream(release)) {
128             properties.load(is);
129             String javaVersion = properties.getProperty("JAVA_VERSION").replace("\"", "");
130             StringTokenizer versions = new StringTokenizer(javaVersion, "._");
131 
132             if (versions.countTokens() == 1) {
133                 javaVersion = versions.nextToken();
134             } else if (versions.countTokens() >= 2) {
135                 String majorVersion = versions.nextToken();
136                 String minorVersion = versions.nextToken();
137                 javaVersion = isNumeric(minorVersion) ? majorVersion + "." + minorVersion : majorVersion;
138             } else {
139                 return null;
140             }
141 
142             return new BigDecimal(javaVersion);
143         } catch (IOException e) {
144             return null;
145         }
146     }
147 
148     public static boolean isJava9AtLeast(String jvmExecutablePath) {
149         File externalJavaHome = toJdkHomeFromJvmExec(jvmExecutablePath);
150         File thisJavaHome = toJdkHomeFromJre();
151         if (thisJavaHome.equals(externalJavaHome)) {
152             return isBuiltInJava9AtLeast();
153         } else {
154             BigDecimal releaseFileVersion =
155                     externalJavaHome == null ? null : toJdkVersionFromReleaseFile(externalJavaHome);
156             return isJava9AtLeast(releaseFileVersion);
157         }
158     }
159 
160     public static boolean isBuiltInJava9AtLeast() {
161         return JAVA_RECENT.atLeast(JAVA_9);
162     }
163 
164     public static boolean isJava9AtLeast(BigDecimal version) {
165         return version != null && version.compareTo(JIGSAW_JAVA_VERSION) >= 0;
166     }
167 
168     public static ClassLoader platformClassLoader() {
169         if (isBuiltInJava9AtLeast()) {
170             return reflectClassLoader(ClassLoader.class, "getPlatformClassLoader");
171         }
172         return null;
173     }
174 
175     public static Long pid() {
176         if (isBuiltInJava9AtLeast()) {
177             Long pid = pidOnJava9();
178             if (pid != null) {
179                 return pid;
180             }
181         }
182 
183         if (IS_OS_LINUX) {
184             try {
185                 return pidStatusOnLinux();
186             } catch (Exception e) {
187                 // examine PID via JMX
188             }
189         } else if (IS_OS_FREE_BSD || IS_OS_NET_BSD || IS_OS_OPEN_BSD) {
190             try {
191                 return pidStatusOnBSD();
192             } catch (Exception e) {
193                 // examine PID via JMX
194             }
195         }
196 
197         return pidOnJMX();
198     }
199 
200     static Long pidOnJMX() {
201         String processName = ManagementFactory.getRuntimeMXBean().getName();
202         if (processName.contains("@")) {
203             String pid = processName.substring(0, processName.indexOf('@')).trim();
204             try {
205                 return Long.parseLong(pid);
206             } catch (NumberFormatException e) {
207                 return null;
208             }
209         }
210 
211         return null;
212     }
213 
214     /**
215      * $ cat /proc/self/stat
216      * <br>
217      * 48982 (cat) R 9744 48982 9744 34818 48982 8192 185 0 0 0 0 0 0 0 20 0 1 0
218      * 137436614 103354368 134 18446744073709551615 4194304 4235780 140737488346592
219      * 140737488343784 252896458544 0 0 0 0 0 0 0 17 2 0 0 0 0 0
220      * <br>
221      * $ SELF_PID=$(cat /proc/self/stat)
222      * <br>
223      * $ echo $CPU_ID | gawk '{print $1}'
224      * <br>
225      * 48982
226      *
227      * @return self PID
228      * @throws Exception i/o and number format exc
229      */
230     static Long pidStatusOnLinux() throws Exception {
231         return pidStatusOnLinux("");
232     }
233 
234     /**
235      * For testing purposes only.
236      *
237      * @param root    shifted to test-classes
238      * @return same as in {@link #pidStatusOnLinux()}
239      * @throws Exception same as in {@link #pidStatusOnLinux()}
240      */
241     static Long pidStatusOnLinux(String root) throws Exception {
242         try (FileReader input = new FileReader(root + "/proc/self/stat")) {
243             // Reading and encoding 20 characters is bit faster than whole line.
244             // size of (long) = 19, + 1 space
245             char[] buffer = new char[PROC_STATUS_PID_FIRST_CHARS];
246             String startLine = new String(buffer, 0, input.read(buffer));
247             return Long.parseLong(startLine.substring(0, startLine.indexOf(' ')));
248         }
249     }
250 
251     /**
252      * The process status.  This file is read-only and returns a single
253      * line containing multiple space-separated fields.
254      * <br>
255      * See <a href="https://www.freebsd.org/cgi/man.cgi?query=procfs&sektion=5">procfs status</a>
256      * <br>
257      * # cat /proc/curproc/status
258      * <br>
259      * cat 60424 60386 60424 60386 5,0 ctty 972854153,236415 0,0 0,1043 nochan 0 0 0,0 prisoner
260      * <br>
261      * Fields are:
262      * <br>
263      * comm pid ppid pgid sid maj, min ctty, sldr start user/system time wmsg euid ruid rgid,egid,
264      * groups[1 .. NGROUPS] hostname
265      *
266      * @return current PID
267      * @throws Exception if could not read /proc/curproc/status
268      */
269     static Long pidStatusOnBSD() throws Exception {
270         return pidStatusOnBSD("");
271     }
272 
273     /**
274      * For testing purposes only.
275      *
276      * @param root    shifted to test-classes
277      * @return same as in {@link #pidStatusOnBSD()}
278      * @throws Exception same as in {@link #pidStatusOnBSD()}
279      */
280     static Long pidStatusOnBSD(String root) throws Exception {
281         try (BufferedReader input = new BufferedReader(new FileReader(root + "/proc/curproc/status"))) {
282             String line = input.readLine();
283             int i1 = 1 + line.indexOf(' ');
284             int i2 = line.indexOf(' ', i1);
285             return Long.parseLong(line.substring(i1, i2));
286         }
287     }
288 
289     static Long pidOnJava9() {
290         ClassLoader classLoader = currentThread().getContextClassLoader();
291         Class<?> processHandle = tryLoadClass(classLoader, "java.lang.ProcessHandle");
292         Class<?>[] classesChain = {processHandle, processHandle};
293         String[] methodChain = {"current", "pid"};
294         return invokeMethodChain(classesChain, methodChain, null);
295     }
296 
297     static ClassLoader reflectClassLoader(Class<?> target, String getterMethodName) {
298         try {
299             Method getter = ReflectionUtils.getMethod(target, getterMethodName);
300             return invokeMethodWithArray(null, getter);
301         } catch (RuntimeException e) {
302             return null;
303         }
304     }
305 }