1 /* 2 * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 25 package org.graalvm.compiler.test; 26 27 import java.io.BufferedReader; 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStreamReader; 31 import java.nio.file.Files; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Formatter; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.function.Predicate; 38 import java.util.regex.Matcher; 39 import java.util.regex.Pattern; 40 41 import org.graalvm.compiler.serviceprovider.JavaVersionUtil; 42 import org.graalvm.util.CollectionsUtil; 43 import org.junit.Assume; 44 45 /** 46 * Utility methods for spawning a VM in a subprocess during unit tests. 47 */ 48 public final class SubprocessUtil { 49 50 private SubprocessUtil() { 51 } 52 53 /** 54 * Gets the command line for the current process. 55 * 56 * @return the command line arguments for the current process or {@code null} if they are not 57 * available 58 */ 59 public static List<String> getProcessCommandLine() { 60 String processArgsFile = System.getenv().get("MX_SUBPROCESS_COMMAND_FILE"); 61 if (processArgsFile != null) { 62 try { 63 return Files.readAllLines(new File(processArgsFile).toPath()); 64 } catch (IOException e) { 65 } 66 } else { 67 Assume.assumeTrue("Process command line unavailable", false); 68 } 69 return null; 70 } 71 72 /** 73 * Pattern for a single shell command argument that does not need to quoted. 74 */ 75 private static final Pattern SAFE_SHELL_ARG = Pattern.compile("[A-Za-z0-9@%_\\-\\+=:,\\./]+"); 76 77 /** 78 * Reliably quote a string as a single shell command argument. 79 */ 80 public static String quoteShellArg(String arg) { 81 if (arg.isEmpty()) { 82 return "\"\""; 83 } 84 Matcher m = SAFE_SHELL_ARG.matcher(arg); 85 if (m.matches()) { 86 return arg; 87 } 88 // See http://stackoverflow.com/a/1250279 89 return "'" + arg.replace("'", "'\"'\"'") + "'"; 90 } 91 92 /** 93 * Returns a new copy {@code args} with debugger arguments removed. 94 */ 95 public static List<String> withoutDebuggerArguments(List<String> args) { 96 List<String> result = new ArrayList<>(args.size()); 97 for (String arg : args) { 98 if (!(arg.equals("-Xdebug") || arg.startsWith("-Xrunjdwp:"))) { 99 result.add(arg); 100 } 101 } 102 return result; 103 } 104 105 /** 106 * Gets the command line options to do the same package opening and exporting specified by the 107 * {@code --open-packages} option to the {@code mx unittest} command. 108 * 109 * Properties defined in {@code com.oracle.mxtool.junit.MxJUnitWrapper}. 110 */ 111 public static List<String> getPackageOpeningOptions() { 112 List<String> result = new ArrayList<>(); 113 String[] actions = {"opens", "exports"}; 114 for (String action : actions) { 115 String opens = System.getProperty("com.oracle.mxtool.junit." + action); 116 if (opens != null && !opens.isEmpty()) { 117 for (String value : opens.split(System.lineSeparator())) { 118 result.add("--add-" + action + "=" + value); 119 } 120 } 121 } 122 return result; 123 } 124 125 /** 126 * Gets the command line used to start the current Java VM, including all VM arguments, but not 127 * including the main class or any Java arguments. This can be used to spawn an identical VM, 128 * but running different Java code. 129 */ 130 public static List<String> getVMCommandLine() { 131 List<String> args = getProcessCommandLine(); 132 if (args == null) { 133 return null; 134 } else { 135 int index = findMainClassIndex(args); 136 return args.subList(0, index); 137 } 138 } 139 140 /** 141 * Detects whether a Java agent matching {@code agentPredicate} is specified in the VM 142 * arguments. 143 * 144 * @param agentPredicate a predicate that is given the value of a {@code -javaagent} VM argument 145 */ 146 public static boolean isJavaAgentAttached(Predicate<String> agentPredicate) { 147 return SubprocessUtil.getVMCommandLine().stream().// 148 filter(args -> args.startsWith("-javaagent:")).// 149 map(s -> s.substring("-javaagent:".length())).// 150 anyMatch(agentPredicate); 151 } 152 153 /** 154 * Detects whether a Java agent is specified in the VM arguments. 155 */ 156 public static boolean isJavaAgentAttached() { 157 return isJavaAgentAttached(javaAgentValue -> true); 158 } 159 160 /** 161 * Detects whether the JaCoCo Java agent is specified in the VM arguments. 162 */ 163 public static boolean isJaCoCoAttached() { 164 return isJavaAgentAttached(s -> s.toLowerCase().contains("jacoco")); 165 } 166 167 /** 168 * The details of a subprocess execution. 169 */ 170 public static class Subprocess { 171 172 /** 173 * The command line of the subprocess. 174 */ 175 public final List<String> command; 176 177 /** 178 * Exit code of the subprocess. 179 */ 180 public final int exitCode; 181 182 /** 183 * Output from the subprocess broken into lines. 184 */ 185 public final List<String> output; 186 187 public Subprocess(List<String> command, int exitCode, List<String> output) { 188 this.command = command; 189 this.exitCode = exitCode; 190 this.output = output; 191 } 192 193 public static final String DASHES_DELIMITER = "-------------------------------------------------------"; 194 195 /** 196 * Returns the command followed by the output as a string. 197 * 198 * @param delimiter if non-null, the returned string has this value as a prefix and suffix 199 */ 200 public String toString(String delimiter) { 201 Formatter msg = new Formatter(); 202 if (delimiter != null) { 203 msg.format("%s%n", delimiter); 204 } 205 msg.format("%s%n", CollectionsUtil.mapAndJoin(command, e -> quoteShellArg(String.valueOf(e)), " ")); 206 for (String line : output) { 207 msg.format("%s%n", line); 208 } 209 if (delimiter != null) { 210 msg.format("%s%n", delimiter); 211 } 212 return msg.toString(); 213 } 214 215 /** 216 * Returns the command followed by the output as a string delimited by 217 * {@value #DASHES_DELIMITER}. 218 */ 219 @Override 220 public String toString() { 221 return toString(DASHES_DELIMITER); 222 } 223 } 224 225 /** 226 * Executes a Java subprocess. 227 * 228 * @param vmArgs the VM arguments 229 * @param mainClassAndArgs the main class and its arguments 230 */ 231 public static Subprocess java(List<String> vmArgs, String... mainClassAndArgs) throws IOException, InterruptedException { 232 return java(vmArgs, Arrays.asList(mainClassAndArgs)); 233 } 234 235 /** 236 * Executes a Java subprocess. 237 * 238 * @param vmArgs the VM arguments 239 * @param mainClassAndArgs the main class and its arguments 240 */ 241 public static Subprocess java(List<String> vmArgs, List<String> mainClassAndArgs) throws IOException, InterruptedException { 242 return javaHelper(vmArgs, null, mainClassAndArgs); 243 } 244 245 /** 246 * Executes a Java subprocess. 247 * 248 * @param vmArgs the VM arguments 249 * @param env the environment variables 250 * @param mainClassAndArgs the main class and its arguments 251 */ 252 public static Subprocess java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs) throws IOException, InterruptedException { 253 return java(vmArgs, env, Arrays.asList(mainClassAndArgs)); 254 } 255 256 /** 257 * Executes a Java subprocess. 258 * 259 * @param vmArgs the VM arguments 260 * @param env the environment variables 261 * @param mainClassAndArgs the main class and its arguments 262 */ 263 public static Subprocess java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException { 264 return javaHelper(vmArgs, env, mainClassAndArgs); 265 } 266 267 /** 268 * Executes a Java subprocess. 269 * 270 * @param vmArgs the VM arguments 271 * @param env the environment variables 272 * @param mainClassAndArgs the main class and its arguments 273 */ 274 private static Subprocess javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException { 275 List<String> command = new ArrayList<>(vmArgs); 276 command.addAll(mainClassAndArgs); 277 ProcessBuilder processBuilder = new ProcessBuilder(command); 278 if (env != null) { 279 Map<String, String> processBuilderEnv = processBuilder.environment(); 280 processBuilderEnv.putAll(env); 281 } 282 processBuilder.redirectErrorStream(true); 283 Process process = processBuilder.start(); 284 BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); 285 String line; 286 List<String> output = new ArrayList<>(); 287 while ((line = stdout.readLine()) != null) { 288 output.add(line); 289 } 290 return new Subprocess(command, process.waitFor(), output); 291 } 292 293 private static final boolean isJava8OrEarlier = JavaVersionUtil.JAVA_SPEC <= 8; 294 295 private static boolean hasArg(String optionName) { 296 if (optionName.equals("-cp") || optionName.equals("-classpath")) { 297 return true; 298 } 299 if (!isJava8OrEarlier) { 300 if (optionName.equals("--version") || 301 optionName.equals("--show-version") || 302 optionName.equals("--dry-run") || 303 optionName.equals("--disable-@files") || 304 optionName.equals("--dry-run") || 305 optionName.equals("--help") || 306 optionName.equals("--help-extra")) { 307 return false; 308 } 309 if (optionName.startsWith("--")) { 310 return optionName.indexOf('=') == -1; 311 } 312 } 313 return false; 314 } 315 316 private static int findMainClassIndex(List<String> commandLine) { 317 int i = 1; // Skip the java executable 318 319 while (i < commandLine.size()) { 320 String s = commandLine.get(i); 321 if (s.charAt(0) != '-') { 322 // https://bugs.openjdk.java.net/browse/JDK-8027634 323 if (isJava8OrEarlier || s.charAt(0) != '@') { 324 return i; 325 } 326 i++; 327 } else if (hasArg(s)) { 328 i += 2; 329 } else { 330 i++; 331 } 332 } 333 throw new InternalError(); 334 } 335 336 }