1 /*
   2  * Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 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.text.SimpleDateFormat;
  30 import java.util.ArrayList;
  31 import java.util.Date;
  32 import jdk.test.lib.Utils;
  33 import jdk.test.lib.process.OutputAnalyzer;
  34 import jdk.test.lib.process.ProcessTools;
  35 import jtreg.SkippedException;
  36 
  37 // This class contains common test utilities for testing CDS
  38 public class CDSTestUtils {
  39     public static final String MSG_RANGE_NOT_WITHIN_HEAP =
  40         "UseSharedSpaces: Unable to allocate region, range is not within java heap.";
  41     public static final String MSG_RANGE_ALREADT_IN_USE =
  42         "Unable to allocate region, java heap range is already in use.";
  43 
  44     public static final boolean DYNAMIC_DUMP = Boolean.getBoolean("test.dynamic.cds.archive");
  45 
  46     public interface Checker {
  47         public void check(OutputAnalyzer output) throws Exception;
  48     }
  49 
  50     /*
  51      * INTRODUCTION
  52      *
  53      * When testing various CDS functionalities, we need to launch JVM processes
  54      * using a "launch method" (such as TestCommon.run), and analyze the results of these
  55      * processes.
  56      *
  57      * While typical jtreg tests would use OutputAnalyzer in such cases, due to the
  58      * complexity of CDS failure modes, we have added the CDSTestUtils.Result class
  59      * to make the analysis more convenient and less error prone.
  60      *
  61      * A Java process can end in one of the following 4 states:
  62      *
  63      *    1: Unexpected error - such as JVM crashing. In this case, the "launch method"
  64      *                          will throw a RuntimeException.
  65      *    2: Mapping Failure  - this happens when the OS (intermittently) fails to map the
  66      *                          CDS archive, normally caused by Address Space Layout Randomization.
  67      *                          We usually treat this as "pass".
  68      *    3: Normal Exit      - the JVM process has finished without crashing, and the exit code is 0.
  69      *    4: Abnormal Exit    - the JVM process has finished without crashing, and the exit code is not 0.
  70      *
  71      * In most test cases, we need to check the JVM process's output in cases 3 and 4. However, we need
  72      * to make sure that our test code is not confused by case 2.
  73      *
  74      * For example, a JVM process is expected to print the string "Hi" and exit with 0. With the old
  75      * CDSTestUtils.runWithArchive API, the test may be written as this:
  76      *
  77      *     OutputAnalyzer out = CDSTestUtils.runWithArchive(args);
  78      *     out.shouldContain("Hi");
  79      *
  80      * However, if the JVM process fails with mapping failure, the string "Hi" will not be in the output,
  81      * and your test case will fail intermittently.
  82      *
  83      * Instead, the test case should be written as
  84      *
  85      *      CDSTestUtils.run(args).assertNormalExit("Hi");
  86      *
  87      * EXAMPLES/HOWTO
  88      *
  89      * 1. For simple substring matching:
  90      *
  91      *      CDSTestUtils.run(args).assertNormalExit("Hi");
  92      *      CDSTestUtils.run(args).assertNormalExit("a", "b", "x");
  93      *      CDSTestUtils.run(args).assertAbnormalExit("failure 1", "failure2");
  94      *
  95      * 2. For more complex output matching: using Lambda expressions
  96      *
  97      *      CDSTestUtils.run(args)
  98      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed");
  99      *      CDSTestUtils.run(args)
 100      *         .assertAbnormalExit(output -> {
 101      *             output.shouldNotContain("this should not be printed");
 102      *             output.shouldHaveExitValue(123);
 103      *           });
 104      *
 105      * 3. Chaining several checks:
 106      *
 107      *      CDSTestUtils.run(args)
 108      *         .assertNormalExit(output -> output.shouldNotContain("this should not be printed")
 109      *         .assertNormalExit("should have this", "should have that");
 110      *
 111      * 4. [Rare use case] if a test sometimes exit normally, and sometimes abnormally:
 112      *
 113      *      CDSTestUtils.run(args)
 114      *         .ifNormalExit("ths string is printed when exiting with 0")
 115      *         .ifAbNormalExit("ths string is printed when exiting with 1");
 116      *
 117      *    NOTE: you usually don't want to write your test case like this -- it should always
 118      *    exit with the same exit code. (But I kept this API because some existing test cases
 119      *    behave this way -- need to revisit).
 120      */
 121     public static class Result {
 122         private final OutputAnalyzer output;
 123         private final CDSOptions options;
 124         private final boolean hasNormalExit;
 125         private final String CDS_DISABLED = "warning: CDS is disabled when the";
 126 
 127         public Result(CDSOptions opts, OutputAnalyzer out) throws Exception {
 128             checkMappingFailure(out);
 129             this.options = opts;
 130             this.output = out;
 131             hasNormalExit = (output.getExitValue() == 0);
 132 
 133             if (hasNormalExit) {
 134                 if ("on".equals(options.xShareMode) &&
 135                     output.getStderr().contains("java version") &&
 136                     !output.getStderr().contains(CDS_DISABLED)) {
 137                     // "-showversion" is always passed in the command-line by the execXXX methods.
 138                     // During normal exit, we require that the VM to show that sharing was enabled.
 139                     output.shouldContain("sharing");
 140                 }
 141             }
 142         }
 143 
 144         public Result assertNormalExit(Checker checker) throws Exception {
 145             checker.check(output);
 146             output.shouldHaveExitValue(0);
 147             return this;
 148         }
 149 
 150         public Result assertAbnormalExit(Checker checker) throws Exception {
 151             checker.check(output);
 152             output.shouldNotHaveExitValue(0);
 153             return this;
 154         }
 155 
 156         // When {--limit-modules, --patch-module, and/or --upgrade-module-path}
 157         // are specified, CDS is silently disabled for both -Xshare:auto and -Xshare:on.
 158         public Result assertSilentlyDisabledCDS(Checker checker) throws Exception {
 159             // this comes from a JVM warning message.
 160             output.shouldContain(CDS_DISABLED);
 161             checker.check(output);
 162             return this;
 163         }
 164 
 165         public Result assertSilentlyDisabledCDS(int exitCode, String... matches) throws Exception {
 166             return assertSilentlyDisabledCDS((out) -> {
 167                 out.shouldHaveExitValue(exitCode);
 168                 checkMatches(out, matches);
 169                    });
 170         }
 171 
 172         public Result ifNormalExit(Checker checker) throws Exception {
 173             if (hasNormalExit) {
 174                 checker.check(output);
 175             }
 176             return this;
 177         }
 178 
 179         public Result ifAbnormalExit(Checker checker) throws Exception {
 180             if (!hasNormalExit) {
 181                 checker.check(output);
 182             }
 183             return this;
 184         }
 185 
 186         public Result ifNoMappingFailure(Checker checker) throws Exception {
 187             checker.check(output);
 188             return this;
 189         }
 190 
 191 
 192         public Result assertNormalExit(String... matches) throws Exception {
 193             checkMatches(output, matches);
 194             output.shouldHaveExitValue(0);
 195             return this;
 196         }
 197 
 198         public Result assertAbnormalExit(String... matches) throws Exception {
 199             checkMatches(output, matches);
 200             output.shouldNotHaveExitValue(0);
 201             return this;
 202         }
 203     }
 204 
 205     // A number to be included in the filename of the stdout and the stderr output file.
 206     static int logCounter = 0;
 207 
 208     private static int getNextLogCounter() {
 209         return logCounter++;
 210     }
 211 
 212     // By default, stdout of child processes are logged in files such as
 213     // <testname>-0000-exec.stdout. If you want to also include the stdout
 214     // inside jtr files, you can override this in the jtreg command line like
 215     // "jtreg -Dtest.cds.copy.child.stdout=true ...."
 216     public static final boolean copyChildStdoutToMainStdout =
 217         Boolean.getBoolean("test.cds.copy.child.stdout");
 218 
 219     // This property is passed to child test processes
 220     public static final String TestTimeoutFactor = System.getProperty("test.timeout.factor", "1.0");
 221 
 222     public static final String UnableToMapMsg =
 223         "Unable to map shared archive: test did not complete";
 224 
 225     // Create bootstrap CDS archive,
 226     // use extra JVM command line args as a prefix.
 227     // For CDS tests specifying prefix makes more sense than specifying suffix, since
 228     // normally there are no classes or arguments to classes, just "-version"
 229     // To specify suffix explicitly use CDSOptions.addSuffix()
 230     public static OutputAnalyzer createArchive(String... cliPrefix)
 231         throws Exception {
 232         return createArchive((new CDSOptions()).addPrefix(cliPrefix));
 233     }
 234 
 235     // Create bootstrap CDS archive
 236     public static OutputAnalyzer createArchive(CDSOptions opts)
 237         throws Exception {
 238 
 239         startNewArchiveName();
 240 
 241         ArrayList<String> cmd = new ArrayList<String>();
 242 
 243         for (String p : opts.prefix) cmd.add(p);
 244 
 245         cmd.add("-Xshare:dump");
 246         cmd.add("-Xlog:cds,cds+hashtables");
 247         if (opts.archiveName == null)
 248             opts.archiveName = getDefaultArchiveName();
 249         cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
 250 
 251         if (opts.classList != null) {
 252             File classListFile = makeClassList(opts.classList);
 253             cmd.add("-XX:ExtraSharedClassListFile=" + classListFile.getPath());
 254         }
 255 
 256         for (String s : opts.suffix) cmd.add(s);
 257 
 258         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
 259         ProcessBuilder pb = ProcessTools.createTestJvm(cmdLine);
 260         return executeAndLog(pb, "dump");
 261     }
 262 
 263     public static boolean isDynamicArchive() {
 264         return DYNAMIC_DUMP;
 265     }
 266 
 267     // check result of 'dump-the-archive' operation, that is "-Xshare:dump"
 268     public static OutputAnalyzer checkDump(OutputAnalyzer output, String... extraMatches)
 269         throws Exception {
 270 
 271         if (!DYNAMIC_DUMP) {
 272             output.shouldContain("Loading classes to share");
 273         } else {
 274             output.shouldContain("Buffer-space to target-space delta")
 275                   .shouldContain("Written dynamic archive 0x");
 276         }
 277         output.shouldHaveExitValue(0);
 278 
 279         for (String match : extraMatches) {
 280             output.shouldContain(match);
 281         }
 282 
 283         return output;
 284     }
 285 
 286     // check result of dumping base archive
 287     public static OutputAnalyzer checkBaseDump(OutputAnalyzer output) throws Exception {
 288         output.shouldContain("Loading classes to share");
 289         output.shouldHaveExitValue(0);
 290         return output;
 291     }
 292 
 293     // A commonly used convenience methods to create an archive and check the results
 294     // Creates an archive and checks for errors
 295     public static OutputAnalyzer createArchiveAndCheck(CDSOptions opts)
 296         throws Exception {
 297         return checkDump(createArchive(opts));
 298     }
 299 
 300 
 301     public static OutputAnalyzer createArchiveAndCheck(String... cliPrefix)
 302         throws Exception {
 303         return checkDump(createArchive(cliPrefix));
 304     }
 305 
 306 
 307     // This method should be used to check the output of child VM for common exceptions.
 308     // Most of CDS tests deal with child VM processes for creating and using the archive.
 309     // However exceptions that occur in the child process do not automatically propagate
 310     // to the parent process. This mechanism aims to improve the propagation
 311     // of exceptions and common errors.
 312     // Exception e argument - an exception to be re-thrown if none of the common
 313     // exceptions match. Pass null if you wish not to re-throw any exception.
 314     public static void checkCommonExecExceptions(OutputAnalyzer output, Exception e)
 315         throws Exception {
 316         if (output.getStdout().contains("https://bugreport.java.com/bugreport/crash.jsp")) {
 317             throw new RuntimeException("Hotspot crashed");
 318         }
 319         if (output.getStdout().contains("TEST FAILED")) {
 320             throw new RuntimeException("Test Failed");
 321         }
 322         if (output.getOutput().contains("Unable to unmap shared space")) {
 323             throw new RuntimeException("Unable to unmap shared space");
 324         }
 325 
 326         // Special case -- sometimes Xshare:on fails because it failed to map
 327         // at given address. This behavior is platform-specific, machine config-specific
 328         // and can be random (see ASLR).
 329         if (isUnableToMap(output)) {
 330             throw new SkippedException(UnableToMapMsg);
 331         }
 332 
 333         if (e != null) {
 334             throw e;
 335         }
 336     }
 337 
 338     public static void checkCommonExecExceptions(OutputAnalyzer output) throws Exception {
 339         checkCommonExecExceptions(output, null);
 340     }
 341 
 342 
 343     // Check the output for indication that mapping of the archive failed.
 344     // Performance note: this check seems to be rather costly - searching the entire
 345     // output stream of a child process for multiple strings. However, it is necessary
 346     // to detect this condition, a failure to map an archive, since this is not a real
 347     // failure of the test or VM operation, and results in a test being "skipped".
 348     // Suggestions to improve:
 349     // 1. VM can designate a special exit code for such condition.
 350     // 2. VM can print a single distinct string indicating failure to map an archive,
 351     //    instead of utilizing multiple messages.
 352     // These are suggestions to improve testibility of the VM. However, implementing them
 353     // could also improve usability in the field.
 354     public static boolean isUnableToMap(OutputAnalyzer output) {
 355         String outStr = output.getOutput();
 356         if ((output.getExitValue() == 1) && (
 357             outStr.contains("Unable to reserve shared space at required address") ||
 358             outStr.contains("Unable to map ReadOnly shared space at required address") ||
 359             outStr.contains("Unable to map ReadWrite shared space at required address") ||
 360             outStr.contains("Unable to map MiscData shared space at required address") ||
 361             outStr.contains("Unable to map MiscCode shared space at required address") ||
 362             outStr.contains("Unable to map OptionalData shared space at required address") ||
 363             outStr.contains("Could not allocate metaspace at a compatible address") ||
 364             outStr.contains("UseSharedSpaces: Unable to allocate region, range is not within java heap") ||
 365             outStr.contains("DynamicDumpSharedSpaces is unsupported when base CDS archive is not loaded") ))
 366         {
 367             return true;
 368         }
 369 
 370         return false;
 371     }
 372 
 373     public static void checkMappingFailure(OutputAnalyzer out) throws SkippedException {
 374         if (isUnableToMap(out)) {
 375             throw new SkippedException(UnableToMapMsg);
 376         }
 377     }
 378 
 379     public static Result run(String... cliPrefix) throws Exception {
 380         CDSOptions opts = new CDSOptions();
 381         opts.setArchiveName(getDefaultArchiveName());
 382         opts.addPrefix(cliPrefix);
 383         return new Result(opts, runWithArchive(opts));
 384     }
 385 
 386     public static Result run(CDSOptions opts) throws Exception {
 387         return new Result(opts, runWithArchive(opts));
 388     }
 389 
 390     // Execute JVM with CDS archive, specify command line args suffix
 391     public static OutputAnalyzer runWithArchive(String... cliPrefix)
 392         throws Exception {
 393 
 394         return runWithArchive( (new CDSOptions())
 395                                .setArchiveName(getDefaultArchiveName())
 396                                .addPrefix(cliPrefix) );
 397     }
 398 
 399 
 400     // Execute JVM with CDS archive, specify CDSOptions
 401     public static OutputAnalyzer runWithArchive(CDSOptions opts)
 402         throws Exception {
 403 
 404         ArrayList<String> cmd = new ArrayList<String>();
 405 
 406         for (String p : opts.prefix) cmd.add(p);
 407 
 408         cmd.add("-Xshare:" + opts.xShareMode);
 409         cmd.add("-Dtest.timeout.factor=" + TestTimeoutFactor);
 410 
 411         if (!opts.useSystemArchive) {
 412             if (opts.archiveName == null)
 413                 opts.archiveName = getDefaultArchiveName();
 414             cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
 415         }
 416 
 417         if (opts.useVersion)
 418             cmd.add("-version");
 419 
 420         for (String s : opts.suffix) cmd.add(s);
 421 
 422         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
 423         ProcessBuilder pb = ProcessTools.createTestJvm(cmdLine);
 424         return executeAndLog(pb, "exec");
 425     }
 426 
 427 
 428     // A commonly used convenience methods to create an archive and check the results
 429     // Creates an archive and checks for errors
 430     public static OutputAnalyzer runWithArchiveAndCheck(CDSOptions opts) throws Exception {
 431         return checkExec(runWithArchive(opts));
 432     }
 433 
 434 
 435     public static OutputAnalyzer runWithArchiveAndCheck(String... cliPrefix) throws Exception {
 436         return checkExec(runWithArchive(cliPrefix));
 437     }
 438 
 439 
 440     public static OutputAnalyzer checkExec(OutputAnalyzer output,
 441                                      String... extraMatches) throws Exception {
 442         CDSOptions opts = new CDSOptions();
 443         return checkExec(output, opts, extraMatches);
 444     }
 445 
 446 
 447     // check result of 'exec' operation, that is when JVM is run using the archive
 448     public static OutputAnalyzer checkExec(OutputAnalyzer output, CDSOptions opts,
 449                                      String... extraMatches) throws Exception {
 450         try {
 451             if ("on".equals(opts.xShareMode)) {
 452                 output.shouldContain("sharing");
 453             }
 454             output.shouldHaveExitValue(0);
 455         } catch (RuntimeException e) {
 456             checkCommonExecExceptions(output, e);
 457             return output;
 458         }
 459 
 460         checkMatches(output, extraMatches);
 461         return output;
 462     }
 463 
 464 
 465     public static OutputAnalyzer checkExecExpectError(OutputAnalyzer output,
 466                                              int expectedExitValue,
 467                                              String... extraMatches) throws Exception {
 468         if (isUnableToMap(output)) {
 469             throw new SkippedException(UnableToMapMsg);
 470         }
 471 
 472         output.shouldHaveExitValue(expectedExitValue);
 473         checkMatches(output, extraMatches);
 474         return output;
 475     }
 476 
 477     public static OutputAnalyzer checkMatches(OutputAnalyzer output,
 478                                               String... matches) throws Exception {
 479         for (String match : matches) {
 480             output.shouldContain(match);
 481         }
 482         return output;
 483     }
 484 
 485 
 486     // get the file object for the test artifact
 487     public static File getTestArtifact(String name, boolean checkExistence) {
 488         File dir = new File(System.getProperty("test.classes", "."));
 489         File file = new File(dir, name);
 490 
 491         if (checkExistence && !file.exists()) {
 492             throw new RuntimeException("Cannot find " + file.getPath());
 493         }
 494 
 495         return file;
 496     }
 497 
 498 
 499     // create file containing the specified class list
 500     public static File makeClassList(String classes[])
 501         throws Exception {
 502         return makeClassList(getTestName() + "-", classes);
 503     }
 504 
 505     // create file containing the specified class list
 506     public static File makeClassList(String testCaseName, String classes[])
 507         throws Exception {
 508 
 509         File classList = getTestArtifact(testCaseName + "test.classlist", false);
 510         FileOutputStream fos = new FileOutputStream(classList);
 511         PrintStream ps = new PrintStream(fos);
 512 
 513         addToClassList(ps, classes);
 514 
 515         ps.close();
 516         fos.close();
 517 
 518         return classList;
 519     }
 520 
 521 
 522     public static void addToClassList(PrintStream ps, String classes[])
 523         throws IOException
 524     {
 525         if (classes != null) {
 526             for (String s : classes) {
 527                 ps.println(s);
 528             }
 529         }
 530     }
 531 
 532 
 533     // Optimization for getting a test name.
 534     // Test name does not change during execution of the test,
 535     // but getTestName() uses stack walking hence it is expensive.
 536     // Therefore cache it and reuse it.
 537     private static String testName;
 538     public static String getTestName() {
 539         if (testName == null) {
 540             testName = Utils.getTestName();
 541         }
 542         return testName;
 543     }
 544 
 545     private static final SimpleDateFormat timeStampFormat =
 546         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
 547 
 548     private static String defaultArchiveName;
 549 
 550     // Call this method to start new archive with new unique name
 551     public static void startNewArchiveName() {
 552         defaultArchiveName = getTestName() +
 553             timeStampFormat.format(new Date()) + ".jsa";
 554     }
 555 
 556     public static String getDefaultArchiveName() {
 557         return defaultArchiveName;
 558     }
 559 
 560 
 561     // ===================== FILE ACCESS convenience methods
 562     public static File getOutputFile(String name) {
 563         File dir = new File(System.getProperty("test.classes", "."));
 564         return new File(dir, getTestName() + "-" + name);
 565     }
 566 
 567 
 568     public static File getOutputSourceFile(String name) {
 569         File dir = new File(System.getProperty("test.classes", "."));
 570         return new File(dir, name);
 571     }
 572 
 573 
 574     public static File getSourceFile(String name) {
 575         File dir = new File(System.getProperty("test.src", "."));
 576         return new File(dir, name);
 577     }
 578 
 579 
 580     // ============================= Logging
 581     public static OutputAnalyzer executeAndLog(ProcessBuilder pb, String logName) throws Exception {
 582         long started = System.currentTimeMillis();
 583         OutputAnalyzer output = new OutputAnalyzer(pb.start());
 584         String outputFileNamePrefix =
 585             getTestName() + "-" + String.format("%04d", getNextLogCounter()) + "-" + logName;
 586 
 587         writeFile(getOutputFile(outputFileNamePrefix + ".stdout"), output.getStdout());
 588         writeFile(getOutputFile(outputFileNamePrefix + ".stderr"), output.getStderr());
 589         System.out.println("[ELAPSED: " + (System.currentTimeMillis() - started) + " ms]");
 590         System.out.println("[logging stdout to " + outputFileNamePrefix + ".stdout]");
 591         System.out.println("[logging stderr to " + outputFileNamePrefix + ".stderr]");
 592         System.out.println("[STDERR]\n" + output.getStderr());
 593 
 594         if (copyChildStdoutToMainStdout)
 595             System.out.println("[STDOUT]\n" + output.getStdout());
 596 
 597         return output;
 598     }
 599 
 600 
 601     private static void writeFile(File file, String content) throws Exception {
 602         FileOutputStream fos = new FileOutputStream(file);
 603         PrintStream ps = new PrintStream(fos);
 604         ps.print(content);
 605         ps.close();
 606         fos.close();
 607     }
 608 
 609     // Format a line that defines an extra symbol in the file specify by -XX:SharedArchiveConfigFile=<file>
 610     public static String formatArchiveConfigSymbol(String symbol) {
 611         int refCount = -1; // This is always -1 in the current HotSpot implementation.
 612         if (isAsciiPrintable(symbol)) {
 613             return symbol.length() + " " + refCount + ": " + symbol;
 614         } else {
 615             StringBuilder sb = new StringBuilder();
 616             int utf8_length = escapeArchiveConfigString(sb, symbol);
 617             return utf8_length + " " + refCount + ": " + sb.toString();
 618         }
 619     }
 620 
 621     // This method generates the same format as HashtableTextDump::put_utf8() in HotSpot,
 622     // to be used by -XX:SharedArchiveConfigFile=<file>.
 623     private static int escapeArchiveConfigString(StringBuilder sb, String s) {
 624         byte arr[];
 625         try {
 626             arr = s.getBytes("UTF8");
 627         } catch (java.io.UnsupportedEncodingException e) {
 628             throw new RuntimeException("Unexpected", e);
 629         }
 630         for (int i = 0; i < arr.length; i++) {
 631             char ch = (char)(arr[i] & 0xff);
 632             if (isAsciiPrintable(ch)) {
 633                 sb.append(ch);
 634             } else if (ch == '\t') {
 635                 sb.append("\\t");
 636             } else if (ch == '\r') {
 637                 sb.append("\\r");
 638             } else if (ch == '\n') {
 639                 sb.append("\\n");
 640             } else if (ch == '\\') {
 641                 sb.append("\\\\");
 642             } else {
 643                 String hex = Integer.toHexString(ch);
 644                 if (ch < 16) {
 645                     sb.append("\\x0");
 646                 } else {
 647                     sb.append("\\x");
 648                 }
 649                 sb.append(hex);
 650             }
 651         }
 652 
 653         return arr.length;
 654     }
 655 
 656     private static boolean isAsciiPrintable(String s) {
 657         for (int i = 0; i < s.length(); i++) {
 658             if (!isAsciiPrintable(s.charAt(i))) {
 659                 return false;
 660             }
 661         }
 662         return true;
 663     }
 664 
 665     private static boolean isAsciiPrintable(char ch) {
 666         return ch >= 32 && ch < 127;
 667     }
 668 }