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}