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 }