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.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Formatter;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.function.Predicate;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 
43 import org.graalvm.compiler.serviceprovider.JavaVersionUtil;
44 import org.graalvm.util.CollectionsUtil;
45 import org.junit.Assume;
46 
47 /**
48  * Utility methods for spawning a VM in a subprocess during unit tests.
49  */
50 public final class SubprocessUtil {
51 
52     /**
53      * The name of the boolean system property that can be set to preserve temporary files created
54      * as arguments files passed to the java launcher.
55      *
56      * @see "https://docs.oracle.com/javase/9/tools/java.htm#JSWOR-GUID-4856361B-8BFD-4964-AE84-121F5F6CF111"
57      */
58     public static final String KEEP_TEMPORARY_ARGUMENT_FILES_PROPERTY_NAME = "test." + SubprocessUtil.class.getSimpleName() + ".keepTempArgumentFiles";
59 
SubprocessUtil()60     private SubprocessUtil() {
61     }
62 
63     /**
64      * Gets the command line for the current process.
65      *
66      * @return the command line arguments for the current process or {@code null} if they are not
67      *         available
68      */
getProcessCommandLine()69     public static List<String> getProcessCommandLine() {
70         String processArgsFile = System.getenv().get("MX_SUBPROCESS_COMMAND_FILE");
71         if (processArgsFile != null) {
72             try {
73                 return Files.readAllLines(new File(processArgsFile).toPath());
74             } catch (IOException e) {
75             }
76         } else {
77             Assume.assumeTrue("Process command line unavailable", false);
78         }
79         return null;
80     }
81 
82     /**
83      * Pattern for a single shell command argument that does not need to quoted.
84      */
85     private static final Pattern SAFE_SHELL_ARG = Pattern.compile("[A-Za-z0-9@%_\\-\\+=:,\\./]+");
86 
87     /**
88      * Reliably quote a string as a single shell command argument.
89      */
quoteShellArg(String arg)90     public static String quoteShellArg(String arg) {
91         if (arg.isEmpty()) {
92             return "\"\"";
93         }
94         Matcher m = SAFE_SHELL_ARG.matcher(arg);
95         if (m.matches()) {
96             return arg;
97         }
98         // See http://stackoverflow.com/a/1250279
99         return "'" + arg.replace("'", "'\"'\"'") + "'";
100     }
101 
102     /**
103      * Returns a new copy {@code args} with debugger arguments removed.
104      */
withoutDebuggerArguments(List<String> args)105     public static List<String> withoutDebuggerArguments(List<String> args) {
106         List<String> result = new ArrayList<>(args.size());
107         for (String arg : args) {
108             if (!(arg.equals("-Xdebug") || arg.startsWith("-Xrunjdwp:"))) {
109                 result.add(arg);
110             }
111         }
112         return result;
113     }
114 
115     /**
116      * Gets the command line options to do the same package opening and exporting specified by the
117      * {@code --open-packages} option to the {@code mx unittest} command.
118      *
119      * Properties defined in {@code com.oracle.mxtool.junit.MxJUnitWrapper}.
120      */
getPackageOpeningOptions()121     public static List<String> getPackageOpeningOptions() {
122         List<String> result = new ArrayList<>();
123         String[] actions = {"opens", "exports"};
124         for (String action : actions) {
125             String opens = System.getProperty("com.oracle.mxtool.junit." + action);
126             if (opens != null && !opens.isEmpty()) {
127                 for (String value : opens.split(System.lineSeparator())) {
128                     result.add("--add-" + action + "=" + value);
129                 }
130             }
131         }
132         return result;
133     }
134 
135     /**
136      * Gets the command line used to start the current Java VM, including all VM arguments, but not
137      * including the main class or any Java arguments. This can be used to spawn an identical VM,
138      * but running different Java code.
139      */
getVMCommandLine()140     public static List<String> getVMCommandLine() {
141         List<String> args = getProcessCommandLine();
142         if (args == null) {
143             return null;
144         } else {
145             int index = findMainClassIndex(args);
146             return args.subList(0, index);
147         }
148     }
149 
150     /**
151      * Detects whether a Java agent matching {@code agentPredicate} is specified in the VM
152      * arguments.
153      *
154      * @param agentPredicate a predicate that is given the value of a {@code -javaagent} VM argument
155      */
isJavaAgentAttached(Predicate<String> agentPredicate)156     public static boolean isJavaAgentAttached(Predicate<String> agentPredicate) {
157         return SubprocessUtil.getVMCommandLine().stream().//
158                         filter(args -> args.startsWith("-javaagent:")).//
159                         map(s -> s.substring("-javaagent:".length())).//
160                         anyMatch(agentPredicate);
161     }
162 
163     /**
164      * Detects whether a Java agent is specified in the VM arguments.
165      */
isJavaAgentAttached()166     public static boolean isJavaAgentAttached() {
167         return isJavaAgentAttached(javaAgentValue -> true);
168     }
169 
170     /**
171      * Detects whether the JaCoCo Java agent is specified in the VM arguments.
172      */
isJaCoCoAttached()173     public static boolean isJaCoCoAttached() {
174         return isJavaAgentAttached(s -> s.toLowerCase().contains("jacoco"));
175     }
176 
177     /**
178      * The details of a subprocess execution.
179      */
180     public static class Subprocess {
181 
182         /**
183          * The command line of the subprocess.
184          */
185         public final List<String> command;
186 
187         /**
188          * Exit code of the subprocess.
189          */
190         public final int exitCode;
191 
192         /**
193          * Output from the subprocess broken into lines.
194          */
195         public final List<String> output;
196 
197         /**
198          * Explicit environment variables.
199          */
200         private Map<String, String> env;
201 
Subprocess(List<String> command, Map<String, String> env, int exitCode, List<String> output)202         public Subprocess(List<String> command, Map<String, String> env, int exitCode, List<String> output) {
203             this.command = command;
204             this.env = env;
205             this.exitCode = exitCode;
206             this.output = output;
207         }
208 
209         public static final String DASHES_DELIMITER = "-------------------------------------------------------";
210 
211         /**
212          * Returns the command followed by the output as a string.
213          *
214          * @param delimiter if non-null, the returned string has this value as a prefix and suffix
215          */
toString(String delimiter)216         public String toString(String delimiter) {
217             Formatter msg = new Formatter();
218             if (delimiter != null) {
219                 msg.format("%s%n", delimiter);
220             }
221             if (env != null && !env.isEmpty()) {
222                 msg.format("env");
223                 for (Map.Entry<String, String> e : env.entrySet()) {
224                     msg.format(" %s=%s", e.getKey(), quoteShellArg(e.getValue()));
225                 }
226                 msg.format("\\%n");
227             }
228             msg.format("%s%n", CollectionsUtil.mapAndJoin(command, e -> quoteShellArg(String.valueOf(e)), " "));
229             for (String line : output) {
230                 msg.format("%s%n", line);
231             }
232             if (delimiter != null) {
233                 msg.format("%s%n", delimiter);
234             }
235             return msg.toString();
236         }
237 
238         /**
239          * Returns the command followed by the output as a string delimited by
240          * {@value #DASHES_DELIMITER}.
241          */
242         @Override
toString()243         public String toString() {
244             return toString(DASHES_DELIMITER);
245         }
246     }
247 
248     /**
249      * A sentinel value which when present in the {@code vmArgs} parameter for any of the
250      * {@code java(...)} methods in this class is replaced with a temporary argument file containing
251      * the contents of {@link #getPackageOpeningOptions}. The argument file is preserved if the
252      * {@link #KEEP_TEMPORARY_ARGUMENT_FILES_PROPERTY_NAME} system property is true.
253      */
254     public static final String PACKAGE_OPENING_OPTIONS = ";:PACKAGE_OPENING_OPTIONS_IN_TEMPORARY_ARGUMENTS_FILE:;";
255 
256     /**
257      * Executes a Java subprocess.
258      *
259      * @param vmArgs the VM arguments
260      * @param mainClassAndArgs the main class and its arguments
261      */
java(List<String> vmArgs, String... mainClassAndArgs)262     public static Subprocess java(List<String> vmArgs, String... mainClassAndArgs) throws IOException, InterruptedException {
263         return java(vmArgs, Arrays.asList(mainClassAndArgs));
264     }
265 
266     /**
267      * Executes a Java subprocess.
268      *
269      * @param vmArgs the VM arguments
270      * @param mainClassAndArgs the main class and its arguments
271      */
java(List<String> vmArgs, List<String> mainClassAndArgs)272     public static Subprocess java(List<String> vmArgs, List<String> mainClassAndArgs) throws IOException, InterruptedException {
273         return javaHelper(vmArgs, null, mainClassAndArgs);
274     }
275 
276     /**
277      * Executes a Java subprocess.
278      *
279      * @param vmArgs the VM arguments
280      * @param env the environment variables
281      * @param mainClassAndArgs the main class and its arguments
282      */
java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs)283     public static Subprocess java(List<String> vmArgs, Map<String, String> env, String... mainClassAndArgs) throws IOException, InterruptedException {
284         return java(vmArgs, env, Arrays.asList(mainClassAndArgs));
285     }
286 
287     /**
288      * Executes a Java subprocess.
289      *
290      * @param vmArgs the VM arguments
291      * @param env the environment variables
292      * @param mainClassAndArgs the main class and its arguments
293      */
java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs)294     public static Subprocess java(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException {
295         return javaHelper(vmArgs, env, mainClassAndArgs);
296     }
297 
298     /**
299      * Executes a Java subprocess.
300      *
301      * @param vmArgs the VM arguments
302      * @param env the environment variables
303      * @param mainClassAndArgs the main class and its arguments
304      */
javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs)305     private static Subprocess javaHelper(List<String> vmArgs, Map<String, String> env, List<String> mainClassAndArgs) throws IOException, InterruptedException {
306         List<String> command = new ArrayList<>(vmArgs.size());
307         Path packageOpeningOptionsArgumentsFile = null;
308         for (String vmArg : vmArgs) {
309             if (vmArg == PACKAGE_OPENING_OPTIONS) {
310                 if (packageOpeningOptionsArgumentsFile == null) {
311                     List<String> packageOpeningOptions = getPackageOpeningOptions();
312                     if (!packageOpeningOptions.isEmpty()) {
313                         packageOpeningOptionsArgumentsFile = Files.createTempFile(Paths.get("."), "package-opening-options-arguments-file", ".txt").toAbsolutePath();
314                         Files.write(packageOpeningOptionsArgumentsFile, packageOpeningOptions);
315                         command.add("@" + packageOpeningOptionsArgumentsFile);
316                     }
317                 }
318             } else {
319                 command.add(vmArg);
320             }
321         }
322         command.addAll(mainClassAndArgs);
323         ProcessBuilder processBuilder = new ProcessBuilder(command);
324         if (env != null) {
325             Map<String, String> processBuilderEnv = processBuilder.environment();
326             processBuilderEnv.putAll(env);
327         }
328         processBuilder.redirectErrorStream(true);
329         try {
330             Process process = processBuilder.start();
331             BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
332             String line;
333             List<String> output = new ArrayList<>();
334             while ((line = stdout.readLine()) != null) {
335                 output.add(line);
336             }
337             return new Subprocess(command, env, process.waitFor(), output);
338         } finally {
339             if (packageOpeningOptionsArgumentsFile != null) {
340                 if (!Boolean.getBoolean(KEEP_TEMPORARY_ARGUMENT_FILES_PROPERTY_NAME)) {
341                     Files.delete(packageOpeningOptionsArgumentsFile);
342                 }
343             }
344         }
345     }
346 
347     private static final boolean isJava8OrEarlier = JavaVersionUtil.JAVA_SPEC <= 8;
348 
hasArg(String optionName)349     private static boolean hasArg(String optionName) {
350         if (optionName.equals("-cp") || optionName.equals("-classpath")) {
351             return true;
352         }
353         if (!isJava8OrEarlier) {
354             if (optionName.equals("--version") ||
355                             optionName.equals("--show-version") ||
356                             optionName.equals("--dry-run") ||
357                             optionName.equals("--disable-@files") ||
358                             optionName.equals("--dry-run") ||
359                             optionName.equals("--help") ||
360                             optionName.equals("--help-extra")) {
361                 return false;
362             }
363             if (optionName.startsWith("--")) {
364                 return optionName.indexOf('=') == -1;
365             }
366         }
367         return false;
368     }
369 
findMainClassIndex(List<String> commandLine)370     private static int findMainClassIndex(List<String> commandLine) {
371         int i = 1; // Skip the java executable
372 
373         while (i < commandLine.size()) {
374             String s = commandLine.get(i);
375             if (s.charAt(0) != '-') {
376                 // https://bugs.openjdk.java.net/browse/JDK-8027634
377                 if (isJava8OrEarlier || s.charAt(0) != '@') {
378                     return i;
379                 }
380                 i++;
381             } else if (hasArg(s)) {
382                 i += 2;
383             } else {
384                 i++;
385             }
386         }
387         throw new InternalError();
388     }
389 
390 }
391