001package daikon;
002
003import daikon.chicory.StreamRedirectThread;
004import daikon.plumelib.bcelutil.BcelUtil;
005import daikon.plumelib.bcelutil.SimpleLog;
006import daikon.plumelib.options.Option;
007import daikon.plumelib.options.Options;
008import java.io.File;
009import java.util.ArrayList;
010import java.util.Arrays;
011import java.util.List;
012import java.util.jar.JarEntry;
013import java.util.jar.JarFile;
014import java.util.regex.Pattern;
015import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
016import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
017import org.checkerframework.checker.nullness.qual.Nullable;
018import org.checkerframework.checker.nullness.qual.RequiresNonNull;
019
020/**
021 * This is the main class for DynComp. It uses the -javaagent switch to Java (which allows classes
022 * to be instrumented as they are loaded). This class parses the command line arguments and starts
023 * Java with the -javaagent switch on the target program. Code based largely on daikon.Chicory.
024 */
025public class DynComp {
026
027  /** Display usage information. */
028  @Option("-h Display usage information")
029  public static boolean help = false;
030
031  /** Print information about the classes being transformed. */
032  @Option("-v Print progress information")
033  public static boolean verbose = false;
034
035  /**
036   * Dump the instrumented classes to disk, for diagnostic purposes. The directory is specified by
037   * {@code --debug-dir} (default {@code debug}).
038   */
039  @Option("Dump the instrumented classes to disk")
040  public static boolean dump = false;
041
042  /** Output debugging information. */
043  @Option("-d Output debugging information (implies --dump)")
044  public static boolean debug = false;
045
046  /** The directory in which to dump instrumented class files. */
047  @Option("Directory in which to create debug files")
048  public static File debug_dir = new File("debug");
049
050  /** The directory in which to create output files (i.e., Daikon input files). */
051  @Option("Directory in which to create output files")
052  public static File output_dir = new File(".");
053
054  /** Output filename for .decls file suitable for input to Daikon. */
055  @Option("-f Output filename for Daikon decl file")
056  public static @MonotonicNonNull String decl_file = null;
057
058  /** Output filename for a more easily human-readable file summarizing comparability sets. */
059  @Option("Output file for comparability sets")
060  // If null, do no output
061  public static @MonotonicNonNull File comparability_file = null;
062
063  /** If specified, write a human-readable file showing some of the interactions that occurred. */
064  @Option("Trace output file")
065  // Null if shouldn't do output
066  public static @MonotonicNonNull File trace_file = null;
067
068  /**
069   * Controls size of the stack displayed in tracing the interactions that occurred. Used in {@code
070   * daikon.dcomp.TagEntry}.
071   */
072  @Option("Depth of call hierarchy for line tracing")
073  public static int trace_line_depth = 1;
074
075  /** Causes DynComp to abridge the variable names printed. */
076  @Option("Display abridged variable names")
077  public static boolean abridged_vars = false;
078
079  /** Only emit program points that match regex. */
080  @Option("Only process program points matching the regex")
081  public static List<Pattern> ppt_select_pattern = new ArrayList<>();
082
083  /** Suppress program points that match regex. */
084  @Option("Ignore program points matching the regex")
085  public static List<Pattern> ppt_omit_pattern = new ArrayList<>();
086
087  /** Specifies the location of the instrumented JDK. */
088  @Option("jar file containing an instrumented JDK")
089  public static @Nullable File rt_file = null;
090
091  /** Causes DynComp to traverse exactly those fields visible from a given program point. */
092  @Option("Use standard visibility")
093  public static boolean std_visibility = false;
094
095  /** Depth to which to examine structure components. */
096  @Option("Variable nesting depth")
097  public static int nesting_depth = 2;
098
099  /**
100   * Path to Java agent .jar file that performs the transformation. The "main" procedure is {@code
101   * Premain.premain()}.
102   *
103   * @see daikon.dcomp.Premain#premain
104   */
105  // Set by start_target()
106  @Option("Path to the DynComp agent jar file (usually dcomp_premain.jar)")
107  public static @Nullable File premain = null;
108
109  /** Holds the path to "daikon.jar" or to "daikon/java:daikon/java/lib/*". */
110  // Set by start_target()
111  public static String daikonPath = "";
112
113  /** The current class path. */
114  static @MonotonicNonNull String cp = null;
115
116  /** Contains the expansion of java/lib/* if it is on the classpath. */
117  static @Nullable String java_lib_classpath = null;
118
119  /** The contents of DAIKONDIR environment setting. */
120  static @Nullable String daikon_dir = null;
121
122  // The following are internal debugging options primarily for use by the DynComp maintainers.
123  // They are not documented in the Daikon User Manual.
124
125  /** Print detailed information on which classes are transformed. */
126  @Option("Print detailed information on which classes are transformed")
127  public static boolean debug_transform = false;
128
129  /** Print detailed information on variables being observed. */
130  @Option("Print detailed information on variables being observed")
131  public static boolean debug_decl_print = false;
132
133  // Note that this is derived from the rt_file option.  There is no command-line argument that
134  // corresponds to this variable.
135  /** Do not use the instrumented JDK. */
136  public static boolean no_jdk = false;
137
138  /** starting time (msecs) */
139  public static long start = System.currentTimeMillis();
140
141  /** Log file if debug is enabled. */
142  private static final SimpleLog basic = new SimpleLog(false);
143
144  /** Synopsis for the DynComp command line. */
145  public static final String synopsis = "daikon.DynComp [options] target [target-args]";
146
147  /**
148   * Entry point of DynComp.
149   *
150   * @param args see usage for argument descriptions
151   */
152  public static void main(String[] args) {
153
154    // Parse our arguments
155    Options options = new Options(synopsis, DynComp.class);
156    options.setParseAfterArg(false);
157    String[] targetArgs = options.parse(true, args);
158    check_args(options, targetArgs);
159
160    // Turn on basic logging if debug was selected
161    basic.enabled = debug;
162    basic.log("targetArgs = %s%n", Arrays.toString(targetArgs));
163
164    // Start the target.  Pass the same options to the premain as
165    // were passed here.
166
167    DynComp dcomp = new DynComp();
168    dcomp.start_target(options.getOptionsString(), targetArgs);
169  }
170
171  /**
172   * Check the command-line arguments for legality. Prints a message and exits if there was an
173   * error.
174   *
175   * @param options set of legal options to DynComp
176   * @param targetArgs arguments being passed to the target program
177   */
178  public static void check_args(Options options, String[] targetArgs) {
179    if (help) {
180      options.printUsage();
181      System.exit(1);
182    }
183    if (nesting_depth < 0) {
184      System.out.printf("nesting depth (%d) must not be negative%n", nesting_depth);
185      options.printUsage();
186      System.exit(1);
187    }
188    if (targetArgs.length == 0) {
189      System.out.println("target program must be specified");
190      options.printUsage();
191      System.exit(1);
192    }
193    if (rt_file != null && rt_file.getName().equalsIgnoreCase("NONE")) {
194      no_jdk = true;
195      rt_file = null;
196    }
197    if (!no_jdk && rt_file != null && !rt_file.exists()) {
198      // if --rt-file was given, but doesn't exist
199      System.out.printf("rt-file %s does not exist%n", rt_file);
200      options.printUsage();
201      System.exit(1);
202    }
203  }
204
205  /**
206   * Starts the target program with the Java agent set up to do the transforms. All Java agent
207   * arguments are passed to it. The current classpath is passed to the new JVM.
208   *
209   * @param premain_args the Java agent argument list
210   * @param targetArgs the test program name and its argument list
211   */
212  /*TO DO: @EnsuresNonNull("premain")*/
213  @EnsuresNonNull("cp")
214  void start_target(String premain_args, String[] targetArgs) {
215
216    // Default the decls file name to <target-program-name>.decls-DynComp
217    if (decl_file == null) {
218      String target_class = targetArgs[0].replaceFirst(".*[/.]", "");
219      decl_file = String.format("%s.decls-DynComp", target_class);
220      premain_args += " --decl-file=" + decl_file;
221    }
222
223    // Get the current classpath
224    cp = System.getProperty("java.class.path");
225    basic.log("classpath = '%s'%n", cp);
226    if (cp == null) {
227      cp = ".";
228    }
229
230    // Get location of DAIKONDIR, may be null.
231    daikon_dir = System.getenv("DAIKONDIR");
232
233    // The separator for items in the class path.
234    basic.log("File.pathSeparator = %s%n", File.pathSeparator);
235
236    // Look for location of dcomp_premain.jar
237    if (premain == null) {
238      premain = locateFile("dcomp_premain.jar");
239    }
240    // If we didn't find a premain it's a fatal error.
241    if (premain == null) {
242      System.err.printf("Can't find dcomp_premain.jar on the classpath");
243      if (daikon_dir == null) {
244        System.err.printf(" and $DAIKONDIR is not set.%n");
245      } else {
246        System.err.printf(" or in %s/java .%n", daikon_dir);
247      }
248      System.err.printf("It should be found in the directory where Daikon was installed.%n");
249      System.err.printf("Use the --premain switch to specify its location,%n");
250      System.err.printf("or change your classpath to include it.%n");
251      System.exit(1);
252    }
253
254    // Are we using the instrumented JDK?
255    if (!no_jdk) {
256      // Yes we are - We need to locate dcomp_rt.jar and add Daikon to the boot classpath.
257      // Look for location of dcomp_rt.jar
258      if (rt_file == null) {
259        rt_file = locateFile("dcomp_rt.jar");
260      }
261      // If we didn't find a rt-file it's a fatal error.
262      if (rt_file == null) {
263        System.err.printf("Can't find dcomp_rt.jar on the classpath");
264        if (daikon_dir == null) {
265          System.err.printf(" and $DAIKONDIR is not set.%n");
266        } else {
267          System.err.printf(" or in %s/java .%n", daikon_dir);
268        }
269        System.err.printf("Probably you forgot to build it.%n");
270        System.err.printf(
271            "See the Daikon manual, section \"Instrumenting the JDK with DynComp\" for help.%n");
272        System.exit(1);
273      }
274      // Add the location of Daikon to the boot classpath.  For each element of the classpath, if it
275      // is part of Daikon, append it to the boot classpath.
276      for (String path : cp.split(File.pathSeparator)) {
277        if (isDaikonOnPath(path)) {
278          daikonPath = daikonPath + File.pathSeparator + path;
279        }
280      }
281      basic.log("daikonPath = '%s'%n", daikonPath);
282    }
283
284    // Build the command line to execute the target with the javaagent.
285    List<String> cmdlist = new ArrayList<>();
286    cmdlist.add("java");
287    // cmdlist.add ("-verbose:class");
288
289    cmdlist.add("-cp");
290    cmdlist.add(cp);
291    cmdlist.add("-ea");
292    cmdlist.add("-esa");
293    // Get max memory given DynComp and pass on to dcomp_premain rounded up to nearest gigabyte.
294    cmdlist.add(
295        "-Xmx" + (int) Math.ceil(java.lang.Runtime.getRuntime().maxMemory() / 1073741824.0) + "G");
296
297    if (BcelUtil.javaVersion <= 8) {
298      if (!no_jdk) {
299        // prepend to rather than replace boot classpath
300        // If daikonPath is nonempty, it starts with a pathSeparator.
301        cmdlist.add("-Xbootclasspath/p:" + rt_file + daikonPath);
302      }
303    } else {
304      // allow DCRuntime to make reflective access to java.land.Object.clone() without a warning
305      cmdlist.add("--add-opens");
306      cmdlist.add("java.base/java.lang=ALL-UNNAMED");
307      if (!no_jdk) {
308        // If we are processing JDK classes, then we need our code on the boot classpath as well.
309        // Otherwise, references to DCRuntime from the JDK would fail.
310        // If daikonPath is nonempty, it starts with a pathSeparator.
311        cmdlist.add("-Xbootclasspath/a:" + rt_file + daikonPath);
312        // allow java.base to access daikon.jar (for instrumentation runtime)
313        cmdlist.add("--add-reads");
314        cmdlist.add("java.base=ALL-UNNAMED");
315        // allow DCRuntime to make reflective access to sun.util.locale (equals_dcomp_instrumented)
316        cmdlist.add("--add-exports");
317        cmdlist.add("java.base/sun.util.locale=ALL-UNNAMED");
318        if (BcelUtil.javaVersion >= 24) {
319          cmdlist.add("--add-opens");
320          cmdlist.add("java.base/sun.util.resources=ALL-UNNAMED");
321        }
322        // replace default java.base with our instrumented version
323        cmdlist.add("--patch-module");
324        cmdlist.add("java.base=" + rt_file);
325      }
326    }
327
328    cmdlist.add(String.format("-javaagent:%s=%s", premain, premain_args));
329
330    // A `for` loop is needed because targetArgs is an array and cmdlist is a list.
331    for (String target_arg : targetArgs) {
332      cmdlist.add(target_arg);
333    }
334    if (verbose) {
335      System.out.printf("%nExecuting target program: %s%n", argsToString(cmdlist));
336    }
337    String[] cmdline = cmdlist.toArray(new String[0]);
338
339    // Execute the command, sending all output to our streams.
340    java.lang.Runtime rt = java.lang.Runtime.getRuntime();
341    Process dcomp_proc;
342    try {
343      dcomp_proc = rt.exec(cmdline);
344    } catch (Exception e) {
345      System.out.printf("Exception '%s' while executing: %s%n", e, cmdline);
346      System.exit(1);
347      throw new Error("Unreachable control flow");
348    }
349
350    int targetResult = redirect_wait(dcomp_proc);
351    if (targetResult != 0) {
352      System.out.printf("Warning: Target exited with %d status.%n", targetResult);
353    }
354    System.exit(targetResult);
355  }
356
357  /** Wait for stream redirect threads to complete. */
358  public int redirect_wait(Process p) {
359
360    // Create the redirect threads and start them.
361    StreamRedirectThread in_thread =
362        new StreamRedirectThread("stdin", System.in, p.getOutputStream(), false);
363    StreamRedirectThread err_thread =
364        new StreamRedirectThread("stderr", p.getErrorStream(), System.err, true);
365    StreamRedirectThread out_thread =
366        new StreamRedirectThread("stdout", p.getInputStream(), System.out, true);
367
368    in_thread.start();
369    err_thread.start();
370    out_thread.start();
371
372    // Wait for the process to terminate and return the results
373    int result = -1;
374    while (true) {
375      try {
376        result = p.waitFor();
377        break;
378      } catch (InterruptedException e) {
379        System.out.printf("unexpected interrupt %s while waiting for target to finish", e);
380      }
381    }
382
383    // Make sure all output is forwarded before we finish
384    try {
385      err_thread.join();
386      out_thread.join();
387    } catch (InterruptedException e) {
388      System.out.printf("unexpected interrupt %s while waiting for threads to join", e);
389    }
390
391    return result;
392  }
393
394  /**
395   * Returns elapsed time since the start of the program.
396   *
397   * @return elapsed time since the start of the program
398   */
399  public static String elapsed() {
400    return "[" + elapsedMsecs() + " msec]";
401  }
402
403  /**
404   * Returns number of milliseconds since the start of the program.
405   *
406   * @return number of milliseconds since the start of the program
407   */
408  public static long elapsedMsecs() {
409    return System.currentTimeMillis() - start;
410  }
411
412  /**
413   * Convert a list of arguments into a command-line string. Only used for debugging output.
414   *
415   * @param args the list of arguments
416   * @return argument string
417   */
418  public String argsToString(List<String> args) {
419    String str = "";
420    for (String arg : args) {
421      if (arg.indexOf(" ") != -1) {
422        arg = "'" + arg + "'";
423      }
424      str += arg + " ";
425    }
426    return str.trim();
427  }
428
429  /**
430   * Returns true if Daikon or a Daikon library jar file is on the path argument. There are three
431   * cases:
432   *
433   * <ul>
434   *   <li>a jar file that contains "DynComp.class"
435   *   <li>a path that ends in "java/lib/<em>something</em>.jar"
436   *   <li>a path that leads to "daikon/DynComp.class"
437   * </ul>
438   *
439   * @param path classpath element to inspect for Daikon
440   * @return true if found
441   */
442  boolean isDaikonOnPath(String path) {
443    if (path.endsWith(".jar")) {
444      // path ends in ".jar".
445      try (JarFile jar = new JarFile(path)) {
446        JarEntry entry = jar.getJarEntry("daikon/DynComp.class");
447        if (entry != null) {
448          return true;
449        }
450      } catch (Exception e) {
451        // do nothing, try next case
452      }
453      // check to see if path is .../java/lib/...
454      String pathElements[] = path.split(Pattern.quote(File.separator));
455      int pathLength = pathElements.length;
456      if (pathLength == 0) {
457        // Can never happen? Fatal error if it does.
458        System.err.printf("classpath appears to be empty.%n");
459        System.exit(1);
460      }
461      if (pathLength > 2) {
462        if (pathElements[pathLength - 2].equals("lib")
463            && pathElements[pathLength - 3].equals("java")) {
464          File javaLibJarFile = new File(path);
465          if (javaLibJarFile.canRead()) {
466            return true;
467          }
468        }
469      }
470    } else {
471      // path does not end in ".jar"
472      File dyncompClassFile =
473          new File(path + File.separator + "daikon" + File.separator + "DynComp.class");
474      if (dyncompClassFile.canRead()) {
475        return true;
476      }
477    }
478    return false;
479  }
480
481  /**
482   * Search for a file on the current classpath, then in ${DAIKONDIR}/java. Returns null if not
483   * found.
484   *
485   * @param fileName the relative name of a file to look for
486   * @return path to fileName or null
487   */
488  @RequiresNonNull("cp")
489  public @Nullable File locateFile(String fileName) {
490    File poss_file = findOnClasspath(fileName);
491    if (poss_file != null) {
492      return poss_file;
493    }
494
495    // If not on the classpath look in ${DAIKONDIR}/java.
496    if (daikon_dir != null) {
497      poss_file = new File(new File(daikon_dir, "java"), fileName);
498      if (poss_file.canRead()) {
499        return poss_file;
500      }
501    }
502    // Couldn't find fileName
503    return null;
504  }
505
506  /**
507   * Search for a file on the current classpath. Returns null if not found.
508   *
509   * @param fileName the relative name of a file to look for
510   * @return path to fileName or null
511   */
512  @RequiresNonNull("cp")
513  public @Nullable File findOnClasspath(String fileName) {
514    for (String path : cp.split(File.pathSeparator)) {
515      File poss_file;
516      if (path.endsWith(fileName)) {
517        int start = path.indexOf(fileName);
518        // There are three cases:
519        //   path == fileName (start == 0)
520        //   path == <something>/fileName (charAt(start-1) == separator)
521        //   path == <something>/<something>fileName (otherwise)
522        // The first two are good, the last is not what we are looking for.
523        if (start == 0 || path.charAt(start - 1) == File.separatorChar) {
524          poss_file = new File(path);
525        } else {
526          poss_file = new File(path, fileName);
527        }
528      } else {
529        poss_file = new File(path, fileName);
530      }
531      if (poss_file.canRead()) {
532        return poss_file;
533      }
534    }
535    return null;
536  }
537}