1 /*
   2  * Copyright (c) 2014, 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  */
  24 
  25 import jdk.test.lib.Utils;
  26 import jdk.test.lib.BuildHelper;
  27 import jdk.test.lib.JDKToolFinder;
  28 import jdk.test.lib.Platform;
  29 import jdk.test.lib.cds.CDSOptions;
  30 import jdk.test.lib.cds.CDSTestUtils;
  31 import jdk.test.lib.cds.CDSTestUtils.Result;
  32 import jdk.test.lib.process.ProcessTools;
  33 import jdk.test.lib.process.OutputAnalyzer;
  34 import java.io.File;
  35 import java.io.FileInputStream;
  36 import java.io.FileOutputStream;
  37 import java.io.InputStream;
  38 import java.net.URI;
  39 import java.nio.file.DirectoryStream;
  40 import java.nio.file.Files;
  41 import java.nio.file.FileSystem;
  42 import java.nio.file.FileSystems;
  43 import java.nio.file.Path;
  44 import java.text.SimpleDateFormat;
  45 import java.util.Arrays;
  46 import java.util.ArrayList;
  47 import java.util.Date;
  48 import java.util.Enumeration;
  49 import java.util.regex.Matcher;
  50 import java.util.regex.Pattern;
  51 import java.util.zip.ZipEntry;
  52 import java.util.zip.ZipFile;
  53 import java.util.zip.ZipOutputStream;
  54 import jtreg.SkippedException;
  55 import cdsutils.DynamicDumpHelper;
  56 
  57 
  58 /**
  59  * This is a test utility class for common AppCDS test functionality.
  60  *
  61  * Various methods use (String ...) for passing VM options. Note that the order
  62  * of the VM options are important in certain cases. Many methods take arguments like
  63  *
  64  *    (String prefix[], String suffix[], String... opts)
  65  *
  66  * Note that the order of the VM options is:
  67  *
  68  *    prefix + opts + suffix
  69  */
  70 public class TestCommon extends CDSTestUtils {
  71     private static final String JSA_FILE_PREFIX = System.getProperty("user.dir") +
  72         File.separator;
  73 
  74     private static final SimpleDateFormat timeStampFormat =
  75         new SimpleDateFormat("HH'h'mm'm'ss's'SSS");
  76 
  77     private static final String timeoutFactor =
  78         System.getProperty("test.timeout.factor", "1.0");
  79 
  80     private static String currentArchiveName;
  81 
  82     // Call this method to start new archive with new unique name
  83     public static void startNewArchiveName() {
  84         deletePriorArchives();
  85         currentArchiveName = getNewArchiveName();
  86     }
  87 
  88     // Call this method to get current archive name
  89     public static String getCurrentArchiveName() {
  90         return currentArchiveName;
  91     }
  92 
  93     public static void setCurrentArchiveName(String archiveName) {
  94         currentArchiveName = archiveName;
  95     }
  96 
  97     public static String getNewArchiveName() {
  98         return getNewArchiveName(null);
  99     }
 100 
 101     public static String getNewArchiveName(String stem) {
 102         if (stem == null) {
 103             stem = "appcds";
 104         }
 105         return JSA_FILE_PREFIX + stem + "-" +
 106             timeStampFormat.format(new Date()) + ".jsa";
 107     }
 108 
 109     // Attempt to clean old archives to preserve space
 110     // Archives are large artifacts (20Mb or more), and much larger than
 111     // most other artifacts created in jtreg testing.
 112     // Therefore it is a good idea to clean the old archives when they are not needed.
 113     // In most cases the deletion attempt will succeed; on rare occasion the
 114     // delete operation will fail since the system or VM process still holds a handle
 115     // to the file; in such cases the File.delete() operation will silently fail, w/o
 116     // throwing an exception, thus allowing testing to continue.
 117     public static void deletePriorArchives() {
 118         File dir = new File(System.getProperty("user.dir"));
 119         String files[] = dir.list();
 120         for (String name : files) {
 121             if (name.startsWith("appcds-") && name.endsWith(".jsa")) {
 122                 if (!(new File(dir, name)).delete())
 123                     System.out.println("deletePriorArchives(): delete failed for file " + name);
 124             }
 125         }
 126     }
 127 
 128     // Create AppCDS archive using most common args - convenience method
 129     // Legacy name preserved for compatibility
 130     public static OutputAnalyzer dump(String appJar, String classList[],
 131                                                String... suffix) throws Exception {
 132         return createArchive(appJar, classList, suffix);
 133     }
 134 
 135     public static OutputAnalyzer dump(String appJarDir, String appJar, String classList[],
 136                                                String... suffix) throws Exception {
 137         return createArchive(appJarDir, appJar, classList, suffix);
 138     }
 139 
 140     // Create AppCDS archive using most common args - convenience method
 141     public static OutputAnalyzer createArchive(String appJar, String classList[],
 142                                                String... suffix) throws Exception {
 143         AppCDSOptions opts = (new AppCDSOptions()).setAppJar(appJar);
 144         opts.setClassList(classList);
 145         opts.addSuffix(suffix);
 146         return createArchive(opts);
 147     }
 148 
 149     public static OutputAnalyzer createArchive(String appJarDir, String appJar, String classList[],
 150                                                String... suffix) throws Exception {
 151         AppCDSOptions opts = (new AppCDSOptions()).setAppJar(appJar);
 152         opts.setAppJarDir(appJarDir);
 153         opts.setClassList(classList);
 154         opts.addSuffix(suffix);
 155         return createArchive(opts);
 156     }
 157 
 158     // Simulate -Xshare:dump with -XX:ArchiveClassesAtExit. See comments around patchJarForDynamicDump()
 159     private static final Class tmp = DynamicDumpHelper.class;
 160 
 161     // Create AppCDS archive using appcds options
 162     public static OutputAnalyzer createArchive(AppCDSOptions opts)
 163         throws Exception {
 164         ArrayList<String> cmd = new ArrayList<String>();
 165         startNewArchiveName();
 166 
 167         for (String p : opts.prefix) cmd.add(p);
 168 
 169         if (opts.appJar != null) {
 170             cmd.add("-cp");
 171             cmd.add(opts.appJar);
 172             File jf = new File(opts.appJar);
 173             if (DYNAMIC_DUMP && !jf.isDirectory()) {
 174                 patchJarForDynamicDump(opts.appJar);
 175             }
 176         } else {
 177             cmd.add("-Djava.class.path=");
 178         }
 179 
 180         if (opts.archiveName == null) {
 181             opts.archiveName = getCurrentArchiveName();
 182         }
 183 
 184         if (DYNAMIC_DUMP) {
 185             cmd.add("-Xshare:on");
 186             cmd.add("-XX:ArchiveClassesAtExit=" + opts.archiveName);
 187 
 188             cmd.add("-Xlog:cds");
 189             cmd.add("-Xlog:cds+dynamic");
 190             boolean mainModuleSpecified = false;
 191             boolean patchModuleSpecified = false;
 192             for (String s : opts.suffix) {
 193                 if (s.length() == 0) {
 194                     continue;
 195                 }
 196                 if (s.equals("-m")) {
 197                     mainModuleSpecified = true;
 198                 }
 199                 if (s.startsWith("--patch-module=")) {
 200                     patchModuleSpecified = true;
 201                 }
 202                 cmd.add(s);
 203             }
 204 
 205             if (opts.appJar != null) {
 206                 // classlist is supported only when we have a Jar file to patch (to insert
 207                 // cdsutils.DynamicDumpHelper)
 208                 if (opts.classList == null) {
 209                     throw new RuntimeException("test.dynamic.dump requires classList file");
 210                 }
 211 
 212                 if (!mainModuleSpecified && !patchModuleSpecified) {
 213                     cmd.add("cdsutils.DynamicDumpHelper");
 214                     File classListFile = makeClassList(opts.classList);
 215                     cmd.add(classListFile.getPath());
 216                 }
 217             } else {
 218                 if (!mainModuleSpecified && !patchModuleSpecified) {
 219                     // If you have an empty classpath, you cannot specify a classlist!
 220                     if (opts.classList != null && opts.classList.length > 0) {
 221                         throw new RuntimeException("test.dynamic.dump not supported empty classpath with non-empty classlist");
 222                     }
 223                     cmd.add("-version");
 224                 }
 225             }
 226         } else {
 227             // static dump
 228             cmd.add("-Xshare:dump");
 229             cmd.add("-XX:SharedArchiveFile=" + opts.archiveName);
 230 
 231             if (opts.classList != null) {
 232                 File classListFile = makeClassList(opts.classList);
 233                 cmd.add("-XX:ExtraSharedClassListFile=" + classListFile.getPath());
 234             }
 235             for (String s : opts.suffix) {
 236                 cmd.add(s);
 237             }
 238         }
 239 
 240         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
 241         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine);
 242         if (opts.appJarDir != null) {
 243             pb.directory(new File(opts.appJarDir));
 244         }
 245         return executeAndLog(pb, "dump");
 246     }
 247 
 248     // This allows you to run the AppCDS tests with JFR enabled at runtime (though not at
 249     // dump time, as that's uncommon for typical AppCDS users).
 250     //
 251     // To run in this special mode, add the following to your jtreg command-line
 252     //    -Dtest.cds.run.with.jfr=true
 253     //
 254     // Some AppCDS tests are not compatible with this mode. See the group
 255     // hotspot_appcds_with_jfr in ../../TEST.ROOT for details.
 256     private static final boolean RUN_WITH_JFR = Boolean.getBoolean("test.cds.run.with.jfr");
 257     // This method simulates -Xshare:dump with -XX:ArchiveClassesAtExit. This way, we
 258     // can re-use many tests (outside of the ./dynamicArchive directory) for testing
 259     // general features of JDK-8215311 (JEP 350: Dynamic CDS Archives).
 260     //
 261     // We insert the cdsutils/DynamicDumpHelper.class into the first Jar file in
 262     // the classpath. We use this class to load all the classes specified in the classlist.
 263     //
 264     // There's no need to change the run-time command-line: in this special mode, two
 265     // archives are involved. The command-line specifies only the top archive. However,
 266     // the location of the base archive is recorded in the top archive, so it can be
 267     // determined by the JVM at runtime start-up.
 268     //
 269     // To run in this special mode, specify the following in your jtreg command-line
 270     //    -Dtest.dynamic.cds.archive=true
 271     //
 272     // Note that some tests are not compatible with this special mode, including
 273     //    + Tests in ./dynamicArchive: these tests are specifically written for
 274     //      dynamic archive, and do not use TestCommon.createArchive(), which works
 275     //      together with patchJarForDynamicDump().
 276     //    + Tests related to cached objects and shared strings: dynamic dumping
 277     //      does not support these.
 278     //    + Custom loader tests: DynamicDumpHelper doesn't support the required
 279     //      classlist syntax. (FIXME).
 280     //    + Extra symbols and extra strings.
 281     // See the hotspot_appcds_dynamic in ../../TEST.ROOT for details.
 282     //
 283     // To run all tests that are compatible with this mode:
 284     //    cd test/hotspot/jtreg
 285     //    jtreg -Dtest.dynamic.cds.archive=true :hotspot_appcds_dynamic
 286     //
 287     private static void patchJarForDynamicDump(String cp) throws Exception {
 288         System.out.println("patchJarForDynamicDump: classpath = " + cp);
 289         String firstJar = cp;
 290         int n = firstJar.indexOf(File.pathSeparator);
 291         if (n > 0) {
 292             firstJar = firstJar.substring(0, n);
 293         }
 294         String classDir = System.getProperty("test.classes");
 295         String expected1 = classDir + File.separator;
 296         String expected2 = System.getProperty("user.dir") + File.separator;
 297 
 298         if (!firstJar.startsWith(expected1) && !firstJar.startsWith(expected2)) {
 299             throw new RuntimeException("FIXME: jar file not at a supported location ('"
 300                                        + expected1 + "', or '" + expected2 + "'): " + firstJar);
 301         }
 302 
 303         String replaceJar = firstJar + ".tmp";
 304         String patchClass = "cdsutils/DynamicDumpHelper.class";
 305         ZipFile zipFile = new ZipFile(firstJar);
 306         byte[] buf = new byte[1024];
 307         int len;
 308         if (zipFile.getEntry(patchClass) == null) {
 309             FileOutputStream fout = new FileOutputStream(replaceJar);
 310             final ZipOutputStream zos = new ZipOutputStream(fout);
 311 
 312             zos.putNextEntry(new ZipEntry(patchClass));
 313             InputStream is = new FileInputStream(classDir + File.separator + patchClass);
 314             while ((len = (is.read(buf))) > 0) {
 315                 zos.write(buf, 0, len);
 316             }
 317             zos.closeEntry();
 318             is.close();
 319 
 320             for (Enumeration e = zipFile.entries(); e.hasMoreElements(); ) {
 321                 ZipEntry entryIn = (ZipEntry) e.nextElement();
 322                 zos.putNextEntry(entryIn);
 323                 is = zipFile.getInputStream(entryIn);
 324                 while ((len = is.read(buf)) > 0) {
 325                     zos.write(buf, 0, len);
 326                 }
 327                 zos.closeEntry();
 328                 is.close();
 329             }
 330 
 331             zos.close();
 332             fout.close();
 333             zipFile.close();
 334 
 335             File oldFile = new File(firstJar);
 336             File newFile = new File(replaceJar);
 337             oldFile.delete();
 338             newFile.renameTo(oldFile);
 339             System.out.println("firstJar = " + firstJar + " Modified");
 340         } else {
 341             System.out.println("firstJar = " + firstJar);
 342         }
 343     }
 344 
 345     // Execute JVM using AppCDS archive with specified AppCDSOptions
 346     public static OutputAnalyzer runWithArchive(AppCDSOptions opts)
 347         throws Exception {
 348 
 349         ArrayList<String> cmd = new ArrayList<String>();
 350 
 351         for (String p : opts.prefix) cmd.add(p);
 352 
 353         cmd.add("-Xshare:" + opts.xShareMode);
 354         cmd.add("-showversion");
 355         cmd.add("-XX:SharedArchiveFile=" + getCurrentArchiveName());
 356         cmd.add("-Dtest.timeout.factor=" + timeoutFactor);
 357 
 358         if (opts.appJar != null) {
 359             cmd.add("-cp");
 360             cmd.add(opts.appJar);
 361         }
 362 
 363         for (String s : opts.suffix) cmd.add(s);
 364 
 365         if (RUN_WITH_JFR) {
 366             boolean usesJFR = false;
 367             for (String s : cmd) {
 368                 if (s.startsWith("-XX:StartFlightRecording=") || s.startsWith("-XX:FlightRecorderOptions")) {
 369                     System.out.println("JFR option might have been specified. Don't interfere: " + s);
 370                     usesJFR = true;
 371                     break;
 372                 }
 373             }
 374             if (!usesJFR) {
 375                 System.out.println("JFR option not specified. Enabling JFR ...");
 376                 cmd.add(0, "-XX:StartFlightRecording=dumponexit=true");
 377                 System.out.println(cmd);
 378             }
 379         }
 380 
 381         String[] cmdLine = cmd.toArray(new String[cmd.size()]);
 382         ProcessBuilder pb = ProcessTools.createJavaProcessBuilder(true, cmdLine);
 383         if (opts.appJarDir != null) {
 384             pb.directory(new File(opts.appJarDir));
 385         }
 386         return executeAndLog(pb, "exec");
 387     }
 388 
 389 
 390     public static OutputAnalyzer execCommon(String... suffix) throws Exception {
 391         AppCDSOptions opts = (new AppCDSOptions());
 392         opts.addSuffix(suffix);
 393         return runWithArchive(opts);
 394     }
 395 
 396     // This is the new API for running a Java process with CDS enabled.
 397     // See comments in the CDSTestUtils.Result class for how to use this method.
 398     public static Result run(String... suffix) throws Exception {
 399         AppCDSOptions opts = (new AppCDSOptions());
 400         opts.addSuffix(suffix);
 401         return new Result(opts, runWithArchive(opts));
 402     }
 403 
 404     public static Result runWithRelativePath(String jarDir, String... suffix) throws Exception {
 405         AppCDSOptions opts = (new AppCDSOptions());
 406         opts.setAppJarDir(jarDir);
 407         opts.addSuffix(suffix);
 408         return new Result(opts, runWithArchive(opts));
 409     }
 410 
 411     public static OutputAnalyzer exec(String appJar, String... suffix) throws Exception {
 412         AppCDSOptions opts = (new AppCDSOptions()).setAppJar(appJar);
 413         opts.addSuffix(suffix);
 414         return runWithArchive(opts);
 415     }
 416 
 417     public static Result runWithModules(String prefix[], String upgrademodulepath, String modulepath,
 418                                             String mid, String... testClassArgs) throws Exception {
 419         AppCDSOptions opts = makeModuleOptions(prefix, upgrademodulepath, modulepath,
 420                                                mid, testClassArgs);
 421         return new Result(opts, runWithArchive(opts));
 422     }
 423 
 424     public static OutputAnalyzer execAuto(String... suffix) throws Exception {
 425         AppCDSOptions opts = (new AppCDSOptions());
 426         opts.addSuffix(suffix).setXShareMode("auto");
 427         return runWithArchive(opts);
 428     }
 429 
 430     public static OutputAnalyzer execOff(String... suffix) throws Exception {
 431         AppCDSOptions opts = (new AppCDSOptions());
 432         opts.addSuffix(suffix).setXShareMode("off");
 433         return runWithArchive(opts);
 434     }
 435 
 436 
 437     private static AppCDSOptions makeModuleOptions(String prefix[], String upgrademodulepath, String modulepath,
 438                                             String mid, String testClassArgs[]) {
 439         AppCDSOptions opts = (new AppCDSOptions());
 440 
 441         opts.addPrefix(prefix);
 442         if (upgrademodulepath == null) {
 443             opts.addSuffix("-p", modulepath, "-m", mid);
 444         } else {
 445             opts.addSuffix("--upgrade-module-path", upgrademodulepath,
 446                            "-p", modulepath, "-m", mid);
 447         }
 448         opts.addSuffix(testClassArgs);
 449         return opts;
 450     }
 451 
 452     public static OutputAnalyzer execModule(String prefix[], String upgrademodulepath, String modulepath,
 453                                             String mid, String... testClassArgs)
 454         throws Exception {
 455         AppCDSOptions opts = makeModuleOptions(prefix, upgrademodulepath, modulepath,
 456                                                mid, testClassArgs);
 457         return runWithArchive(opts);
 458     }
 459 
 460     // A common operation: dump, then check results
 461     public static OutputAnalyzer testDump(String appJar, String classList[],
 462                                           String... suffix) throws Exception {
 463         OutputAnalyzer output = dump(appJar, classList, suffix);
 464         if (DYNAMIC_DUMP) {
 465             if (isUnableToMap(output)) {
 466                 throw new SkippedException(UnableToMapMsg);
 467             }
 468             output.shouldContain("Written dynamic archive");
 469         } else {
 470             output.shouldContain("Loading classes to share");
 471         }
 472         output.shouldHaveExitValue(0);
 473         return output;
 474     }
 475 
 476     public static OutputAnalyzer testDump(String appJarDir, String appJar, String classList[],
 477                                           String... suffix) throws Exception {
 478         OutputAnalyzer output = dump(appJarDir, appJar, classList, suffix);
 479         if (DYNAMIC_DUMP) {
 480             if (isUnableToMap(output)) {
 481                 throw new SkippedException(UnableToMapMsg);
 482             }
 483             output.shouldContain("Written dynamic archive");
 484         } else {
 485             output.shouldContain("Loading classes to share");
 486         }
 487         output.shouldHaveExitValue(0);
 488         return output;
 489     }
 490 
 491     /**
 492      * Simple test -- dump and execute appJar with the given classList in classlist.
 493      */
 494     public static OutputAnalyzer test(String appJar, String classList[], String... args)
 495         throws Exception {
 496         testDump(appJar, classList);
 497 
 498         OutputAnalyzer output = exec(appJar, args);
 499         return checkExec(output);
 500     }
 501 
 502 
 503     public static OutputAnalyzer checkExecReturn(OutputAnalyzer output, int ret,
 504                            boolean checkContain, String... matches) throws Exception {
 505         try {
 506             for (String s : matches) {
 507                 if (checkContain) {
 508                     output.shouldContain(s);
 509                 } else {
 510                     output.shouldNotContain(s);
 511                 }
 512             }
 513             output.shouldHaveExitValue(ret);
 514         } catch (Exception e) {
 515             checkCommonExecExceptions(output, e);
 516         }
 517 
 518         return output;
 519     }
 520 
 521     // Convenience concatenation utils
 522     public static String[] list(String ...args) {
 523         return args;
 524     }
 525 
 526 
 527     public static String[] list(String arg, int count) {
 528         ArrayList<String> stringList = new ArrayList<String>();
 529         for (int i = 0; i < count; i++) {
 530             stringList.add(arg);
 531         }
 532 
 533         String outputArray[] = stringList.toArray(new String[stringList.size()]);
 534         return outputArray;
 535     }
 536 
 537 
 538     public static String[] concat(String... args) {
 539         return list(args);
 540     }
 541 
 542 
 543     public static String[] concat(String prefix[], String... extra) {
 544         ArrayList<String> list = new ArrayList<String>();
 545         for (String s : prefix) {
 546             list.add(s);
 547         }
 548         for (String s : extra) {
 549             list.add(s);
 550         }
 551 
 552         return list.toArray(new String[list.size()]);
 553     }
 554 
 555     public static String[] concat(String prefix, String[] extra) {
 556         ArrayList<String> list = new ArrayList<String>();
 557         list.add(prefix);
 558         for (String s : extra) {
 559             list.add(s);
 560         }
 561 
 562         return list.toArray(new String[list.size()]);
 563     }
 564 
 565     // ===================== Concatenate paths
 566     public static String concatPaths(String... paths) {
 567         String prefix = "";
 568         String s = "";
 569         for (String p : paths) {
 570             s += prefix;
 571             s += p;
 572             prefix = File.pathSeparator;
 573         }
 574         return s;
 575     }
 576 
 577 
 578     public static String getTestJar(String jar) {
 579         File jarFile = CDSTestUtils.getTestArtifact(jar, true);
 580         if (!jarFile.isFile()) {
 581             throw new RuntimeException("Not a regular file: " + jarFile.getPath());
 582         }
 583         return jarFile.getPath();
 584     }
 585 
 586 
 587     public static String getTestDir(String d) {
 588         File dirFile = CDSTestUtils.getTestArtifact(d, true);
 589         if (!dirFile.isDirectory()) {
 590             throw new RuntimeException("Not a directory: " + dirFile.getPath());
 591         }
 592         return dirFile.getPath();
 593     }
 594 
 595     public static boolean checkOutputStrings(String outputString1,
 596                                              String outputString2,
 597                                              String split_regex) {
 598         String[] sa1 = outputString1.split(split_regex);
 599         String[] sa2 = outputString2.split(split_regex);
 600         Arrays.sort(sa1);
 601         Arrays.sort(sa2);
 602 
 603         int i = 0;
 604         for (String s : sa1) {
 605             if (!s.equals(sa2[i])) {
 606                 throw new RuntimeException(s + " is different from " + sa2[i]);
 607             }
 608             i ++;
 609         }
 610         return true;
 611     }
 612 
 613     static Pattern pattern;
 614 
 615     static void findAllClasses(ArrayList<String> list) throws Throwable {
 616         // Find all the classes in the jrt file system
 617         pattern = Pattern.compile("/modules/[a-z.]*[a-z]+/([^-]*)[.]class");
 618         FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
 619         Path base = fs.getPath("/modules/");
 620         findAllClassesAtPath(base, list);
 621     }
 622 
 623     private static void findAllClassesAtPath(Path p, ArrayList<String> list) throws Throwable {
 624         try (DirectoryStream<Path> stream = Files.newDirectoryStream(p)) {
 625             for (Path entry: stream) {
 626                 Matcher matcher = pattern.matcher(entry.toString());
 627                 if (matcher.find()) {
 628                     String className = matcher.group(1);
 629                     list.add(className);
 630                 }
 631                 try {
 632                     findAllClassesAtPath(entry, list);
 633                 } catch (Throwable t) {}
 634             }
 635         }
 636     }
 637 
 638     public static String composeRelPath(String appJar) {
 639          int idx = appJar.lastIndexOf(File.separator);
 640          String jarName = appJar.substring(idx + 1);
 641          String jarDir = appJar.substring(0, idx);
 642          String lastDir = jarDir.substring(jarDir.lastIndexOf(File.separator));
 643          String relPath = jarDir + File.separator + ".." + File.separator + lastDir;
 644          String newJar = relPath + File.separator + jarName;
 645          return newJar;
 646     }
 647 
 648 
 649     public static File createSymLink(String appJar) throws Exception {
 650          int idx = appJar.lastIndexOf(File.separator);
 651          String jarName = appJar.substring(idx + 1);
 652          String jarDir = appJar.substring(0, idx);
 653          File origJar = new File(jarDir, jarName);
 654          String linkedJarName = "linked_" + jarName;
 655          File linkedJar = null;
 656          if (!Platform.isWindows()) {
 657              linkedJar = new File(jarDir, linkedJarName);
 658              if (linkedJar.exists()) {
 659                  linkedJar.delete();
 660              }
 661              Files.createSymbolicLink(linkedJar.toPath(), origJar.toPath());
 662          }
 663          return linkedJar;
 664     }
 665 }