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