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 }