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