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.nio.file.Path; 33 import java.nio.file.Paths; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Formatter; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.function.Predicate; 40 import java.util.regex.Matcher; 41 import java.util.regex.Pattern; 42 43 import org.graalvm.compiler.serviceprovider.JavaVersionUtil; 44 import org.graalvm.util.CollectionsUtil; 45 import org.junit.Assume; 46 47 /** 48 * Utility methods for spawning a VM in a subprocess during unit tests. 49 */ 50 public final class SubprocessUtil { 51 52 /** 53 * The name of the boolean system property that can be set to preserve temporary files created 54 * as arguments files passed to the java launcher. 55 * 56 * @see "https://docs.oracle.com/javase/9/tools/java.htm#JSWOR-GUID-4856361B-8BFD-4964-AE84-121F5F6CF111" 57 */ 58 public static final String KEEP_TEMPORARY_ARGUMENT_FILES_PROPERTY_NAME = "test." + SubprocessUtil.class.getSimpleName() + ".keepTempArgumentFiles"; 59 SubprocessUtil()60 private SubprocessUtil() { 61 } 62 63 /** 64 * Gets the command line for the current process. 65 * 66 * @return the command line arguments for the current process or {@code null} if they are not 67 * available 68 */ getProcessCommandLine()69 public static List<String> getProcessCommandLine() { 70 String processArgsFile = System.getenv().get("MX_SUBPROCESS_COMMAND_FILE"); 71 if (processArgsFile != null) { 72 try { 73 return Files.readAllLines(new File(processArgsFile).toPath()); 74 } catch (IOException e) { 75 } 76 } else { 77 Assume.assumeTrue("Process command line unavailable", false); 78 } 79 return null; 80 } 81 82 /** 83 * Pattern for a single shell command argument that does not need to quoted. 84 */ 85 private static final Pattern SAFE_SHELL_ARG = Pattern.compile("[A-Za-z0-9@%_\\-\\+=:,\\./]+"); 86 87 /** 88 * Reliably quote a string as a single shell command argument. 89 */ quoteShellArg(String arg)90 public static String quoteShellArg(String arg) { 91 if (arg.isEmpty()) { 92 return "\"\""; 93 } 94 Matcher m = SAFE_SHELL_ARG.matcher(arg); 95 if (m.matches()) { 96 return arg; 97 } 98 // See http://stackoverflow.com/a/1250279 99 return "'" + arg.replace("'", "'\"'\"'") + "'"; 100 } 101 102 /** 103 * Returns a new copy {@code args} with debugger arguments removed. 104 */ withoutDebuggerArguments(List<String> args)105 public static List<String> withoutDebuggerArguments(List<String> args) { 106 List<String> result = new ArrayList<>(args.size()); 107 for (String arg : args) { 108 if (!(arg.equals("-Xdebug") || arg.startsWith("-Xrunjdwp:"))) { 109 result.add(arg); 110 } 111 } 112 return result; 113 } 114 115 /** 116 * Gets the command line options to do the same package opening and exporting specified by the 117 * {@code --open-packages} option to the {@code mx unittest} command. 118 * 119 * Properties defined in {@code com.oracle.mxtool.junit.MxJUnitWrapper}. 120 */ getPackageOpeningOptions()121 public static List<String> getPackageOpeningOptions() { 122 List<String> result = new ArrayList<>(); 123 String[] actions = {"opens", "exports"}; 124 for (String action : actions) { 125 String opens = System.getProperty("com.oracle.mxtool.junit." + action); 126 if (opens != null && !opens.isEmpty()) { 127 for (String value : opens.split(System.lineSeparator())) { 128 result.add("--add-" + action + "=" + value); 129 } 130 } 131 } 132 return result; 133 } 134 135 /** 136 * Gets the command line used to start the current Java VM, including all VM arguments, but not 137 * including the main class or any Java arguments. This can be used to spawn an identical VM, 138 * but running different Java code. 139 */ getVMCommandLine()140 public static List<String> getVMCommandLine() { 141 List<String> args = getProcessCommandLine(); 142 if (args == null) { 143 return null; 144 } else { 145 int index = findMainClassIndex(args); 146 return args.subList(0, index); 147 } 148 } 149 150 /** 151 * Detects whether a Java agent matching {@code agentPredicate} is specified in the VM 152 * arguments. 153 * 154 * @param agentPredicate a predicate that is given the value of a {@code -javaagent} VM argument 155 */ isJavaAgentAttached(Predicate<String> agentPredicate)156 public static boolean isJavaAgentAttached(Predicate<String> agentPredicate) { 157 return SubprocessUtil.getVMCommandLine().stream().// 158 filter(args -> args.startsWith("-javaagent:")).// 159 map(s -> s.substring("-javaagent:".length())).// 160 anyMatch(agentPredicate); 161 } 162 163 /** 164 * Detects whether a Java agent is specified in the VM arguments. 165 */ isJavaAgentAttached()166 public static boolean isJavaAgentAttached() { 167 return isJavaAgentAttached(javaAgentValue -> true); 168 } 169 170 /** 171 * Detects whether the JaCoCo Java agent is specified in the VM arguments. 172 */ isJaCoCoAttached()173 public static boolean isJaCoCoAttached() { 174 return isJavaAgentAttached(s -> s.toLowerCase().contains("jacoco")); 175 } 176 177 /** 178 * The details of a subprocess execution. 179 */ 180 public static class Subprocess { 181 182 /** 183 * The command line of the subprocess. 184 */ 185 public final List<String> command; 186 187 /** 188 * Exit code of the subprocess. 189 */ 190 public final int exitCode; 191 192 /** 193 * Output from the subprocess broken into lines. 194 */ 195 public final List<String> output; 196 197 /** 198 * Explicit environment variables. 199 */ 200 private Map<String, String> env; 201 Subprocess(List<String> command, Map<String, String> env, int exitCode, List<String> output)202 public Subprocess(List<String> command, Map<String, String> env, int exitCode, List<String> output) { 203 this.command = command; 204 this.env = env; 205 this.exitCode = exitCode; 206 this.output = output; 207 } 208 209 public static final String DASHES_DELIMITER = "-------------------------------------------------------"; 210 211 /** 212 * Returns the command followed by the output as a string. 213 * 214 * @param delimiter if non-null, the returned string has this value as a prefix and suffix 215 */ toString(String delimiter)216 public String toString(String delimiter) { 217 Formatter msg = new Formatter(); 218 if (delimiter != null) { 219 msg.format("%s%n", delimiter); 220 } 221 if (env != null && !env.isEmpty()) { 222 msg.format("env"); 223 for (Map.Entry<String, String> e : env.entrySet()) { 224 msg.format(" %s=%s", e.getKey(), quoteShellArg(e.getValue())); 225 } 226 msg.format("\\%n"); 227 } 228 msg.format("%s%n", CollectionsUtil.mapAndJoin(command, e -> quoteShellArg(String.valueOf(e)), " ")); 229 for (String line : output) { 230 msg.format("%s%n", line); 231 } 232 if (delimiter != null) { 233 msg.format("%s%n", delimiter); 234 } 235 return msg.toString(); 236 } 237 238 /** 239 * Returns the command followed by the output as a string delimited by 240 * {@value #DASHES_DELIMITER}. 241 */ 242 @Override toString()243 public String toString() { 244 return toString(DASHES_DELIMITER); 245 } 246 } 247 248 /** 249 * A sentinel value which when present in the {@code vmArgs} parameter for any of the 250 * {@code java(...)} methods in this class is replaced with a temporary argument file containing 251 * the contents of {@link #getPackageOpeningOptions}. The argument file is preserved if the 252 * {@link #KEEP_TEMPORARY_ARGUMENT_FILES_PROPERTY_NAME} system property is true. 253 */ 254 public static final String PACKAGE_OPENING_OPTIONS = ";:PACKAGE_OPENING_OPTIONS_IN_TEMPORARY_ARGUMENTS_FILE:;"; 255 256 /** 257 * Executes a Java subprocess. 258 * 259 * @param vmArgs the VM arguments 260 * @param mainClassAndArgs the main class and its arguments 261 */ java(List<String> vmArgs, String... mainClassAndArgs)262 public static Subprocess java(List<String> vmArgs, String... mainClassAndArgs) throws IOException, InterruptedException { 263 return java(vmArgs, Arrays.asList(mainClassAndArgs)); 264 } 265 266 /** 267 * Executes a Java subprocess. 268 * 269 * @param vmArgs the VM arguments 270 * @param mainClassAndArgs the main class and its arguments 271 */ java(List<String> vmArgs, List<String> mainClassAndArgs)272 public static Subprocess java(List<String> vmArgs, List<String> mainClassAndArgs) throws IOException, InterruptedException { 273 return javaHelper(vmArgs, null, mainClassAndArgs); 274 } 275 276 /** 277 * Executes a Java subprocess. 278 * 279 * @param vmArgs the VM arguments 280 * @param env the environment variables 281 * @param mainClassAndArgs the main class and its arguments 282 */ java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs)283 public static Subprocess java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs) throws IOException, InterruptedException { 284 return java(vmArgs, env, Arrays.asList(mainClassAndArgs)); 285 } 286 287 /** 288 * Executes a Java subprocess. 289 * 290 * @param vmArgs the VM arguments 291 * @param env the environment variables 292 * @param mainClassAndArgs the main class and its arguments 293 */ java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs)294 public static Subprocess java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException { 295 return javaHelper(vmArgs, env, mainClassAndArgs); 296 } 297 298 /** 299 * Executes a Java subprocess. 300 * 301 * @param vmArgs the VM arguments 302 * @param env the environment variables 303 * @param mainClassAndArgs the main class and its arguments 304 */ javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs)305 private static Subprocess javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException { 306 List<String> command = new ArrayList<>(vmArgs.size()); 307 Path packageOpeningOptionsArgumentsFile = null; 308 for (String vmArg : vmArgs) { 309 if (vmArg == PACKAGE_OPENING_OPTIONS) { 310 if (packageOpeningOptionsArgumentsFile == null) { 311 List<String> packageOpeningOptions = getPackageOpeningOptions(); 312 if (!packageOpeningOptions.isEmpty()) { 313 packageOpeningOptionsArgumentsFile = Files.createTempFile(Paths.get("."), "package-opening-options-arguments-file", ".txt").toAbsolutePath(); 314 Files.write(packageOpeningOptionsArgumentsFile, packageOpeningOptions); 315 command.add("@" + packageOpeningOptionsArgumentsFile); 316 } 317 } 318 } else { 319 command.add(vmArg); 320 } 321 } 322 command.addAll(mainClassAndArgs); 323 ProcessBuilder processBuilder = new ProcessBuilder(command); 324 if (env != null) { 325 Map<String, String> processBuilderEnv = processBuilder.environment(); 326 processBuilderEnv.putAll(env); 327 } 328 processBuilder.redirectErrorStream(true); 329 try { 330 Process process = processBuilder.start(); 331 BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); 332 String line; 333 List<String> output = new ArrayList<>(); 334 while ((line = stdout.readLine()) != null) { 335 output.add(line); 336 } 337 return new Subprocess(command, env, process.waitFor(), output); 338 } finally { 339 if (packageOpeningOptionsArgumentsFile != null) { 340 if (!Boolean.getBoolean(KEEP_TEMPORARY_ARGUMENT_FILES_PROPERTY_NAME)) { 341 Files.delete(packageOpeningOptionsArgumentsFile); 342 } 343 } 344 } 345 } 346 347 private static final boolean isJava8OrEarlier = JavaVersionUtil.JAVA_SPEC <= 8; 348 hasArg(String optionName)349 private static boolean hasArg(String optionName) { 350 if (optionName.equals("-cp") || optionName.equals("-classpath")) { 351 return true; 352 } 353 if (!isJava8OrEarlier) { 354 if (optionName.equals("--version") || 355 optionName.equals("--show-version") || 356 optionName.equals("--dry-run") || 357 optionName.equals("--disable-@files") || 358 optionName.equals("--dry-run") || 359 optionName.equals("--help") || 360 optionName.equals("--help-extra")) { 361 return false; 362 } 363 if (optionName.startsWith("--")) { 364 return optionName.indexOf('=') == -1; 365 } 366 } 367 return false; 368 } 369 findMainClassIndex(List<String> commandLine)370 private static int findMainClassIndex(List<String> commandLine) { 371 int i = 1; // Skip the java executable 372 373 while (i < commandLine.size()) { 374 String s = commandLine.get(i); 375 if (s.charAt(0) != '-') { 376 // https://bugs.openjdk.java.net/browse/JDK-8027634 377 if (isJava8OrEarlier || s.charAt(0) != '@') { 378 return i; 379 } 380 i++; 381 } else if (hasArg(s)) { 382 i += 2; 383 } else { 384 i++; 385 } 386 } 387 throw new InternalError(); 388 } 389 390 } 391