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    cmdlist.add("-cp");
289    cmdlist.add(cp);
290    cmdlist.add("-ea");
291    cmdlist.add("-esa");
292    // Get max memory given DynComp and pass on to dcomp_premain rounded up to nearest gigabyte.
293    cmdlist.add(
294        "-Xmx" + (int) Math.ceil(java.lang.Runtime.getRuntime().maxMemory() / 1073741824.0) + "G");
295
296    if (BcelUtil.javaVersion <= 8) {
297      if (!no_jdk) {
298        // prepend to rather than replace boot classpath
299        // If daikonPath is nonempty, it starts with a pathSeparator.
300        cmdlist.add("-Xbootclasspath/p:" + rt_file + daikonPath);
301      }
302    } else {
303      // allow DCRuntime to make reflective access to java.land.Object.clone() without a warning
304      cmdlist.add("--add-opens");
305      cmdlist.add("java.base/java.lang=ALL-UNNAMED");
306      if (!no_jdk) {
307        // If we are processing JDK classes, then we need our code on the boot classpath as well.
308        // Otherwise, references to DCRuntime from the JDK would fail.
309        // If daikonPath is nonempty, it starts with a pathSeparator.
310        cmdlist.add("-Xbootclasspath/a:" + rt_file + daikonPath);
311        // allow java.base to access daikon.jar (for instrumentation runtime)
312        cmdlist.add("--add-reads");
313        cmdlist.add("java.base=ALL-UNNAMED");
314        // allow DCRuntime to make reflective access to sun.util.locale (equals_dcomp_instrumented)
315        cmdlist.add("--add-exports");
316        cmdlist.add("java.base/sun.util.locale=ALL-UNNAMED");
317        // replace default java.base with our instrumented version
318        cmdlist.add("--patch-module");
319        cmdlist.add("java.base=" + rt_file);
320      }
321    }
322
323    cmdlist.add(String.format("-javaagent:%s=%s", premain, premain_args));
324
325    // A `for` loop is needed because targetArgs is an array and cmdlist is a list.
326    for (String target_arg : targetArgs) {
327      cmdlist.add(target_arg);
328    }
329    if (verbose) {
330      System.out.printf("%nExecuting target program: %s%n", argsToString(cmdlist));
331    }
332    String[] cmdline = cmdlist.toArray(new String[0]);
333
334    // Execute the command, sending all output to our streams.
335    java.lang.Runtime rt = java.lang.Runtime.getRuntime();
336    Process dcomp_proc;
337    try {
338      dcomp_proc = rt.exec(cmdline);
339    } catch (Exception e) {
340      System.out.printf("Exception '%s' while executing: %s%n", e, cmdline);
341      System.exit(1);
342      throw new Error("Unreachable control flow");
343    }
344
345    int targetResult = redirect_wait(dcomp_proc);
346    if (targetResult != 0) {
347      System.out.printf("Warning: Target exited with %d status.%n", targetResult);
348    }
349    System.exit(targetResult);
350  }
351
352  /** Wait for stream redirect threads to complete. */
353  public int redirect_wait(Process p) {
354
355    // Create the redirect threads and start them.
356    StreamRedirectThread in_thread =
357        new StreamRedirectThread("stdin", System.in, p.getOutputStream(), false);
358    StreamRedirectThread err_thread =
359        new StreamRedirectThread("stderr", p.getErrorStream(), System.err, true);
360    StreamRedirectThread out_thread =
361        new StreamRedirectThread("stdout", p.getInputStream(), System.out, true);
362
363    in_thread.start();
364    err_thread.start();
365    out_thread.start();
366
367    // Wait for the process to terminate and return the results
368    int result = -1;
369    while (true) {
370      try {
371        result = p.waitFor();
372        break;
373      } catch (InterruptedException e) {
374        System.out.printf("unexpected interrupt %s while waiting for target to finish", e);
375      }
376    }
377
378    // Make sure all output is forwarded before we finish
379    try {
380      err_thread.join();
381      out_thread.join();
382    } catch (InterruptedException e) {
383      System.out.printf("unexpected interrupt %s while waiting for threads to join", e);
384    }
385
386    return result;
387  }
388
389  /**
390   * Returns elapsed time since the start of the program.
391   *
392   * @return elapsed time since the start of the program
393   */
394  public static String elapsed() {
395    return "[" + elapsedMsecs() + " msec]";
396  }
397
398  /**
399   * Returns number of milliseconds since the start of the program.
400   *
401   * @return number of milliseconds since the start of the program
402   */
403  public static long elapsedMsecs() {
404    return System.currentTimeMillis() - start;
405  }
406
407  /**
408   * Convert a list of arguments into a command-line string. Only used for debugging output.
409   *
410   * @param args the list of arguments
411   * @return argument string
412   */
413  public String argsToString(List<String> args) {
414    String str = "";
415    for (String arg : args) {
416      if (arg.indexOf(" ") != -1) {
417        arg = "'" + arg + "'";
418      }
419      str += arg + " ";
420    }
421    return str.trim();
422  }
423
424  /**
425   * Returns true if Daikon or a Daikon library jar file is on the path argument. There are three
426   * cases:
427   *
428   * <ul>
429   *   <li>a jar file that contains "DynComp.class"
430   *   <li>a path that ends in "java/lib/<em>something</em>.jar"
431   *   <li>a path that leads to "daikon/DynComp.class"
432   * </ul>
433   *
434   * @param path classpath element to inspect for Daikon
435   * @return true if found
436   */
437  boolean isDaikonOnPath(String path) {
438    if (path.endsWith(".jar")) {
439      // path ends in ".jar".
440      try (JarFile jar = new JarFile(path)) {
441        JarEntry entry = jar.getJarEntry("daikon/DynComp.class");
442        if (entry != null) {
443          return true;
444        }
445      } catch (Exception e) {
446        // do nothing, try next case
447      }
448      // check to see if path is .../java/lib/...
449      String pathElements[] = path.split(Pattern.quote(File.separator));
450      int pathLength = pathElements.length;
451      if (pathLength == 0) {
452        // Can never happen? Fatal error if it does.
453        System.err.printf("classpath appears to be empty.%n");
454        System.exit(1);
455      }
456      if (pathLength > 2) {
457        if (pathElements[pathLength - 2].equals("lib")
458            && pathElements[pathLength - 3].equals("java")) {
459          File javaLibJarFile = new File(path);
460          if (javaLibJarFile.canRead()) {
461            return true;
462          }
463        }
464      }
465    } else {
466      // path does not end in ".jar"
467      File dyncompClassFile =
468          new File(path + File.separator + "daikon" + File.separator + "DynComp.class");
469      if (dyncompClassFile.canRead()) {
470        return true;
471      }
472    }
473    return false;
474  }
475
476  /**
477   * Search for a file on the current classpath, then in ${DAIKONDIR}/java. Returns null if not
478   * found.
479   *
480   * @param fileName the relative name of a file to look for
481   * @return path to fileName or null
482   */
483  @RequiresNonNull("cp")
484  public @Nullable File locateFile(String fileName) {
485    File poss_file = findOnClasspath(fileName);
486    if (poss_file != null) {
487      return poss_file;
488    }
489
490    // If not on the classpath look in ${DAIKONDIR}/java.
491    if (daikon_dir != null) {
492      poss_file = new File(new File(daikon_dir, "java"), fileName);
493      if (poss_file.canRead()) {
494        return poss_file;
495      }
496    }
497    // Couldn't find fileName
498    return null;
499  }
500
501  /**
502   * Search for a file on the current classpath. Returns null if not found.
503   *
504   * @param fileName the relative name of a file to look for
505   * @return path to fileName or null
506   */
507  @RequiresNonNull("cp")
508  public @Nullable File findOnClasspath(String fileName) {
509    for (String path : cp.split(File.pathSeparator)) {
510      File poss_file;
511      if (path.endsWith(fileName)) {
512        int start = path.indexOf(fileName);
513        // There are three cases:
514        //   path == fileName (start == 0)
515        //   path == <something>/fileName (charAt(start-1) == separator)
516        //   path == <something>/<something>fileName (otherwise)
517        // The first two are good, the last is not what we are looking for.
518        if (start == 0 || path.charAt(start - 1) == File.separatorChar) {
519          poss_file = new File(path);
520        } else {
521          poss_file = new File(path, fileName);
522        }
523      } else {
524        poss_file = new File(path, fileName);
525      }
526      if (poss_file.canRead()) {
527        return poss_file;
528      }
529    }
530    return null;
531  }
532}