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     // TODO simplify or remove when Java 8 support is dropped
176     public static Long pid() {
177         if (isBuiltInJava9AtLeast()) {
178             Long pid = pidOnJava9();
179             if (pid != null) {
180                 return pid;
181             }
182         }
183 
184         if (IS_OS_LINUX) {
185             try {
186                 return pidStatusOnLinux();
187             } catch (Exception e) {
188                 // examine PID via JMX
189             }
190         } else if (IS_OS_FREE_BSD || IS_OS_NET_BSD || IS_OS_OPEN_BSD) {
191             try {
192                 return pidStatusOnBSD();
193             } catch (Exception e) {
194                 // examine PID via JMX
195             }
196         }
197 
198         return pidOnJMX();
199     }
200 
201     static Long pidOnJMX() {
202         String processName = ManagementFactory.getRuntimeMXBean().getName();
203         if (processName.contains("@")) {
204             String pid = processName.substring(0, processName.indexOf('@')).trim();
205             try {
206                 return Long.parseLong(pid);
207             } catch (NumberFormatException e) {
208                 return null;
209             }
210         }
211 
212         return null;
213     }
214 
215     /**
216      * $ cat /proc/self/stat
217      * <br>
218      * 48982 (cat) R 9744 48982 9744 34818 48982 8192 185 0 0 0 0 0 0 0 20 0 1 0
219      * 137436614 103354368 134 18446744073709551615 4194304 4235780 140737488346592
220      * 140737488343784 252896458544 0 0 0 0 0 0 0 17 2 0 0 0 0 0
221      * <br>
222      * $ SELF_PID=$(cat /proc/self/stat)
223      * <br>
224      * $ echo $CPU_ID | gawk '{print $1}'
225      * <br>
226      * 48982
227      *
228      * @return self PID
229      * @throws Exception i/o and number format exc
230      */
231     static Long pidStatusOnLinux() throws Exception {
232         return pidStatusOnLinux("");
233     }
234 
235     /**
236      * For testing purposes only.
237      *
238      * @param root    shifted to test-classes
239      * @return same as in {@link #pidStatusOnLinux()}
240      * @throws Exception same as in {@link #pidStatusOnLinux()}
241      */
242     static Long pidStatusOnLinux(String root) throws Exception {
243         try (FileReader input = new FileReader(root + "/proc/self/stat")) {
244             // Reading and encoding 20 characters is bit faster than whole line.
245             // size of (long) = 19, + 1 space
246             char[] buffer = new char[PROC_STATUS_PID_FIRST_CHARS];
247             String startLine = new String(buffer, 0, input.read(buffer));
248             return Long.parseLong(startLine.substring(0, startLine.indexOf(' ')));
249         }
250     }
251 
252     /**
253      * The process status.  This file is read-only and returns a single
254      * line containing multiple space-separated fields.
255      * <br>
256      * See <a href="https://www.freebsd.org/cgi/man.cgi?query=procfs&sektion=5">procfs status</a>
257      * <br>
258      * # cat /proc/curproc/status
259      * <br>
260      * cat 60424 60386 60424 60386 5,0 ctty 972854153,236415 0,0 0,1043 nochan 0 0 0,0 prisoner
261      * <br>
262      * Fields are:
263      * <br>
264      * comm pid ppid pgid sid maj, min ctty, sldr start user/system time wmsg euid ruid rgid,egid,
265      * groups[1 .. NGROUPS] hostname
266      *
267      * @return current PID
268      * @throws Exception if could not read /proc/curproc/status
269      */
270     static Long pidStatusOnBSD() throws Exception {
271         return pidStatusOnBSD("");
272     }
273 
274     /**
275      * For testing purposes only.
276      *
277      * @param root    shifted to test-classes
278      * @return same as in {@link #pidStatusOnBSD()}
279      * @throws Exception same as in {@link #pidStatusOnBSD()}
280      */
281     static Long pidStatusOnBSD(String root) throws Exception {
282         try (BufferedReader input = new BufferedReader(new FileReader(root + "/proc/curproc/status"))) {
283             String line = input.readLine();
284             int i1 = 1 + line.indexOf(' ');
285             int i2 = line.indexOf(' ', i1);
286             return Long.parseLong(line.substring(i1, i2));
287         }
288     }
289 
290     static Long pidOnJava9() {
291         ClassLoader classLoader = currentThread().getContextClassLoader();
292         Class<?> processHandle = tryLoadClass(classLoader, "java.lang.ProcessHandle");
293         Class<?>[] classesChain = {processHandle, processHandle};
294         String[] methodChain = {"current", "pid"};
295         return invokeMethodChain(classesChain, methodChain, null);
296     }
297 
298     static ClassLoader reflectClassLoader(Class<?> target, String getterMethodName) {
299         try {
300             Method getter = ReflectionUtils.getMethod(target, getterMethodName);
301             return invokeMethodWithArray(null, getter);
302         } catch (RuntimeException e) {
303             return null;
304         }
305     }
306 }