1 /*
  2  * Copyright (c) 2017, 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 package jdk.test.lib.cds;
 24 
 25 import java.io.IOException;
 26 import java.io.File;
 27 import java.io.FileOutputStream;
 28 import java.io.PrintStream;
 29 import java.nio.file.Files;
 30 import java.nio.file.CopyOption;
 31 import java.nio.file.Path;
 32 import java.nio.file.Paths;
 33 import java.nio.file.StandardCopyOption;
 34 import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
 35 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 36 import java.text.SimpleDateFormat;
 37 import java.util.List;
 38 import java.util.ArrayList;
 39 import java.util.Date;
 40 import jdk.test.lib.JDKToolFinder;
 41 import jdk.test.lib.Utils;
 42 import jdk.test.lib.process.OutputAnalyzer;
 43 import jdk.test.lib.process.ProcessTools;
 44 import jtreg.SkippedException;
 45 
 46 // This class contains common test utilities for testing CDS
 47 public class CDSTestUtils {
 48     public static final String MSG_RANGE_NOT_WITHIN_HEAP =
 49         "Unable to allocate region, range is not within java heap.";
 50     public static final String MSG_RANGE_ALREADT_IN_USE =
 51         "Unable to allocate region, java heap range is already in use.";
 52     public static final String MSG_DYNAMIC_NOT_SUPPORTED =
 53         "-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded";
 54     public static final boolean DYNAMIC_DUMP = Boolean.getBoolean("test.dynamic.cds.archive");
 55 
 56     public interface Checker {
 57         public void check(OutputAnalyzer output) throws Exception;
 58     }
 59 
 60     /*
 61      * INTRODUCTION
 62      *
 63      * When testing various CDS functionalities, we need to launch JVM processes
 64      * using a "launch method" (such as TestCommon.run), and analyze the results of these
 65      * processes.
 66      *
 67      * While typical jtreg tests would use OutputAnalyzer in such cases, due to the
 68      * complexity of CDS failure modes, we have added the CDSTestUtils.Result class
 69      * to make the analysis more convenient and less error prone.
 70      *
 71      * A Java process can end in one of the following 4 states:
 72      *
 73      *    1: Unexpected error - such as JVM crashing. In this case, the "launch method"
 74      *                          will throw a RuntimeException.
 75      *    2: Mapping Failure  - this happens when the OS (intermittently) fails to map the
 76      *                          CDS archive, normally caused by Address Space Layout Randomization.
 77      *                          We usually treat this as "pass".
 78      *    3: Normal Exit      - the JVM process has finished without crashing, and the exit code is 0.
 79      *    4: Abnormal Exit    - the JVM process has finished without crashing, and the exit code is not 0.
 80      *
 81      * In most test cases, we need to check the JVM process's output in cases 3 and 4. However, we need
 82      * to make sure that our test code is not confused by case 2.
 83      *
 84      * For example, a JVM process is expected to print the string "Hi" and exit with 0. With the old
 85      * CDSTestUtils.runWithArchive API, the test may be written as this:
 86      *
 87      *     OutputAnalyzer out = CDSTestUtils.runWithArchive(args);
 88      *     out.shouldContain("Hi");
 89      *
 90      * However, if the JVM process fails with mapping failure, the string "Hi" will not be in the output,
 91      * and your test case will fail intermittently.
 92      *
 93      * Instead, the test case should be written as
 94      *
 95      *      CDSTestUtils.run(args).assertNormalExit("Hi");
 96      *
 97      * EXAMPLES/HOWTO
 98      *
 99      * 1. For simple substring matching:
100      *
101      *      CDSTestUtils.run(args).assertNormalExit("Hi");
102      *      CDSTestUtils.run(args).assertNormalExit("a", "b", "x");
103      *      CDSTestUtils.run(args).assertAbnormalExit("failure 1", "failure2");
104      *
105      * 2. For more complex output matching: using Lambda expressions
106      *
107      *      CDSTestUtils.run(args)
108      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed");
109      *      CDSTestUtils.run(args)
110      *         .assertAbnormalExit(output -> {
111      *             output.shouldNotContain("this should not be printed");
112      *             output.shouldHaveExitValue(123);
113      *           });
114      *
115      * 3. Chaining several checks:
116      *
117      *      CDSTestUtils.run(args)
118      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed")
119      *         .assertNormalExit("should have this", "should have that");
120      *
121      * 4. [Rare use case] if a test sometimes exit normally, and sometimes abnormally:
122      *
123      *      CDSTestUtils.run(args)
124      *         .ifNormalExit("ths string is printed when exiting with 0")
125      *         .ifAbNormalExit("ths string is printed when exiting with 1");
126      *
127      *    NOTE: you usually don't want to write your test case like this -- it should always
128      *    exit with the same exit code. (But I kept this API because some existing test cases
129      *    behave this way -- need to revisit).
130      */
131     public static class Result {
132         private final OutputAnalyzer output;
133         private final CDSOptions options;
134         private final boolean hasNormalExit;
135         private final String CDS_DISABLED = "warning: CDS is disabled when the";
136 
137         public Result(CDSOptions opts, OutputAnalyzer out) throws Exception {
138             checkMappingFailure(out);
139             this.options = opts;
140             this.output = out;
141             hasNormalExit = (output.getExitValue() == 0);
142 
143             if (hasNormalExit) {
144                 if ("on".equals(options.xShareMode) &&
145                     output.getStderr().contains("java version") &&
146                     !output.getStderr().contains(CDS_DISABLED)) {
147                     // "-showversion" is always passed in the command-line by the execXXX methods.
148                     // During normal exit, we require that the VM to show that sharing was enabled.
149                     output.shouldContain("sharing");
150                 }
151             }
152         }
153 
154         public Result assertNormalExit(Checker checker) throws Exception {
155             checker.check(output);
156             output.shouldHaveExitValue(0);
157             return this;
158         }
159 
160         public Result assertAbnormalExit(Checker checker) throws Exception {
161             checker.check(output);
162             output.shouldNotHaveExitValue(0);
163             return this;
164         }
165 
166         // When {--limit-modules, --patch-module, and/or --upgrade-module-path}
167         // are specified, CDS is silently disabled for both -Xshare:auto and -Xshare:on.
168         public Result assertSilentlyDisabledCDS(Checker checker) throws Exception {
169             // this comes from a JVM warning message.
170             output.shouldContain(CDS_DISABLED);
171             checker.check(output);
172             return this;
173         }
174 
175         public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception {
176             return assertSilentlyDisabledCDS((out) -> {
177                 out.shouldHaveExitValue(exitCode);
178                 checkMatches(out, matches);
179                    });
180         }
181 
182         public Result ifNormalExit(Checker checker) throws Exception {
183             if (hasNormalExit) {
184                 checker.check(output);
185             }
186             return this;
187         }
188 
189         public Result ifAbnormalExit(Checker checker) throws Exception {
190             if (!hasNormalExit) {
191                 checker.check(output);
192             }
193             return this;
194         }
195 
196         public Result ifNoMappingFailure(Checker checker) throws Exception {
197             checker.check(output);
198             return this;
199         }
200 
201 
202         public Result assertNormalExit(String... matches) throws Exception {
203             checkMatches(output, matches);
204             output.shouldHaveExitValue(0);
205             return this;
206         }
207 
208         public Result assertAbnormalExit(String... matches) throws Exception {
209             checkMatches(output, matches);
210             output.shouldNotHaveExitValue(0);
211             return this;
212         }
213     }
214 
215     // A number to be included in the filename of the stdout and the stderr output file.
216     static int logCounter = 0;
217 
218     private static int getNextLogCounter() {
219         return logCounter++;
220     }
221 
222     // By default, stdout of child processes are logged in files such as
223     // <testname>-0000-exec.stdout. If you want to also include the stdout
224     // inside jtr files, you can override this in the jtreg command line like
225     // "jtreg -Dtest.cds.copy.child.stdout=true ...."
226     public static final boolean copyChildStdoutToMainStdout =
227         Boolean.getBoolean("test.cds.copy.child.stdout");
228 
229     // This property is passed to child test processes
230     public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0");
231 
232     public static final String UnableToMapMsg =
233         "Unable to map shared archive: test did not complete";
234 
235     // Create bootstrap CDS archive,
236     // use extra JVM command line args as a prefix.
237     // For CDS tests specifying prefix makes more sense than specifying suffix, since
238     // normally there are no classes or arguments to classes, just "-version"
239     // To specify suffix explicitly use CDSOptions.addSuffix()
240     public static OutputAnalyzer createArchive(String... cliPrefix)
241         throws Exception {
242         return createArchive((new CDSOptions()).addPrefix(cliPrefix));
243     }
244 
245     // Create bootstrap CDS archive
246     public static OutputAnalyzer createArchive(CDSOptions opts)
247         throws Exception {
248 
249         startNewArchiveName();
250 
251         ArrayList<String> cmd = new ArrayList<String>();
252 
253         for (String p : opts.prefix) cmd.add(p);
254 
255         cmd.add("-Xshare:dump");
256         cmd.add("-Xlog:cds,cds+hashtables");
257         if (opts.archiveName == null)
258             opts.archiveName = getDefaultArchiveName();
259         cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
260 
261         if (opts.classList != null) {
262             File classListFile = makeClassList(opts.classList);
263             cmd.add("-XX:ExtraSharedClassListFile=" + classListFile.getPath());
264         }
265 
266         for (String s : opts.suffix) cmd.add(s);
267 
268         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
269         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
270         return executeAndLog(pb, "dump");
271     }
272 
273     public static boolean isDynamicArchive() {
274         return DYNAMIC_DUMP;
275     }
276 
277     // check result of 'dump-the-archive' operation, that is "-Xshare:dump"
278     public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches)
279         throws Exception {
280 
281         if (!DYNAMIC_DUMP) {
282             output.shouldContain("Loading classes to share");
283         } else {
284             output.shouldContain("Written dynamic archive 0x");
285         }
286         output.shouldHaveExitValue(0);
287 
288         for (String match : extraMatches) {
289             output.shouldContain(match);
290         }
291 
292         return output;
293     }
294 
295     // check result of dumping base archive
296     public static OutputAnalyzer checkBaseDump(OutputAnalyzer output) throws Exception {
297         output.shouldContain("Loading classes to share");
298         output.shouldHaveExitValue(0);
299         return output;
300     }
301 
302     // A commonly used convenience methods to create an archive and check the results
303     // Creates an archive and checks for errors
304     public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts)
305         throws Exception {
306         return checkDump(createArchive(opts));
307     }
308 
309 
310     public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix)
311         throws Exception {
312         return checkDump(createArchive(cliPrefix));
313     }
314 
315 
316     // This method should be used to check the output of child VM for common exceptions.
317     // Most of CDS tests deal with child VM processes for creating and using the archive.
318     // However exceptions that occur in the child process do not automatically propagate
319     // to the parent process. This mechanism aims to improve the propagation
320     // of exceptions and common errors.
321     // Exception e argument - an exception to be re-thrown if none of the common
322     // exceptions match. Pass null if you wish not to re-throw any exception.
323     public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e)
324         throws Exception {
325         if (output.getStdout().contains("https://bugreport.java.com/bugreport/crash.jsp")) {
326             throw new RuntimeException("Hotspot crashed");
327         }
328         if (output.getStdout().contains("TEST FAILED")) {
329             throw new RuntimeException("Test Failed");
330         }
331         if (output.getOutput().contains("Unable to unmap shared space")) {
332             throw new RuntimeException("Unable to unmap shared space");
333         }
334 
335         // Special case -- sometimes Xshare:on fails because it failed to map
336         // at given address. This behavior is platform-specific, machine config-specific
337         // and can be random (see ASLR).
338         checkMappingFailure(output);
339 
340         if (e != null) {
341             throw e;
342         }
343     }
344 
345     public static void checkCommonExecExceptions(OutputAnalyzer output) throws Exception {
346         checkCommonExecExceptions(output, null);
347     }
348 
349 
350     // Check the output for indication that mapping of the archive failed.
351     // Performance note: this check seems to be rather costly - searching the entire
352     // output stream of a child process for multiple strings. However, it is necessary
353     // to detect this condition, a failure to map an archive, since this is not a real
354     // failure of the test or VM operation, and results in a test being "skipped".
355     // Suggestions to improve:
356     // 1. VM can designate a special exit code for such condition.
357     // 2. VM can print a single distinct string indicating failure to map an archive,
358     //    instead of utilizing multiple messages.
359     // These are suggestions to improve testibility of the VM. However, implementing them
360     // could also improve usability in the field.
361     private static String hasUnableToMapMessage(OutputAnalyzer output) {
362         String outStr = output.getOutput();
363         if ((output.getExitValue() == 1)) {
364             if (outStr.contains(MSG_RANGE_NOT_WITHIN_HEAP)) {
365                 return MSG_RANGE_NOT_WITHIN_HEAP;
366             }
367             if (outStr.contains(MSG_DYNAMIC_NOT_SUPPORTED)) {
368                 return MSG_DYNAMIC_NOT_SUPPORTED;
369             }
370         }
371 
372         return null;
373     }
374 
375     public static boolean isUnableToMap(OutputAnalyzer output) {
376         return hasUnableToMapMessage(output) != null;
377     }
378 
379     public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException {
380         String match = hasUnableToMapMessage(out);
381         if (match != null) {
382             throw new SkippedException(UnableToMapMsg + ": " + match);
383         }
384     }
385 
386     public static Result run(String... cliPrefix) throws Exception {
387         CDSOptions opts = new CDSOptions();
388         opts.setArchiveName(getDefaultArchiveName());
389         opts.addPrefix(cliPrefix);
390         return new Result(opts, runWithArchive(opts));
391     }
392 
393     public static Result run(CDSOptions opts) throws Exception {
394         return new Result(opts, runWithArchive(opts));
395     }
396 
397     // Dump a classlist using the -XX:DumpLoadedClassList option.
398     public static Result dumpClassList(String classListName, String... cli)
399         throws Exception {
400         CDSOptions opts = (new CDSOptions())
401             .setUseVersion(false)
402             .setXShareMode("auto")
403             .addPrefix("-XX:DumpLoadedClassList=" + classListName)
404             .addSuffix(cli);
405         Result res = run(opts).assertNormalExit();
406         return res;
407     }
408 
409     // Execute JVM with CDS archive, specify command line args suffix
410     public static OutputAnalyzer runWithArchive(String... cliPrefix)
411         throws Exception {
412 
413         return runWithArchive( (new CDSOptions())
414                                .setArchiveName(getDefaultArchiveName())
415                                .addPrefix(cliPrefix) );
416     }
417 
418     // Enable basic verification (VerifyArchivedFields=1, no side effects) for all CDS
419     // tests to make sure the archived heap objects are mapped/loaded properly.
420     public static void addVerifyArchivedFields(ArrayList<String> cmd) {
421         cmd.add("-XX:+UnlockDiagnosticVMOptions");
422         cmd.add("-XX:VerifyArchivedFields=1");
423     }
424 
425     // Execute JVM with CDS archive, specify CDSOptions
426     public static OutputAnalyzer runWithArchive(CDSOptions opts)
427         throws Exception {
428 
429         ArrayList<String> cmd = new ArrayList<String>();
430         cmd.addAll(opts.prefix);
431         cmd.add("-Xshare:" + opts.xShareMode);
432         cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor);
433 
434         if (!opts.useSystemArchive) {
435             if (opts.archiveName == null)
436                 opts.archiveName = getDefaultArchiveName();
437             cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
438         }
439         addVerifyArchivedFields(cmd);
440 
441         if (opts.useVersion)
442             cmd.add("-version");
443 
444         for (String s : opts.suffix) cmd.add(s);
445 
446         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
447         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
448         return executeAndLog(pb, "exec");
449     }
450 
451 
452     // A commonly used convenience methods to create an archive and check the results
453     // Creates an archive and checks for errors
454     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
455         return checkExec(runWithArchive(opts));
456     }
457 
458 
459     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
460         return checkExec(runWithArchive(cliPrefix));
461     }
462 
463 
464     public static OutputAnalyzer checkExec(OutputAnalyzer output,
465                                      String... extraMatches) throws Exception {
466         CDSOptions opts = new CDSOptions();
467         return checkExec(output, opts, extraMatches);
468     }
469 
470 
471     // check result of 'exec' operation, that is when JVM is run using the archive
472     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
473                                      String... extraMatches) throws Exception {
474         try {
475             if ("on".equals(opts.xShareMode)) {
476                 output.shouldContain("sharing");
477             }
478             output.shouldHaveExitValue(0);
479         } catch (RuntimeException e) {
480             checkCommonExecExceptions(output, e);
481             return output;
482         }
483 
484         checkMatches(output, extraMatches);
485         return output;
486     }
487 
488 
489     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
490                                              int expectedExitValue,
491                                              String... extraMatches) throws Exception {
492         checkMappingFailure(output);
493         output.shouldHaveExitValue(expectedExitValue);
494         checkMatches(output, extraMatches);
495         return output;
496     }
497 
498     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
499                                               String... matches) throws Exception {
500         for (String match : matches) {
501             output.shouldContain(match);
502         }
503         return output;
504     }
505 
506     private static final String outputDir;
507     private static final File outputDirAsFile;
508 
509     static {
510         outputDir = System.getProperty("user.dir", ".");
511         outputDirAsFile = new File(outputDir);
512     }
513 
514     public static String getOutputDir() {
515         return outputDir;
516     }
517 
518     public static File getOutputDirAsFile() {
519         return outputDirAsFile;
520     }
521 
522     // get the file object for the test artifact
523     public static File getTestArtifact(String name, boolean checkExistence) {
524         File file = new File(outputDirAsFile, name);
525 
526         if (checkExistence && !file.exists()) {
527             throw new RuntimeException("Cannot find " + file.getPath());
528         }
529 
530         return file;
531     }
532 
533 
534     // create file containing the specified class list
535     public static File makeClassList(String classes[])
536         throws Exception {
537         return makeClassList(testName + "-", classes);
538     }
539 
540     // create file containing the specified class list
541     public static File makeClassList(String testCaseName, String classes[])
542         throws Exception {
543 
544         File classList = getTestArtifact(testCaseName + "test.classlist", false);
545         FileOutputStream fos = new FileOutputStream(classList);
546         PrintStream ps = new PrintStream(fos);
547 
548         addToClassList(ps, classes);
549 
550         ps.close();
551         fos.close();
552 
553         return classList;
554     }
555 
556 
557     public static void addToClassList(PrintStream ps, String classes[])
558         throws IOException
559     {
560         if (classes != null) {
561             for (String s : classes) {
562                 ps.println(s);
563             }
564         }
565     }
566 
567     private static String testName = Utils.TEST_NAME.replace('/', '.');
568 
569     private static final SimpleDateFormat timeStampFormat =
570         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
571 
572     private static String defaultArchiveName;
573 
574     // Call this method to start new archive with new unique name
575     public static void startNewArchiveName() {
576         defaultArchiveName = testName +
577             timeStampFormat.format(new Date()) + ".jsa";
578     }
579 
580     public static String getDefaultArchiveName() {
581         return defaultArchiveName;
582     }
583 
584 
585     // ===================== FILE ACCESS convenience methods
586     public static File getOutputFile(String name) {
587         return new File(outputDirAsFile, testName + "-" + name);
588     }
589 
590     public static String getOutputFileName(String name) {
591         return getOutputFile(name).getName();
592     }
593 
594 
595     public static File getOutputSourceFile(String name) {
596         return new File(outputDirAsFile, name);
597     }
598 
599 
600     public static File getSourceFile(String name) {
601         File dir = new File(System.getProperty("test.src", "."));
602         return new File(dir, name);
603     }
604 
605     // Check commandline for the last instance of Xshare to see if the process can load
606     // a CDS archive
607     public static boolean isRunningWithArchive(List<String> cmd) {
608         // -Xshare only works for the java executable
609         if (!cmd.get(0).equals(JDKToolFinder.getJDKTool("java")) || cmd.size() < 2) {
610             return false;
611         }
612 
613         // -Xshare options are likely at the end of the args list
614         for (int i = cmd.size() - 1; i >= 1; i--) {
615             String s = cmd.get(i);
616             if (s.equals("-Xshare:dump") || s.equals("-Xshare:off")) {
617                 return false;
618             }
619         }
620         return true;
621     }
622 
623     public static boolean isGCOption(String s) {
624         return s.startsWith("-XX:+Use") && s.endsWith("GC");
625     }
626 
627     public static boolean hasGCOption(List<String> cmd) {
628         for (String s : cmd) {
629             if (isGCOption(s)) {
630                 return true;
631             }
632         }
633         return false;
634     }
635 
636     // Handle and insert test.cds.runtime.options to commandline
637     // The test.cds.runtime.options property is used to inject extra VM options to
638     // subprocesses launched by the CDS test cases using executeAndLog().
639     // The injection applies only to subprocesses that:
640     //   - are launched by the standard java launcher (bin/java)
641     //   - are not dumping the CDS archive with -Xshare:dump
642     //   - do not explicitly disable CDS via -Xshare:off
643     //
644     // The main purpose of this property is to test the runtime loading of
645     // the CDS "archive heap region" with non-default garbage collectors. E.g.,
646     //
647     // jtreg -vmoptions:-Dtest.cds.runtime.options=-XX:+UnlockExperimentalVMOptions,-XX:+UseEpsilonGC \
648     //       test/hotspot/jtreg/runtime/cds
649     //
650     // Note that the injection is not applied to -Xshare:dump, so that the CDS archives
651     // will be dumped with G1, which is the only collector that supports dumping
652     // the archive heap region. Similarly, if a UseXxxGC option already exists in the command line,
653     // the UseXxxGC option added in test.cds.runtime.options will be ignored.
654     public static void handleCDSRuntimeOptions(ProcessBuilder pb) {
655         List<String> cmd = pb.command();
656         String jtropts = System.getProperty("test.cds.runtime.options");
657         if (jtropts != null && isRunningWithArchive(cmd)) {
658             // There cannot be multiple GC options in the command line so some
659             // options may be ignored
660             ArrayList<String> cdsRuntimeOpts = new ArrayList<String>();
661             boolean hasGCOption = hasGCOption(cmd);
662             for (String s : jtropts.split(",")) {
663                 if (!CDSOptions.disabledRuntimePrefixes.contains(s) &&
664                     !(hasGCOption && isGCOption(s))) {
665                     cdsRuntimeOpts.add(s);
666                 }
667             }
668             pb.command().addAll(1, cdsRuntimeOpts);
669         }
670     }
671 
672     // ============================= Logging
673     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
674         handleCDSRuntimeOptions(pb);
675         return executeAndLog(pb.start(), logName);
676     }
677 
678     public static OutputAnalyzer executeAndLog(Process process, String logName) throws Exception {
679         long started = System.currentTimeMillis();
680 
681         OutputAnalyzer output = new OutputAnalyzer(process);
682         String logFileNameStem =
683             String.format("%04d", getNextLogCounter()) + "-" + logName;
684 
685         File stdout = getOutputFile(logFileNameStem + ".stdout");
686         File stderr = getOutputFile(logFileNameStem + ".stderr");
687 
688         writeFile(stdout, output.getStdout());
689         writeFile(stderr, output.getStderr());
690         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
691         System.out.println("[logging stdout to " + stdout + "]");
692         System.out.println("[logging stderr to " + stderr + "]");
693         System.out.println("[STDERR]\n" + output.getStderr());
694 
695         if (copyChildStdoutToMainStdout)
696             System.out.println("[STDOUT]\n" + output.getStdout());
697 
698         if (output.getExitValue() != 0 && output.getStdout().contains("A fatal error has been detected")) {
699           throw new RuntimeException("Hotspot crashed");
700         }
701         return output;
702     }
703 
704 
705     private static void writeFile(File file, String content) throws Exception {
706         FileOutputStream fos = new FileOutputStream(file);
707         PrintStream ps = new PrintStream(fos);
708         ps.print(content);
709         ps.close();
710         fos.close();
711     }
712 
713     // Format a line that defines an extra symbol in the file specify by -XX:SharedArchiveConfigFile=<file>
714     public static String formatArchiveConfigSymbol(String symbol) {
715         int refCount = -1; // This is always -1 in the current HotSpot implementation.
716         if (isAsciiPrintable(symbol)) {
717             return symbol.length() + " " + refCount + ": " + symbol;
718         } else {
719             StringBuilder sb = new StringBuilder();
720             int utf8_length = escapeArchiveConfigString(sb, symbol);
721             return utf8_length + " " + refCount + ": " + sb.toString();
722         }
723     }
724 
725     // This method generates the same format as HashtableTextDump::put_utf8() in HotSpot,
726     // to be used by -XX:SharedArchiveConfigFile=<file>.
727     private static int escapeArchiveConfigString(StringBuilder sb, String s) {
728         byte arr[];
729         try {
730             arr = s.getBytes("UTF8");
731         } catch (java.io.UnsupportedEncodingException e) {
732             throw new RuntimeException("Unexpected", e);
733         }
734         for (int i = 0; i < arr.length; i++) {
735             char ch = (char)(arr[i] & 0xff);
736             if (isAsciiPrintable(ch)) {
737                 sb.append(ch);
738             } else if (ch == '\t') {
739                 sb.append("\\t");
740             } else if (ch == '\r') {
741                 sb.append("\\r");
742             } else if (ch == '\n') {
743                 sb.append("\\n");
744             } else if (ch == '\\') {
745                 sb.append("\\\\");
746             } else {
747                 String hex = Integer.toHexString(ch);
748                 if (ch < 16) {
749                     sb.append("\\x0");
750                 } else {
751                     sb.append("\\x");
752                 }
753                 sb.append(hex);
754             }
755         }
756 
757         return arr.length;
758     }
759 
760     private static boolean isAsciiPrintable(String s) {
761         for (int i = 0; i < s.length(); i++) {
762             if (!isAsciiPrintable(s.charAt(i))) {
763                 return false;
764             }
765         }
766         return true;
767     }
768 
769     private static boolean isAsciiPrintable(char ch) {
770         return ch >= 32 && ch < 127;
771     }
772 
773     // JDK utility
774 
775     // Do a cheap clone of the JDK. Most files can be sym-linked. However, $JAVA_HOME/bin/java and $JAVA_HOME/lib/.../libjvm.so"
776     // must be copied, because the java.home property is derived from the canonicalized paths of these 2 files.
777     // Set a list of {jvm, "java"} which will be physically copied. If a file needs copied physically, add it to the list.
778     private static String[] phCopied = {System.mapLibraryName("jvm"), "java"};
779     public static void clone(File src, File dst) throws Exception {
780         if (dst.exists()) {
781             if (!dst.isDirectory()) {
782                 throw new RuntimeException("Not a directory :" + dst);
783             }
784         } else {
785             if (!dst.mkdir()) {
786                 throw new RuntimeException("Cannot create directory: " + dst);
787             }
788         }
789         // final String jvmLib = System.mapLibraryName("jvm");
790         for (String child : src.list()) {
791             if (child.equals(".") || child.equals("..")) {
792                 continue;
793             }
794 
795             File child_src = new File(src, child);
796             File child_dst = new File(dst, child);
797             if (child_dst.exists()) {
798                 throw new RuntimeException("Already exists: " + child_dst);
799             }
800             if (child_src.isFile()) {
801                 boolean needPhCopy = false;
802                 for (String target : phCopied) {
803                     if (child.equals(target)) {
804                         needPhCopy = true;
805                         break;
806                     }
807                 }
808                 if (needPhCopy) {
809                     Files.copy(child_src.toPath(), /* copy data to -> */ child_dst.toPath(),
810                                new CopyOption[] { StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES});
811                 } else {
812                     Files.createSymbolicLink(child_dst.toPath(),  /* link to -> */ child_src.toPath());
813                 }
814             } else {
815                 clone(child_src, child_dst);
816             }
817         }
818     }
819 
820     // modulesDir, like $JDK/lib
821     // oldName, module name under modulesDir
822     // newName, new name for oldName
823     public static void rename(File fromFile, File toFile) throws Exception {
824         if (!fromFile.exists()) {
825             throw new RuntimeException(fromFile.getName() + " does not exist");
826         }
827 
828         if (toFile.exists()) {
829             throw new RuntimeException(toFile.getName() + " already exists");
830         }
831 
832         boolean success = fromFile.renameTo(toFile);
833         if (!success) {
834             throw new RuntimeException("rename file " + fromFile.getName()+ " to " + toFile.getName() + " failed");
835         }
836     }
837 
838     public static ProcessBuilder makeBuilder(String... args) throws Exception {
839         System.out.print("[");
840         for (String s : args) {
841             System.out.print(" " + s);
842         }
843         System.out.println(" ]");
844         return new ProcessBuilder(args);
845     }
846 
847     public static Path copyFile(String srcFile, String destDir) throws Exception {
848         int idx = srcFile.lastIndexOf(File.separator);
849         String jarName = srcFile.substring(idx + 1);
850         Path srcPath = Paths.get(jarName);
851         Path newPath = Paths.get(destDir);
852         Path newDir;
853         if (!Files.exists(newPath)) {
854             newDir = Files.createDirectories(newPath);
855         } else {
856             newDir = newPath;
857         }
858         Path destPath = newDir.resolve(jarName);
859         Files.copy(srcPath, destPath, REPLACE_EXISTING, COPY_ATTRIBUTES);
860         return destPath;
861     }
862 }