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         if (!opts.benchmarkMode) {
440           addVerifyArchivedFields(cmd);
441         }
442 
443         if (opts.useVersion)
444             cmd.add("-version");
445 
446         for (String s : opts.suffix) cmd.add(s);
447 
448         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
449         ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(cmdLine);
450         return executeAndLog(pb, "exec");
451     }
452 
453 
454     // A commonly used convenience methods to create an archive and check the results
455     // Creates an archive and checks for errors
456     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
457         return checkExec(runWithArchive(opts));
458     }
459 
460 
461     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
462         return checkExec(runWithArchive(cliPrefix));
463     }
464 
465 
466     public static OutputAnalyzer checkExec(OutputAnalyzer output,
467                                      String... extraMatches) throws Exception {
468         CDSOptions opts = new CDSOptions();
469         return checkExec(output, opts, extraMatches);
470     }
471 
472 
473     // check result of 'exec' operation, that is when JVM is run using the archive
474     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
475                                      String... extraMatches) throws Exception {
476         try {
477             if ("on".equals(opts.xShareMode)) {
478                 output.shouldContain("sharing");
479             }
480             output.shouldHaveExitValue(0);
481         } catch (RuntimeException e) {
482             checkCommonExecExceptions(output, e);
483             return output;
484         }
485 
486         checkMatches(output, extraMatches);
487         return output;
488     }
489 
490 
491     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
492                                              int expectedExitValue,
493                                              String... extraMatches) throws Exception {
494         checkMappingFailure(output);
495         output.shouldHaveExitValue(expectedExitValue);
496         checkMatches(output, extraMatches);
497         return output;
498     }
499 
500     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
501                                               String... matches) throws Exception {
502         for (String match : matches) {
503             output.shouldContain(match);
504         }
505         return output;
506     }
507 
508     private static final String outputDir;
509     private static final File outputDirAsFile;
510 
511     static {
512         outputDir = System.getProperty("user.dir", ".");
513         outputDirAsFile = new File(outputDir);
514     }
515 
516     public static String getOutputDir() {
517         return outputDir;
518     }
519 
520     public static File getOutputDirAsFile() {
521         return outputDirAsFile;
522     }
523 
524     // get the file object for the test artifact
525     public static File getTestArtifact(String name, boolean checkExistence) {
526         File file = new File(outputDirAsFile, name);
527 
528         if (checkExistence && !file.exists()) {
529             throw new RuntimeException("Cannot find " + file.getPath());
530         }
531 
532         return file;
533     }
534 
535 
536     // create file containing the specified class list
537     public static File makeClassList(String classes[])
538         throws Exception {
539         return makeClassList(testName + "-", classes);
540     }
541 
542     // create file containing the specified class list
543     public static File makeClassList(String testCaseName, String classes[])
544         throws Exception {
545 
546         File classList = getTestArtifact(testCaseName + "test.classlist", false);
547         FileOutputStream fos = new FileOutputStream(classList);
548         PrintStream ps = new PrintStream(fos);
549 
550         addToClassList(ps, classes);
551 
552         ps.close();
553         fos.close();
554 
555         return classList;
556     }
557 
558 
559     public static void addToClassList(PrintStream ps, String classes[])
560         throws IOException
561     {
562         if (classes != null) {
563             for (String s : classes) {
564                 ps.println(s);
565             }
566         }
567     }
568 
569     private static String testName = Utils.TEST_NAME.replace('/', '.');
570 
571     private static final SimpleDateFormat timeStampFormat =
572         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
573 
574     private static String defaultArchiveName;
575 
576     // Call this method to start new archive with new unique name
577     public static void startNewArchiveName() {
578         defaultArchiveName = testName +
579             timeStampFormat.format(new Date()) + ".jsa";
580     }
581 
582     public static String getDefaultArchiveName() {
583         return defaultArchiveName;
584     }
585 
586 
587     // ===================== FILE ACCESS convenience methods
588     public static File getOutputFile(String name) {
589         return new File(outputDirAsFile, testName + "-" + name);
590     }
591 
592     public static String getOutputFileName(String name) {
593         return getOutputFile(name).getName();
594     }
595 
596 
597     public static File getOutputSourceFile(String name) {
598         return new File(outputDirAsFile, name);
599     }
600 
601 
602     public static File getSourceFile(String name) {
603         File dir = new File(System.getProperty("test.src", "."));
604         return new File(dir, name);
605     }
606 
607     // Check commandline for the last instance of Xshare to see if the process can load
608     // a CDS archive
609     public static boolean isRunningWithArchive(List<String> cmd) {
610         // -Xshare only works for the java executable
611         if (!cmd.get(0).equals(JDKToolFinder.getJDKTool("java")) || cmd.size() < 2) {
612             return false;
613         }
614 
615         // -Xshare options are likely at the end of the args list
616         for (int i = cmd.size() - 1; i >= 1; i--) {
617             String s = cmd.get(i);
618             if (s.equals("-Xshare:dump") || s.equals("-Xshare:off")) {
619                 return false;
620             }
621         }
622         return true;
623     }
624 
625     public static boolean isGCOption(String s) {
626         return s.startsWith("-XX:+Use") && s.endsWith("GC");
627     }
628 
629     public static boolean hasGCOption(List<String> cmd) {
630         for (String s : cmd) {
631             if (isGCOption(s)) {
632                 return true;
633             }
634         }
635         return false;
636     }
637 
638     // Handle and insert test.cds.runtime.options to commandline
639     // The test.cds.runtime.options property is used to inject extra VM options to
640     // subprocesses launched by the CDS test cases using executeAndLog().
641     // The injection applies only to subprocesses that:
642     //   - are launched by the standard java launcher (bin/java)
643     //   - are not dumping the CDS archive with -Xshare:dump
644     //   - do not explicitly disable CDS via -Xshare:off
645     //
646     // The main purpose of this property is to test the runtime loading of
647     // the CDS "archive heap region" with non-default garbage collectors. E.g.,
648     //
649     // jtreg -vmoptions:-Dtest.cds.runtime.options=-XX:+UnlockExperimentalVMOptions,-XX:+UseEpsilonGC \
650     //       test/hotspot/jtreg/runtime/cds
651     //
652     // Note that the injection is not applied to -Xshare:dump, so that the CDS archives
653     // will be dumped with G1, which is the only collector that supports dumping
654     // the archive heap region. Similarly, if a UseXxxGC option already exists in the command line,
655     // the UseXxxGC option added in test.cds.runtime.options will be ignored.
656     public static void handleCDSRuntimeOptions(ProcessBuilder pb) {
657         List<String> cmd = pb.command();
658         String jtropts = System.getProperty("test.cds.runtime.options");
659         if (jtropts != null && isRunningWithArchive(cmd)) {
660             // There cannot be multiple GC options in the command line so some
661             // options may be ignored
662             ArrayList<String> cdsRuntimeOpts = new ArrayList<String>();
663             boolean hasGCOption = hasGCOption(cmd);
664             for (String s : jtropts.split(",")) {
665                 if (!CDSOptions.disabledRuntimePrefixes.contains(s) &&
666                     !(hasGCOption && isGCOption(s))) {
667                     cdsRuntimeOpts.add(s);
668                 }
669             }
670             pb.command().addAll(1, cdsRuntimeOpts);
671         }
672     }
673 
674     // ============================= Logging
675     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
676         handleCDSRuntimeOptions(pb);
677         return executeAndLog(pb.start(), logName);
678     }
679 
680     public static OutputAnalyzer executeAndLog(Process process, String logName) throws Exception {
681         long started = System.currentTimeMillis();
682 
683         OutputAnalyzer output = new OutputAnalyzer(process);
684         String logFileNameStem =
685             String.format("%04d", getNextLogCounter()) + "-" + logName;
686 
687         File stdout = getOutputFile(logFileNameStem + ".stdout");
688         File stderr = getOutputFile(logFileNameStem + ".stderr");
689 
690         writeFile(stdout, output.getStdout());
691         writeFile(stderr, output.getStderr());
692         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
693         System.out.println("[logging stdout to " + stdout + "]");
694         System.out.println("[logging stderr to " + stderr + "]");
695         System.out.println("[STDERR]\n" + output.getStderr());
696 
697         if (copyChildStdoutToMainStdout)
698             System.out.println("[STDOUT]\n" + output.getStdout());
699 
700         if (output.getExitValue() != 0 && output.getStdout().contains("A fatal error has been detected")) {
701           throw new RuntimeException("Hotspot crashed");
702         }
703         return output;
704     }
705 
706 
707     private static void writeFile(File file, String content) throws Exception {
708         FileOutputStream fos = new FileOutputStream(file);
709         PrintStream ps = new PrintStream(fos);
710         ps.print(content);
711         ps.close();
712         fos.close();
713     }
714 
715     // Format a line that defines an extra symbol in the file specify by -XX:SharedArchiveConfigFile=<file>
716     public static String formatArchiveConfigSymbol(String symbol) {
717         int refCount = -1; // This is always -1 in the current HotSpot implementation.
718         if (isAsciiPrintable(symbol)) {
719             return symbol.length() + " " + refCount + ": " + symbol;
720         } else {
721             StringBuilder sb = new StringBuilder();
722             int utf8_length = escapeArchiveConfigString(sb, symbol);
723             return utf8_length + " " + refCount + ": " + sb.toString();
724         }
725     }
726 
727     // This method generates the same format as HashtableTextDump::put_utf8() in HotSpot,
728     // to be used by -XX:SharedArchiveConfigFile=<file>.
729     private static int escapeArchiveConfigString(StringBuilder sb, String s) {
730         byte arr[];
731         try {
732             arr = s.getBytes("UTF8");
733         } catch (java.io.UnsupportedEncodingException e) {
734             throw new RuntimeException("Unexpected", e);
735         }
736         for (int i = 0; i < arr.length; i++) {
737             char ch = (char)(arr[i] & 0xff);
738             if (isAsciiPrintable(ch)) {
739                 sb.append(ch);
740             } else if (ch == '\t') {
741                 sb.append("\\t");
742             } else if (ch == '\r') {
743                 sb.append("\\r");
744             } else if (ch == '\n') {
745                 sb.append("\\n");
746             } else if (ch == '\\') {
747                 sb.append("\\\\");
748             } else {
749                 String hex = Integer.toHexString(ch);
750                 if (ch < 16) {
751                     sb.append("\\x0");
752                 } else {
753                     sb.append("\\x");
754                 }
755                 sb.append(hex);
756             }
757         }
758 
759         return arr.length;
760     }
761 
762     private static boolean isAsciiPrintable(String s) {
763         for (int i = 0; i < s.length(); i++) {
764             if (!isAsciiPrintable(s.charAt(i))) {
765                 return false;
766             }
767         }
768         return true;
769     }
770 
771     private static boolean isAsciiPrintable(char ch) {
772         return ch >= 32 && ch < 127;
773     }
774 
775     // JDK utility
776 
777     // Do a cheap clone of the JDK. Most files can be sym-linked. However, $JAVA_HOME/bin/java and $JAVA_HOME/lib/.../libjvm.so"
778     // must be copied, because the java.home property is derived from the canonicalized paths of these 2 files.
779     // Set a list of {jvm, "java"} which will be physically copied. If a file needs copied physically, add it to the list.
780     private static String[] phCopied = {System.mapLibraryName("jvm"), "java"};
781     public static void clone(File src, File dst) throws Exception {
782         if (dst.exists()) {
783             if (!dst.isDirectory()) {
784                 throw new RuntimeException("Not a directory :" + dst);
785             }
786         } else {
787             if (!dst.mkdir()) {
788                 throw new RuntimeException("Cannot create directory: " + dst);
789             }
790         }
791         // final String jvmLib = System.mapLibraryName("jvm");
792         for (String child : src.list()) {
793             if (child.equals(".") || child.equals("..")) {
794                 continue;
795             }
796 
797             File child_src = new File(src, child);
798             File child_dst = new File(dst, child);
799             if (child_dst.exists()) {
800                 throw new RuntimeException("Already exists: " + child_dst);
801             }
802             if (child_src.isFile()) {
803                 boolean needPhCopy = false;
804                 for (String target : phCopied) {
805                     if (child.equals(target)) {
806                         needPhCopy = true;
807                         break;
808                     }
809                 }
810                 if (needPhCopy) {
811                     Files.copy(child_src.toPath(), /* copy data to -> */ child_dst.toPath(),
812                                new CopyOption[] { StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES});
813                 } else {
814                     Files.createSymbolicLink(child_dst.toPath(),  /* link to -> */ child_src.toPath());
815                 }
816             } else {
817                 clone(child_src, child_dst);
818             }
819         }
820     }
821 
822     // modulesDir, like $JDK/lib
823     // oldName, module name under modulesDir
824     // newName, new name for oldName
825     public static void rename(File fromFile, File toFile) throws Exception {
826         if (!fromFile.exists()) {
827             throw new RuntimeException(fromFile.getName() + " does not exist");
828         }
829 
830         if (toFile.exists()) {
831             throw new RuntimeException(toFile.getName() + " already exists");
832         }
833 
834         boolean success = fromFile.renameTo(toFile);
835         if (!success) {
836             throw new RuntimeException("rename file " + fromFile.getName()+ " to " + toFile.getName() + " failed");
837         }
838     }
839 
840     public static ProcessBuilder makeBuilder(String... args) throws Exception {
841         System.out.print("[");
842         for (String s : args) {
843             System.out.print(" " + s);
844         }
845         System.out.println(" ]");
846         return new ProcessBuilder(args);
847     }
848 
849     public static Path copyFile(String srcFile, String destDir) throws Exception {
850         int idx = srcFile.lastIndexOf(File.separator);
851         String jarName = srcFile.substring(idx + 1);
852         Path srcPath = Paths.get(jarName);
853         Path newPath = Paths.get(destDir);
854         Path newDir;
855         if (!Files.exists(newPath)) {
856             newDir = Files.createDirectories(newPath);
857         } else {
858             newDir = newPath;
859         }
860         Path destPath = newDir.resolve(jarName);
861         Files.copy(srcPath, destPath, REPLACE_EXISTING, COPY_ATTRIBUTES);
862         return destPath;
863     }
864 }