001package daikon;
002
003import static java.nio.charset.StandardCharsets.UTF_8;
004
005import daikon.chicory.StreamRedirectThread;
006import daikon.plumelib.bcelutil.SimpleLog;
007import daikon.plumelib.options.Option;
008import daikon.plumelib.options.Options;
009import daikon.plumelib.util.RegexUtil;
010import java.io.BufferedReader;
011import java.io.File;
012import java.io.IOException;
013import java.io.InputStream;
014import java.io.InputStreamReader;
015import java.util.ArrayList;
016import java.util.Arrays;
017import java.util.List;
018import java.util.regex.Pattern;
019import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
020import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
021import org.checkerframework.checker.nullness.qual.NonNull;
022import org.checkerframework.checker.nullness.qual.Nullable;
023import org.checkerframework.checker.nullness.qual.RequiresNonNull;
024import org.checkerframework.dataflow.qual.Pure;
025
026/**
027 * This is the main class for Chicory which transforms the class files of a program to instrument it
028 * for Daikon. The instrumentation uses the javaagent switch to java (which allows classes to be
029 * instrumented as they are loaded). This class parses the command line arguments, starts java with
030 * the javaagent switch on the target program and if requested starts Daikon on the result.
031 */
032public class Chicory {
033
034  /** Display usage information. */
035  @Option("-h Display usage information")
036  public static boolean help = false;
037
038  /** Print information about the classes being transformed. */
039  @Option("-v Print progress information")
040  public static boolean verbose = false;
041
042  /** Print debug information and save instrumented classes. */
043  @Option("-d Print debug information and save instrumented classes")
044  public static boolean debug = false;
045
046  /** File in which to put dtrace output. */
047  @Option("File in which to put dtrace output")
048  public static @MonotonicNonNull File dtrace_file = null;
049
050  /** Decl formatted file containing comparability information. */
051  @Option("Decl formatted file containing comparability information")
052  public static @Nullable File comparability_file = null;
053
054  /** Directory in which to create output files. */
055  @Option("Directory in which to create output files")
056  public static File output_dir = new File(".");
057
058  /** Directory in which to find configuration files. */
059  @Option("Directory in which to find configuration files")
060  public static @Nullable File config_dir = null;
061
062  /** Run Daikon in a separate process after Chicory. */
063  @Option("Run Daikon on the generated data trace file")
064  public static boolean daikon = false;
065
066  /** Send trace information to Daikon over a socket. */
067  @Option("Send trace information to Daikon over a socket")
068  public static boolean daikon_online = false;
069
070  // TODO: splitting on whitespace is error-prone.
071  /**
072   * Specifies Daikon arguments to be used if Daikon is run on a generated trace file {@code
073   * --daikon} or online via a socket {@code --daikon-online}. These arguments will be split on
074   * whitespace.
075   */
076  @Option("Specify Daikon arguments for either --daikon or --daikon-online")
077  public static String daikon_args = "";
078
079  // Should perhaps permit specifying the heap for the target program and
080  // for Daikon separately.
081  /** Heap size for the target program, and for Daikon if Daikon is run. */
082  @Option("Size of the heap for the target program, and for Daikon if it is run")
083  public static String heap_size = "3600m";
084
085  /**
086   * Path to Java agent jar file that performs the transformation. The "main" procedure is {@link
087   * daikon.chicory.ChicoryPremain#premain}.
088   */
089  @Option("Path to the Chicory agent jar file")
090  public static @MonotonicNonNull File premain = null;
091
092  /** Only emit program points that match the given regex. */
093  @Option("Include only program points that match")
094  public static List<Pattern> ppt_select_pattern = new ArrayList<>();
095
096  /** Suppress program points that match the given regex. */
097  @Option("Omit all program points that match")
098  public static List<Pattern> ppt_omit_pattern = new ArrayList<>();
099
100  /**
101   * When this option is chosen, Chicory will record each program point until that program point has
102   * been executed sample-cnt times. Chicory will then begin sampling. Sampling starts at 10% and
103   * decreases by a factor of 10 each time another sample-cnt samples have been recorded. If
104   * sample-cnt is 0, then all calls will be recorded.
105   */
106  @Option("Number of calls after which sampling will begin")
107  public static int sample_start = 0;
108
109  /** Treat classes that match the regex as boot classes (do not instrument). */
110  @Option("Treat classes that match the regex as boot classes (do not instrument)")
111  public static @Nullable Pattern boot_classes = null;
112
113  /**
114   * If true, no variable values are printed. Static variables are not initialized yet when the
115   * routine is entered, and static variable are not necessarily initialized to their final values
116   * when the routine is exited. These .dtrace entries are purely for the benefit of tools that use
117   * Chicory for program tracing, to determine when methods are entered and exited.
118   */
119  @Option("Write static initializer program points")
120  public static boolean instrument_clinit = false;
121
122  /** Depth to examine structure components. */
123  @Option("Depth to examine structure components")
124  public static int nesting_depth = 2;
125
126  /** Also see Daikon's {@code --var-omit-pattern} command-line argument. */
127  @Option("Omit variables that match this regular expression.")
128  public static @Nullable Pattern omit_var = null;
129
130  /**
131   * If false, every field in an instrumented class is visible. If true, use standard Java behavior
132   * (if the field is in a class in a different package, it is only visible if public, etc.).
133   */
134  @Option("Only include variables that are visible under normal Java access rules")
135  public static boolean std_visibility = false;
136
137  /**
138   * The name of the file to read for a list of pure methods. Should be 1 method per line. Each
139   * method should be in the same format as output by the purity analysis.
140   */
141  @Option("File of pure methods to use as additional Daikon variables")
142  public static @Nullable File purity_file;
143
144  // The next three command-line options are internal debugging
145  // options that are primarily for the use of the Daikon developers.
146
147  /** Print detailed information on which classes are transformed. */
148  @Option("Print detailed information on which classes are transformed")
149  public static boolean debug_transform = false;
150
151  /** Print detailed information on variables being observed. */
152  @Option("Print detailed information on variables being observed")
153  public static boolean debug_decl_print = false;
154
155  /** Print information about each ppt name as it is created. */
156  @Option("Print information about each ppt name as it is created")
157  public static boolean debug_ppt_names = false;
158
159  /** Daikon port number. Daikon writes this to stdout when it is started in online mode. */
160  private static int daikon_port = -1;
161
162  /** Thread that copies output from target to our output. */
163  public static @MonotonicNonNull StreamRedirectThread out_thread;
164
165  /** Thread that copies stderr from target to our stderr. */
166  public static @MonotonicNonNull StreamRedirectThread err_thread;
167
168  /** starting time (msecs) */
169  public static long start = System.currentTimeMillis();
170
171  /** daikon process for {@code --daikon} command-line option. */
172  // non-null if either daikon==true or daikon_online==true
173  public static @MonotonicNonNull Process daikon_proc;
174
175  private static final String traceLimTermString = "DTRACELIMITTERMINATE";
176  private static final String traceLimString = "DTRACELIMIT";
177
178  /** Flag to use if we want to turn on the static initialization checks. */
179  public static final boolean checkStaticInit = true;
180
181  private static final boolean RemoteDebug = false;
182
183  /** Flag to initiate a purity analysis and use results to create add vars. */
184  private static boolean purityAnalysis = false;
185
186  /** Log file if debug is enabled. */
187  private static final SimpleLog basic = new SimpleLog(false);
188
189  /** Synopsis for the Chicory command line. */
190  public static final String synopsis = "daikon.Chicory [options] target [target-args]";
191
192  /**
193   * Entry point of Chicory.
194   *
195   * @param args see usage for argument descriptions
196   */
197  public static void main(String[] args) {
198
199    // Parse our arguments
200    Options options = new Options(synopsis, Chicory.class);
201    options.setParseAfterArg(false);
202    String[] target_args = options.parse(true, args);
203    check_args(options, target_args);
204
205    // Turn on basic logging if debug was selected
206    basic.enabled = debug;
207    basic.log("target_args = %s%n", Arrays.toString(target_args));
208
209    // Start the target.  Pass the same options to the premain as
210    // were passed here.
211
212    Chicory chicory = new Chicory();
213    chicory.start_target(options.getOptionsString(), target_args);
214  }
215
216  /**
217   * Check the command-line arguments for legality. Prints a message and exits if there was an
218   * error.
219   *
220   * @param options set of legal options to Chicory
221   * @param target_args arguments being passed to the target program
222   */
223  public static void check_args(Options options, String[] target_args) {
224    if (help) {
225      options.printUsage();
226      System.exit(1);
227    }
228    if (nesting_depth < 0) {
229      System.out.printf("nesting depth (%d) must not be negative%n", nesting_depth);
230      options.printUsage();
231      System.exit(1);
232    }
233    if (target_args.length == 0) {
234      System.out.println("target program must be specified");
235      options.printUsage();
236      System.exit(1);
237    }
238    if (daikon && daikon_online) {
239      System.out.printf("may not specify both daikon and daikon-onlne%n");
240      options.printUsage();
241      System.exit(1);
242    }
243    if (!daikon_args.trim().isEmpty() && !(daikon || daikon_online)) {
244      System.out.printf("may not specify daikon-args without either daikon or daikon-onlne%n");
245      options.printUsage();
246      System.exit(1);
247    }
248  }
249
250  /**
251   * Return true iff argument was given to run a purity analysis.
252   *
253   * <p>You should only call this after parsing arguments.
254   */
255  public static boolean doPurity() {
256    return purityAnalysis;
257  }
258
259  /** Return true iff a file name was specified to supply pure method names. */
260  @Pure
261  public static @Nullable File get_purity_file() {
262    return purity_file;
263  }
264
265  /**
266   * Starts the target program with the Java agent setup to do the transforms. All Java agent
267   * arguments are passed to it. Our classpath is passed to the new JVM.
268   *
269   * @param premain_args the Java agent argument list
270   * @param target_args the test program name and its argument list
271   */
272  void start_target(String premain_args, String[] target_args) {
273
274    // Default the trace file name to <target-program-name>.dtrace.gz
275    if (dtrace_file == null) {
276      String target_class = target_args[0].replaceFirst(".*[/.]", "");
277      dtrace_file = new File(String.format("%s.dtrace.gz", target_class));
278      premain_args += " --dtrace-file=" + dtrace_file;
279    }
280
281    // Get the current classpath
282    String cp = System.getProperty("java.class.path");
283    basic.log("classpath = '%s'%n", cp);
284    if (cp == null) {
285      cp = ".";
286    }
287
288    // The separator for items in the class path
289    basic.log("File.pathSeparator = %s%n", File.pathSeparator);
290    if (!RegexUtil.isRegex(File.pathSeparator)) {
291      // This can't happen, at least on Unix & Windows.
292      throw new Daikon.UserError(
293          "Bad regexp "
294              + File.pathSeparator
295              + " for path.separator: "
296              + RegexUtil.regexError(File.pathSeparator));
297    }
298
299    // Look for ChicoryPremain.jar along the classpath
300    if (premain == null) {
301      String[] cpath = cp.split(File.pathSeparator);
302      for (String path : cpath) {
303        File poss_premain = new File(path, "ChicoryPremain.jar");
304        if (poss_premain.canRead()) {
305          premain = poss_premain;
306          break;
307        }
308      }
309    }
310
311    // If not on the classpath look in ${DAIKONDIR}/java
312    String daikon_dir = System.getenv("DAIKONDIR");
313    if (premain == null) {
314      if (daikon_dir != null) {
315        File poss_premain = new File(new File(daikon_dir, "java"), "ChicoryPremain.jar");
316        if (poss_premain.canRead()) {
317          premain = poss_premain;
318        }
319      }
320    }
321
322    // If not found, try the daikon.jar file itself
323    if (premain == null) {
324      for (String path : cp.split(File.pathSeparator)) {
325        File poss_premain = new File(path);
326        if (poss_premain.getName().equals("daikon.jar")) {
327          if (poss_premain.canRead()) {
328            premain = poss_premain;
329          }
330        }
331      }
332    }
333
334    // If we didn't find a premain, give up
335    if (premain == null) {
336      System.err.printf("Can't find ChicoryPremain.jar or daikon.jar on the classpath");
337      if (daikon_dir == null) {
338        System.err.printf(" and $DAIKONDIR is not set.%n");
339      } else {
340        System.err.printf(" or in $DAIKONDIR/java .%n");
341      }
342      System.err.printf("It should be found in the directory where Daikon was installed.%n");
343      System.err.printf("Use the --premain switch to specify its location,%n");
344      System.err.printf("or change your classpath to include it.%n");
345      System.exit(1);
346    }
347
348    String dtraceLim, terminate;
349    dtraceLim = System.getProperty(traceLimString);
350    terminate = System.getProperty(traceLimTermString);
351
352    // Run Daikon if we're in online mode
353    StreamRedirectThread daikon_err = null;
354    StreamRedirectThread daikon_out = null;
355    if (daikon_online) {
356      runDaikon();
357
358      StreamRedirectThread tmp_daikon_err =
359          new StreamRedirectThread("stderr", daikon_proc.getErrorStream(), System.err);
360      daikon_err = tmp_daikon_err;
361      daikon_err.start();
362
363      @NonNull InputStream daikonStdOut = daikon_proc.getInputStream();
364      // daikonReader escapes, so it is not closed in this method.
365      BufferedReader daikonReader = new BufferedReader(new InputStreamReader(daikonStdOut, UTF_8));
366
367      // Examine up to 100 lines of Daikon output, looking for
368      // the "DaikonChicoryOnlinePort=" line.  Note that if file progress
369      // is turned on in Daikon, it may be preceded by a timestamp.
370      for (int i = 0; i < 100; i++) {
371        String line;
372        try {
373          line = daikonReader.readLine();
374        } catch (IOException e1) {
375          System.out.printf("Exception reading output from Daikon: %s%n", e1);
376          line = null;
377        }
378
379        if (line == null) {
380          throw new RuntimeException("Did not receive socket port from Daikon!");
381        } else {
382          System.out.println(line);
383
384          if (line.contains("DaikonChicoryOnlinePort=")) {
385            String portStr = line.replaceFirst(".*DaikonChicoryOnlinePort=", "");
386            daikon_port = Integer.decode(portStr);
387            System.out.println("GOT PORT STRING " + daikon_port);
388            break;
389          }
390        }
391      }
392
393      if (daikon_port == -1) {
394        throw new RuntimeException("After 100 lines of output, Daikon port not received");
395      }
396
397      // continue reading daikon output in separate thread
398      daikon_out = new StreamRedirectThread("stdout", daikonStdOut, System.out);
399      daikon_out.start();
400    }
401
402    // Build the command line to execute the target with the javaagent
403    List<String> cmdlist = new ArrayList<>();
404    cmdlist.add("java");
405
406    if (RemoteDebug) {
407      cmdlist.add("-Xdebug");
408
409      cmdlist.add("-Xrunjdwp:server=n,transport=dt_socket,address=8000,suspend=y");
410      // cmdlist.add("-Xrunjdwp:server=y,transport=dt_socket,address=4142,suspend=n");
411
412      // cmdlist.add("-Xnoagent");
413      // cmdlist.add("-Xrunjdwp:server=n,transport=dt_socket,address=8000,suspend=n");
414      // cmdlist.add("-Djava.compiler=NONE");
415    }
416
417    cmdlist.add("-cp");
418    cmdlist.add(cp);
419    cmdlist.add("-ea");
420    cmdlist.add("-esa");
421    cmdlist.add("-Xmx" + heap_size);
422    // cmdlist.add ("-verbose");
423
424    if (dtraceLim != null) {
425      cmdlist.add("-D" + traceLimString + "=" + dtraceLim);
426    }
427    if (terminate != null) {
428      cmdlist.add("-D" + traceLimTermString + "=" + terminate);
429    }
430
431    // Specify the port to use to talk to Daikon if in online mode
432    if (daikon_online) {
433      assert daikon_port != -1 : daikon_port;
434      premain_args += " --daikon-port " + daikon_port;
435    }
436
437    cmdlist.add(String.format("-javaagent:%s=%s", premain, premain_args));
438
439    for (String target_arg : target_args) {
440      cmdlist.add(target_arg);
441    }
442    if (verbose) {
443      System.out.printf("%nExecuting target program: %s%n", args_to_string(cmdlist));
444    }
445    String[] cmdline = cmdlist.toArray(new String[0]);
446
447    // Execute the command, sending all output to our streams
448    java.lang.Runtime rt = java.lang.Runtime.getRuntime();
449    Process chicory_proc;
450    try {
451      chicory_proc = rt.exec(cmdline);
452    } catch (Exception e) {
453      System.out.printf("Exception '%s' while executing '%s'%n", e, cmdline);
454      System.exit(1);
455      throw new Error("Unreachable control flow");
456    }
457
458    int targetResult = redirect_wait(chicory_proc);
459
460    if (daikon) {
461      // Terminate if target didn't end properly
462      if (targetResult != 0) {
463        System.out.printf(
464            "Warning: Did not run Daikon because target exited with %d status%n", targetResult);
465        System.exit(targetResult);
466      }
467
468      runDaikon();
469      int daikonResult = waitForDaikon();
470      System.exit(daikonResult);
471    } else if (daikon_online) {
472      assert daikon_proc != null
473          : "@AssumeAssertion(nullness): conditional: just tested daikon_online, and ran"
474              + " runDaikon() earlier in this method";
475      if (targetResult != 0) {
476        System.out.printf("Warning: Target exited with %d status%n", targetResult);
477      }
478
479      // Wait for the process to terminate and return the results
480      int daikonResult = 0; // initialized to nonsense value to suppress compiler warning
481      while (true) {
482        try {
483          daikonResult = daikon_proc.waitFor();
484          break;
485        } catch (InterruptedException e) {
486          System.out.printf("unexpected interrupt %s while waiting for target to finish", e);
487        }
488      }
489
490      // Make sure all output is forwarded before we finish
491      try {
492        assert daikon_err != null
493            : "@AssumeAssertion(nullness): dependent: because daikon_online is true";
494        assert daikon_out != null
495            : "@AssumeAssertion(nullness): dependent: because daikon_online is true";
496        daikon_err.join();
497        daikon_out.join();
498      } catch (InterruptedException e) {
499        System.out.printf("unexpected interrupt %s while waiting for threads to join", e);
500      }
501
502      if (daikonResult != 0) {
503        System.out.printf("Warning: Daikon exited with %d status%n", daikonResult);
504      }
505      System.exit(daikonResult);
506    } else {
507      // No daikon command specified, so just exit
508      if (targetResult != 0) {
509        System.out.printf("Warning: Target exited with %d status%n", targetResult);
510      }
511      System.exit(targetResult);
512    }
513  }
514
515  /** Runs daikon either online or on the generated trace file. */
516  @EnsuresNonNull("daikon_proc")
517  public void runDaikon() {
518
519    java.lang.Runtime rt = java.lang.Runtime.getRuntime();
520
521    // Get the current classpath
522    String cp = System.getProperty("java.class.path");
523    if (cp == null) {
524      cp = ".";
525    }
526
527    List<String> cmd = new ArrayList<>();
528    cmd.add("java");
529    cmd.add("-Xmx" + heap_size);
530    cmd.add("-cp");
531    cmd.add(cp);
532    cmd.add("-ea");
533    cmd.add("daikon.Daikon");
534    if (!daikon_args.trim().isEmpty()) {
535      for (String arg : daikon_args.split(" +")) {
536        cmd.add(arg);
537      }
538    }
539    if (daikon_online) {
540      cmd.add("+");
541    } else {
542      cmd.add(output_dir + File.separator + dtrace_file);
543    }
544
545    // System.out.println("daikon command cmd " + cmd);
546
547    if (verbose) {
548      System.out.printf("%nExecuting daikon: %s%n", cmd);
549    }
550
551    try {
552      daikon_proc = rt.exec(cmd.toArray(new String[0]));
553    } catch (Exception e) {
554      System.out.printf("Exception '%s' while executing '%s'%n", e, cmd);
555      System.exit(1);
556    }
557  }
558
559  /**
560   * Wait for daikon to complete and return its exit status.
561   *
562   * @return Daikon's exit status
563   */
564  @RequiresNonNull("daikon_proc")
565  private int waitForDaikon() {
566    return redirect_wait(daikon_proc);
567  }
568
569  /**
570   * Wait for stream redirect threads to complete and return their exit status.
571   *
572   * @param p the process to wait for completion
573   * @return process result
574   */
575  public int redirect_wait(Process p) {
576
577    // Create the redirect threads and start them.
578    StreamRedirectThread in_thread =
579        new StreamRedirectThread("stdin", System.in, p.getOutputStream(), false);
580    StreamRedirectThread err_thread =
581        new StreamRedirectThread("stderr", p.getErrorStream(), System.err, true);
582    StreamRedirectThread out_thread =
583        new StreamRedirectThread("stdout", p.getInputStream(), System.out, true);
584
585    in_thread.start();
586    err_thread.start();
587    out_thread.start();
588
589    // Wait for the process to terminate and return the results
590    int result = -1;
591    while (true) {
592      try {
593        result = p.waitFor();
594        break;
595      } catch (InterruptedException e) {
596        System.out.printf("unexpected interrupt %s while waiting for target to finish", e);
597      }
598    }
599
600    // Make sure all output is forwarded before we finish
601    try {
602      err_thread.join();
603      out_thread.join();
604    } catch (InterruptedException e) {
605      System.out.printf("unexpected interrupt %s while waiting for threads to join", e);
606    }
607
608    return result;
609  }
610
611  /**
612   * Returns string representation of elapsed time since the start of the program.
613   *
614   * @return string representation of elapsed time since the start of the program
615   */
616  public static String elapsed() {
617    return "[" + (System.currentTimeMillis() - start) + " msec]";
618  }
619
620  /**
621   * Returns number of milliseconds since the start of the program.
622   *
623   * @return number of milliseconds since the start of the program
624   */
625  public static long elapsed_msecs() {
626    return System.currentTimeMillis() - start;
627  }
628
629  /** Convert a list of arguments into a command-line string. Only used for debugging output. */
630  public String args_to_string(List<String> args) {
631    String str = "";
632    for (String arg : args) {
633      if (arg.indexOf(" ") != -1) {
634        str = "'" + str + "'";
635      }
636      str += arg + " ";
637    }
638    return str.trim();
639  }
640}