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 }