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      * The details of a subprocess execution.
 118      */
 119     public static class Subprocess {
 120 
 121         /**
 122          * The command line of the subprocess.
 123          */
 124         public final List<String> command;
 125 
 126         /**
 127          * Exit code of the subprocess.
 128          */
 129         public final int exitCode;
 130 
 131         /**
 132          * Output from the subprocess broken into lines.
 133          */
 134         public final List<String> output;
 135 
 136         public Subprocess(List<String> command, int exitCode, List<String> output) {
 137             this.command = command;
 138             this.exitCode = exitCode;
 139             this.output = output;
 140         }
 141 
 142         public static final String DASHES_DELIMITER = "-------------------------------------------------------";
 143 
 144         /**
 145          * Returns the command followed by the output as a string.
 146          *
 147          * @param delimiter if non-null, the returned string has this value as a prefix and suffix
 148          */
 149         public String toString(String delimiter) {
 150             Formatter msg = new Formatter();
 151             if (delimiter != null) {
 152                 msg.format("%s%n", delimiter);
 153             }
 154             msg.format("%s%n", CollectionsUtil.mapAndJoin(command, e -> quoteShellArg(String.valueOf(e)), " "));
 155             for (String line : output) {
 156                 msg.format("%s%n", line);
 157             }
 158             if (delimiter != null) {
 159                 msg.format("%s%n", delimiter);
 160             }
 161             return msg.toString();
 162         }
 163 
 164         /**
 165          * Returns the command followed by the output as a string delimited by
 166          * {@value #DASHES_DELIMITER}.
 167          */
 168         @Override
 169         public String toString() {
 170             return toString(DASHES_DELIMITER);
 171         }
 172     }
 173 
 174     /**
 175      * Executes a Java subprocess.
 176      *
 177      * @param vmArgs the VM arguments
 178      * @param mainClassAndArgs the main class and its arguments
 179      */
 180     public static Subprocess java(List<String> vmArgs, String... mainClassAndArgs) throws IOException, InterruptedException {
 181         return java(vmArgs, Arrays.asList(mainClassAndArgs));
 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, List<String> mainClassAndArgs) throws IOException, InterruptedException {
 191         return javaHelper(vmArgs, null, mainClassAndArgs);
 192     }
 193 
 194     /**
 195      * Executes a Java subprocess.
 196      *
 197      * @param vmArgs the VM arguments
 198      * @param env the environment variables
 199      * @param mainClassAndArgs the main class and its arguments
 200      */
 201     public static Subprocess java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs) throws IOException, InterruptedException {
 202         return java(vmArgs, env, Arrays.asList(mainClassAndArgs));
 203     }
 204 
 205     /**
 206      * Executes a Java subprocess.
 207      *
 208      * @param vmArgs the VM arguments
 209      * @param env the environment variables
 210      * @param mainClassAndArgs the main class and its arguments
 211      */
 212     public static Subprocess java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException {
 213         return javaHelper(vmArgs, env, mainClassAndArgs);
 214     }
 215 
 216     /**
 217      * Executes a Java subprocess.
 218      *
 219      * @param vmArgs the VM arguments
 220      * @param env the environment variables
 221      * @param mainClassAndArgs the main class and its arguments
 222      */
 223     private static Subprocess javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException {
 224         List<String> command = new ArrayList<>(vmArgs);
 225         command.addAll(mainClassAndArgs);
 226         ProcessBuilder processBuilder = new ProcessBuilder(command);
 227         if (env != null) {
 228             Map<String, String> processBuilderEnv = processBuilder.environment();
 229             processBuilderEnv.putAll(env);
 230         }
 231         processBuilder.redirectErrorStream(true);
 232         Process process = processBuilder.start();
 233         BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
 234         String line;
 235         List<String> output = new ArrayList<>();
 236         while ((line = stdout.readLine()) != null) {
 237             output.add(line);
 238         }
 239         return new Subprocess(command, process.waitFor(), output);
 240     }
 241 
 242     private static final boolean isJava8OrEarlier = GraalServices.Java8OrEarlier;
 243 
 244     private static boolean hasArg(String optionName) {
 245         if (optionName.equals("-cp") || optionName.equals("-classpath")) {
 246             return true;
 247         }
 248         if (!isJava8OrEarlier) {
 249             if (optionName.equals("--version") ||
 250                             optionName.equals("--show-version") ||
 251                             optionName.equals("--dry-run") ||
 252                             optionName.equals("--disable-@files") ||
 253                             optionName.equals("--dry-run") ||
 254                             optionName.equals("--help") ||
 255                             optionName.equals("--help-extra")) {
 256                 return false;
 257             }
 258             if (optionName.startsWith("--")) {
 259                 return optionName.indexOf('=') == -1;
 260             }
 261         }
 262         return false;
 263     }
 264 
 265     private static int findMainClassIndex(List<String> commandLine) {
 266         int i = 1; // Skip the java executable
 267 
 268         while (i < commandLine.size()) {
 269             String s = commandLine.get(i);
 270             if (s.charAt(0) != '-') {
 271                 return i;
 272             } else if (hasArg(s)) {
 273                 i += 2;
 274             } else {
 275                 i++;
 276             }
 277         }
 278         throw new InternalError();
 279     }
 280 
 281 }