1 /* 2 * Copyright (c) 2015, 2018, 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.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 import org.graalvm.compiler.serviceprovider.GraalServices; 41 import org.graalvm.util.CollectionsUtil; 42 import org.junit.Assume; 43 44 /** 45 * Utility methods for spawning a VM in a subprocess during unit tests. 46 */ 47 public final class SubprocessUtil { 48 49 private SubprocessUtil() { 50 } 51 52 /** 53 * Gets the command line for the current process. 54 * 55 * @return the command line arguments for the current process or {@code null} if they are not 56 * available 57 */ 58 public static List<String> getProcessCommandLine() { 59 String processArgsFile = System.getenv().get("MX_SUBPROCESS_COMMAND_FILE"); 60 if (processArgsFile != null) { 61 try { 62 return Files.readAllLines(new File(processArgsFile).toPath()); 63 } catch (IOException e) { 64 } 65 } else { 66 Assume.assumeTrue("Process command line unavailable", false); 67 } 68 return null; 69 } 70 71 /** 72 * Pattern for a single shell command argument that does not need to quoted. 73 */ 74 private static final Pattern SAFE_SHELL_ARG = Pattern.compile("[A-Za-z0-9@%_\\-\\+=:,\\./]+"); 75 76 /** 77 * Reliably quote a string as a single shell command argument. 78 */ 79 public static String quoteShellArg(String arg) { 80 if (arg.isEmpty()) { 81 return "\"\""; 82 } 83 Matcher m = SAFE_SHELL_ARG.matcher(arg); 84 if (m.matches()) { 85 return arg; 86 } 87 // See http://stackoverflow.com/a/1250279 88 return "'" + arg.replace("'", "'\"'\"'") + "'"; 89 } 90 91 /** 92 * Returns a new copy {@code args} with debugger arguments removed. 93 */ 94 public static List<String> withoutDebuggerArguments(List<String> args) { 95 List<String> result = new ArrayList<>(args.size()); 96 for (String arg : args) { 97 if (!(arg.equals("-Xdebug") || arg.startsWith("-Xrunjdwp:"))) { 98 result.add(arg); 99 } 100 } 101 return result; 102 } 103 104 /** 105 * Gets the command line used to start the current Java VM, including all VM arguments, but not 106 * including the main class or any Java arguments. This can be used to spawn an identical VM, 107 * but running different Java code. 108 */ 109 public static List<String> getVMCommandLine() { 110 List<String> args = getProcessCommandLine(); 111 if (args == null) { 112 return null; 113 } else { 114 int index = findMainClassIndex(args); 115 return args.subList(0, index); 116 } 117 } 118 119 /** 120 * Detects whether a java agent is attached. 121 */ 122 public static boolean isJavaAgentAttached() { 123 return SubprocessUtil.getVMCommandLine().stream().anyMatch(args -> args.startsWith("-javaagent")); 124 } 125 126 /** 127 * The details of a subprocess execution. 128 */ 129 public static class Subprocess { 130 131 /** 132 * The command line of the subprocess. 133 */ 134 public final List<String> command; 135 136 /** 137 * Exit code of the subprocess. 138 */ 139 public final int exitCode; 140 141 /** 142 * Output from the subprocess broken into lines. 143 */ 144 public final List<String> output; 145 146 public Subprocess(List<String> command, int exitCode, List<String> output) { 147 this.command = command; 148 this.exitCode = exitCode; 149 this.output = output; 150 } 151 152 public static final String DASHES_DELIMITER = "-------------------------------------------------------"; 153 154 /** 155 * Returns the command followed by the output as a string. 156 * 157 * @param delimiter if non-null, the returned string has this value as a prefix and suffix 158 */ 159 public String toString(String delimiter) { 160 Formatter msg = new Formatter(); 161 if (delimiter != null) { 162 msg.format("%s%n", delimiter); 163 } 164 msg.format("%s%n", CollectionsUtil.mapAndJoin(command, e -> quoteShellArg(String.valueOf(e)), " ")); 165 for (String line : output) { 166 msg.format("%s%n", line); 167 } 168 if (delimiter != null) { 169 msg.format("%s%n", delimiter); 170 } 171 return msg.toString(); 172 } 173 174 /** 175 * Returns the command followed by the output as a string delimited by 176 * {@value #DASHES_DELIMITER}. 177 */ 178 @Override 179 public String toString() { 180 return toString(DASHES_DELIMITER); 181 } 182 } 183 184 /** 185 * Executes a Java subprocess. 186 * 187 * @param vmArgs the VM arguments 188 * @param mainClassAndArgs the main class and its arguments 189 */ 190 public static Subprocess java(List<String> vmArgs, String... mainClassAndArgs) throws IOException, InterruptedException { 191 return java(vmArgs, Arrays.asList(mainClassAndArgs)); 192 } 193 194 /** 195 * Executes a Java subprocess. 196 * 197 * @param vmArgs the VM arguments 198 * @param mainClassAndArgs the main class and its arguments 199 */ 200 public static Subprocess java(List<String> vmArgs, List<String> mainClassAndArgs) throws IOException, InterruptedException { 201 return javaHelper(vmArgs, null, mainClassAndArgs); 202 } 203 204 /** 205 * Executes a Java subprocess. 206 * 207 * @param vmArgs the VM arguments 208 * @param env the environment variables 209 * @param mainClassAndArgs the main class and its arguments 210 */ 211 public static Subprocess java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs) throws IOException, InterruptedException { 212 return java(vmArgs, env, Arrays.asList(mainClassAndArgs)); 213 } 214 215 /** 216 * Executes a Java subprocess. 217 * 218 * @param vmArgs the VM arguments 219 * @param env the environment variables 220 * @param mainClassAndArgs the main class and its arguments 221 */ 222 public static Subprocess java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException { 223 return javaHelper(vmArgs, env, mainClassAndArgs); 224 } 225 226 /** 227 * Executes a Java subprocess. 228 * 229 * @param vmArgs the VM arguments 230 * @param env the environment variables 231 * @param mainClassAndArgs the main class and its arguments 232 */ 233 private static Subprocess javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException { 234 List<String> command = new ArrayList<>(vmArgs); 235 command.addAll(mainClassAndArgs); 236 ProcessBuilder processBuilder = new ProcessBuilder(command); 237 if (env != null) { 238 Map<String, String> processBuilderEnv = processBuilder.environment(); 239 processBuilderEnv.putAll(env); 240 } 241 processBuilder.redirectErrorStream(true); 242 Process process = processBuilder.start(); 243 BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream())); 244 String line; 245 List<String> output = new ArrayList<>(); 246 while ((line = stdout.readLine()) != null) { 247 output.add(line); 248 } 249 return new Subprocess(command, process.waitFor(), output); 250 } 251 252 private static final boolean isJava8OrEarlier = GraalServices.Java8OrEarlier; 253 254 private static boolean hasArg(String optionName) { 255 if (optionName.equals("-cp") || optionName.equals("-classpath")) { 256 return true; 257 } 258 if (!isJava8OrEarlier) { 259 if (optionName.equals("--version") || 260 optionName.equals("--show-version") || 261 optionName.equals("--dry-run") || 262 optionName.equals("--disable-@files") || 263 optionName.equals("--dry-run") || 264 optionName.equals("--help") || 265 optionName.equals("--help-extra")) { 266 return false; 267 } 268 if (optionName.startsWith("--")) { 269 return optionName.indexOf('=') == -1; 270 } 271 } 272 return false; 273 } 274 275 private static int findMainClassIndex(List<String> commandLine) { 276 int i = 1; // Skip the java executable 277 278 while (i < commandLine.size()) { 279 String s = commandLine.get(i); 280 if (s.charAt(0) != '-') { 281 return i; 282 } else if (hasArg(s)) { 283 i += 2; 284 } else { 285 i++; 286 } 287 } 288 throw new InternalError(); 289 } 290 291 }