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}