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}