1 /*
2  * Copyright (c) 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 import java.io.File;
25 import java.io.IOException;
26 import java.io.OutputStream;
27 import java.lang.invoke.MethodHandle;
28 import java.lang.invoke.MethodHandles;
29 import java.lang.module.ModuleDescriptor;
30 import java.lang.reflect.Method;
31 import java.nio.file.FileSystems;
32 import java.nio.file.Files;
33 import java.nio.file.Path;
34 import java.util.ArrayList;
35 import java.util.LinkedList;
36 import java.util.List;
37 import java.util.jar.Attributes;
38 import java.util.jar.JarEntry;
39 import java.util.jar.JarOutputStream;
40 import java.util.jar.Manifest;
41 import java.util.stream.Collectors;
42 import java.util.stream.Stream;
43 
44 import jdk.internal.module.ModuleInfoWriter;
45 import jdk.test.lib.JDKToolFinder;
46 import jdk.test.lib.process.ProcessTools;
47 import jdk.test.lib.util.JarUtils;
48 
49 /*
50  * @test
51  * @bug 8205654
52  * @summary Unit test for sun.tools.ProcessHelper class. The test launches Java processes with different Java options
53  * and checks that sun.tools.ProcessHelper.getMainClass(pid) method returns a correct main class.                                                                                                                               return a .
54  *
55  * @requires (os.family == "linux" | os.family == "freebsd")
56  * @library /test/lib
57  * @modules jdk.jcmd/sun.tools.common:+open
58  *          java.base/jdk.internal.module
59  * @build test.TestProcess
60  * @run main/othervm TestProcessHelper
61  */
62 public class TestProcessHelper {
63 
64     private static final String TEST_PROCESS_MAIN_CLASS_NAME = "TestProcess";
65     private static final String TEST_PROCESS_MAIN_CLASS_PACKAGE = "test";
66     private static final String TEST_PROCESS_MAIN_CLASS = TEST_PROCESS_MAIN_CLASS_PACKAGE + "."
67             + TEST_PROCESS_MAIN_CLASS_NAME;
68     private static final Path TEST_CLASSES = FileSystems.getDefault().getPath(System.getProperty("test.classes"));
69     private static final Path USER_DIR = FileSystems.getDefault().getPath(System.getProperty("user.dir", "."));
70     private static final Path TEST_MODULES = USER_DIR.resolve("testmodules");
71     private static final String JAVA_PATH = JDKToolFinder.getJDKTool("java");
72     private static final Path TEST_CLASS = TEST_CLASSES.resolve(TEST_PROCESS_MAIN_CLASS_PACKAGE)
73             .resolve(TEST_PROCESS_MAIN_CLASS_NAME + ".class");
74 
75     private static final String[] CP_OPTIONS = {"-cp", "-classpath", "--class-path"};
76     private static final String[][] VM_ARGS = {{}, {"-Dtest1=aaa"}, {"-Dtest1=aaa", "-Dtest2=bbb ccc"}};
77     private static final String[][] ARGS = {{}, {"param1"}, {"param1", "param2"}};
78     private static final String[] MP_OPTIONS = {"-p", "--module-path"};
79     private static final String[] MODULE_OPTIONS = {"-m", "--module", "--module="};
80     private static final String JAR_OPTION = "-jar";
81     private static final String MODULE_NAME = "module1";
82     private static final String[][] EXTRA_MODULAR_OPTIONS = {null,
83             {"--add-opens", "java.base/java.net=ALL-UNNAMED"},
84             {"--add-exports", "java.base/java.net=ALL-UNNAMED"},
85             {"--add-reads", "java.base/java.net=ALL-UNNAMED"},
86             {"--add-modules", "java.management"},
87             {"--limit-modules", "java.management"},
88             {"--upgrade-module-path", "test"}};
89 
90     private static final String[] PATCH_MODULE_OPTIONS = {"--patch-module", null};
91 
92     private static final MethodHandle MH_GET_MAIN_CLASS = resolveMainClassMH();
93 
resolveMainClassMH()94     private static MethodHandle resolveMainClassMH() {
95         try {
96             Method getMainClassMethod = Class
97                 .forName("sun.tools.common.ProcessHelper")
98                 .getDeclaredMethod("getMainClass", String.class);
99             getMainClassMethod.setAccessible(true);
100             return MethodHandles.lookup().unreflect(getMainClassMethod);
101         } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException e) {
102             throw new RuntimeException(e);
103         }
104     }
105 
callGetMainClass(Process p)106     private static String callGetMainClass(Process p) {
107         try {
108             return (String)MH_GET_MAIN_CLASS.invoke(Long.toString(p.pid()));
109         } catch (Throwable e) {
110             throw new RuntimeException(e);
111         }
112 
113     }
114 
main(String[] args)115     public static void main(String[] args) throws Exception {
116         new TestProcessHelper().runTests();
117     }
118 
runTests()119     public void runTests() throws Exception {
120         testClassPath();
121         testJar();
122         testModule();
123     }
124 
125     // Test Java processes that are started with -classpath, -cp, or --class-path options
126     // and with different combinations of VM and program args.
testClassPath()127     private void testClassPath() throws Exception {
128         for (String cp : CP_OPTIONS) {
129             for (String[] vma : VM_ARGS) {
130                 for (String[] arg : ARGS) {
131                     for (String[] modularOptions : EXTRA_MODULAR_OPTIONS) {
132                         List<String> cmd = new LinkedList<>();
133                         cmd.add(JAVA_PATH);
134                         cmd.add(cp);
135                         cmd.add(TEST_CLASSES.toAbsolutePath().toString());
136                         for (String v : vma) {
137                             cmd.add(v);
138                         }
139                         if (modularOptions != null) {
140                             cmd.add(modularOptions[0]);
141                             cmd.add(modularOptions[1]);
142                         }
143                         cmd.add(TEST_PROCESS_MAIN_CLASS);
144                         for (String a : arg) {
145                             cmd.add(a);
146                         }
147                         testProcessHelper(cmd, TEST_PROCESS_MAIN_CLASS);
148                     }
149                 }
150             }
151         }
152     }
153 
154     // Test Java processes that are started with -jar option
155     // and with different combinations of VM and program args.
testJar()156     private void testJar() throws Exception {
157         File jarFile = prepareJar();
158         for (String[] vma : VM_ARGS) {
159             for (String[] arg : ARGS) {
160                 List<String> cmd = new LinkedList<>();
161                 cmd.add(JAVA_PATH);
162                 for (String v : vma) {
163                     cmd.add(v);
164                 }
165                 cmd.add(JAR_OPTION);
166                 cmd.add(jarFile.getAbsolutePath());
167                 for (String a : arg) {
168                     cmd.add(a);
169                 }
170                 testProcessHelper(cmd, jarFile.getAbsolutePath());
171             }
172         }
173 
174     }
175 
176     // Test Java processes that are started with -m or --module options
177     // and with different combination of VM and program args.
testModule()178     private void testModule() throws Exception {
179         prepareModule();
180         for (String mp : MP_OPTIONS) {
181             for (String m : MODULE_OPTIONS) {
182                 for (String[] vma : VM_ARGS) {
183                     for (String[] arg : ARGS) {
184                         for(String patchModuleOption : PATCH_MODULE_OPTIONS) {
185                             List<String> cmd = new LinkedList<>();
186                             cmd.add(JAVA_PATH);
187                             cmd.add(mp);
188                             cmd.add(TEST_MODULES.toAbsolutePath().toString());
189                             if (patchModuleOption != null) {
190                                 cmd.add(patchModuleOption);
191                                 cmd.add(MODULE_NAME + "=" + TEST_MODULES.toAbsolutePath().toString());
192                             }
193                             for (String v : vma) {
194                                 cmd.add(v);
195                             }
196                             if (m.endsWith("=")) {
197                                 cmd.add(m + MODULE_NAME + "/" + TEST_PROCESS_MAIN_CLASS);
198                             } else {
199                                 cmd.add(m);
200                                 cmd.add(MODULE_NAME + "/" + TEST_PROCESS_MAIN_CLASS);
201                             }
202                             for (String a : arg) {
203                                 cmd.add(a);
204                             }
205                             testProcessHelper(cmd, MODULE_NAME + "/" + TEST_PROCESS_MAIN_CLASS);
206                         }
207                     }
208                 }
209             }
210         }
211     }
212 
checkMainClass(Process p, String expectedMainClass)213     private void checkMainClass(Process p, String expectedMainClass) {
214         String mainClass = callGetMainClass(p);
215         // getMainClass() may return null, e.g. due to timing issues.
216         // Attempt some limited retries.
217         if (mainClass == null) {
218             System.err.println("Main class returned by ProcessHelper was null.");
219             // sleep time doubles each round, altogether, wait no longer than 1 sec
220             final int MAX_RETRIES = 10;
221             int retrycount = 0;
222             long sleepms = 1;
223             while (retrycount < MAX_RETRIES && mainClass == null) {
224                 System.err.println("Retry " + retrycount + ", sleeping for " + sleepms + "ms.");
225                 try {
226                     Thread.sleep(sleepms);
227                 } catch (InterruptedException e) {
228                     // ignore
229                 }
230                 mainClass = callGetMainClass(p);
231                 retrycount++;
232                 sleepms *= 2;
233             }
234         }
235         p.destroyForcibly();
236         if (!expectedMainClass.equals(mainClass)) {
237             throw new RuntimeException("Main class is wrong: " + mainClass);
238         }
239     }
240 
testProcessHelper(List<String> args, String expectedValue)241     private void testProcessHelper(List<String> args, String expectedValue) throws Exception {
242         ProcessBuilder pb = new ProcessBuilder(args);
243         String cmd = pb.command().stream().collect(Collectors.joining(" "));
244         System.out.println("Starting the process:" + cmd);
245         Process p = ProcessTools.startProcess("test", pb);
246         if (!p.isAlive()) {
247             throw new RuntimeException("Cannot start the process: " + cmd);
248         }
249         checkMainClass(p, expectedValue);
250     }
251 
prepareJar()252     private File prepareJar() throws Exception {
253         Path jarFile = USER_DIR.resolve("testprocess.jar");
254         Manifest manifest = createManifest();
255         JarUtils.createJarFile(jarFile, manifest, TEST_CLASSES, TEST_CLASS);
256         return jarFile.toFile();
257     }
258 
prepareModule()259     private void prepareModule() throws Exception {
260         TEST_MODULES.toFile().mkdirs();
261         Path moduleJar = TEST_MODULES.resolve("mod1.jar");
262         ModuleDescriptor md = createModuleDescriptor();
263         createModuleJarFile(moduleJar, md, TEST_CLASSES, TEST_CLASS);
264     }
265 
createManifest()266     private Manifest createManifest() {
267         Manifest manifest = new Manifest();
268         manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
269         manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, TEST_PROCESS_MAIN_CLASS);
270         return manifest;
271     }
272 
createModuleDescriptor()273     private ModuleDescriptor createModuleDescriptor() {
274         ModuleDescriptor.Builder builder
275                 = ModuleDescriptor.newModule(MODULE_NAME).requires("java.base");
276         return builder.build();
277     }
278 
createModuleJarFile(Path jarfile, ModuleDescriptor md, Path dir, Path... files)279     private static void createModuleJarFile(Path jarfile, ModuleDescriptor md, Path dir, Path... files)
280             throws IOException {
281 
282         Path parent = jarfile.getParent();
283         if (parent != null) {
284             Files.createDirectories(parent);
285         }
286 
287         List<Path> entries = findAllRegularFiles(dir, files);
288 
289         try (OutputStream out = Files.newOutputStream(jarfile);
290              JarOutputStream jos = new JarOutputStream(out)) {
291             if (md != null) {
292                 JarEntry je = new JarEntry("module-info.class");
293                 jos.putNextEntry(je);
294                 ModuleInfoWriter.write(md, jos);
295                 jos.closeEntry();
296             }
297 
298             for (Path entry : entries) {
299                 String name = toJarEntryName(entry);
300                 jos.putNextEntry(new JarEntry(name));
301                 Files.copy(dir.resolve(entry), jos);
302                 jos.closeEntry();
303             }
304         }
305     }
306 
toJarEntryName(Path file)307     private static String toJarEntryName(Path file) {
308         Path normalized = file.normalize();
309         return normalized.subpath(0, normalized.getNameCount())
310                 .toString()
311                 .replace(File.separatorChar, '/');
312     }
313 
findAllRegularFiles(Path dir, Path[] files)314     private static List<Path> findAllRegularFiles(Path dir, Path[] files) throws IOException {
315         List<Path> entries = new ArrayList<>();
316         for (Path file : files) {
317             try (Stream<Path> stream = Files.find(dir.resolve(file), Integer.MAX_VALUE,
318                     (p, attrs) -> attrs.isRegularFile())) {
319                 stream.map(dir::relativize)
320                         .forEach(entries::add);
321             }
322         }
323         return entries;
324     }
325 
326 }
327