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