001package daikon.chicory; 002 003// import harpoon.ClassFile.HMethod; 004 005import static daikon.tools.nullness.NullnessUtil.castNonNull; 006 007import daikon.Chicory; 008import daikon.plumelib.bcelutil.SimpleLog; 009import daikon.plumelib.options.Option; 010import daikon.plumelib.options.Options; 011import daikon.plumelib.util.FilesPlume; 012import java.io.BufferedReader; 013import java.io.File; 014import java.io.FileNotFoundException; 015import java.io.IOException; 016import java.io.UncheckedIOException; 017import java.lang.instrument.ClassFileTransformer; 018import java.lang.instrument.Instrumentation; 019import java.lang.reflect.Member; 020import java.net.URL; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collections; 024import java.util.Enumeration; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Set; 028import java.util.jar.JarFile; 029import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; 030import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 031import org.checkerframework.checker.nullness.qual.Nullable; 032import org.checkerframework.checker.nullness.qual.RequiresNonNull; 033import org.checkerframework.checker.signature.qual.BinaryName; 034 035/** 036 * This class is the entry point for the Chicory instrumentation agent. It is the only code in 037 * ChicoryPremain.jar. 038 */ 039public class ChicoryPremain { 040 041 // These command-line options cannot be accessed from Chicory. These are internal debugging 042 // options that may be used when ChicoryPremain is invoked directly from the command line. 043 044 /** Socket port to communicate with Daikon. */ 045 @Option("socket port to communicate with Daikon") 046 public static int daikon_port = -1; 047 048 /** Turn on most Runtime debugging options. */ 049 @Option("Turn on most Runtime debugging options") 050 public static boolean debug_runtime = false; 051 052 /** Print information about the classes being transformed. */ 053 public static boolean verbose = false; 054 055 /** Set of pure methods returned by Alexandru Salcianu's purity analysis. */ 056 // Non-null if doPurity == true 057 private static @MonotonicNonNull Set<String> pureMethods = null; 058 059 /** True iff Chicory should add variables based on pure methods during instrumentation. */ 060 private static boolean doPurity = false; 061 062 /** 063 * This method is the entry point of the Java agent. Its main purpose is to set up the transformer 064 * so that when classes from the target app are loaded, they are first transformed. 065 * 066 * <p>This method also sets up some other initialization tasks: it connects to Daikon over a port 067 * if necessary, or reads in a purity analysis. 068 */ 069 public static void premain(String agentArgs, Instrumentation inst) throws IOException { 070 071 // System.out.format ("In premain, agentargs ='%s', " + 072 // "Instrumentation = '%s'%n", agentArgs, inst); 073 074 // Because Chicory started ChicoryPremain in a separate process, we must rescan 075 // the options to set up the Chicory static variables. 076 Options options = new Options(Chicory.synopsis, Chicory.class, ChicoryPremain.class); 077 String[] target_args = options.parse(true, Options.tokenize(agentArgs)); 078 if (target_args.length > 0) { 079 System.err.printf("Unexpected ChicoryPremain arguments %s%n", Arrays.toString(target_args)); 080 options.printUsage(); 081 System.exit(1); 082 } 083 084 verbose = Chicory.verbose || Chicory.debug; 085 if (debug_runtime) { 086 Runtime.debug = true; 087 } 088 089 if (verbose) { 090 System.out.printf( 091 "In Chicory premain, agentargs ='%s', Instrumentation = '%s'", agentArgs, inst); 092 System.out.printf("Options settings: %n%s%n", options.settings()); 093 } 094 095 // Open the dtrace file 096 if (Chicory.daikon_online) { 097 Runtime.setDtraceOnlineMode(daikon_port); 098 } else if (Chicory.dtrace_file == null) { 099 File trace_file_path = new File(Chicory.output_dir, "dtrace.gz"); 100 Runtime.setDtraceMaybe(trace_file_path.toString()); 101 } else { 102 File trace_file_path = new File(Chicory.output_dir, Chicory.dtrace_file.getPath()); 103 Runtime.setDtraceMaybe(trace_file_path.toString()); 104 } 105 106 // Setup argument fields in Runtime 107 Runtime.nesting_depth = Chicory.nesting_depth; 108 // daikon.chicory.Instrument.shouldIgnore is shared by Chicory and DynComp. 109 // It uses the Runtime copy of the patterns. 110 Runtime.ppt_omit_pattern = Chicory.ppt_omit_pattern; 111 Runtime.ppt_select_pattern = Chicory.ppt_select_pattern; 112 Runtime.sample_start = Chicory.sample_start; 113 DaikonVariableInfo.std_visibility = Chicory.std_visibility; 114 DaikonVariableInfo.debug_vars.enabled = Chicory.debug_decl_print; 115 if (Chicory.comparability_file != null) { 116 Runtime.comp_info = new DeclReader(); 117 try { 118 castNonNull(Runtime.comp_info).read(castNonNull(Chicory.comparability_file)); 119 } catch (FileNotFoundException e) { 120 System.err.printf("%nCould not find comparability file: %s%n", Chicory.comparability_file); 121 Runtime.chicoryLoaderInstantiationError = true; 122 System.exit(1); 123 } 124 if (verbose) { 125 System.out.printf("Read comparability from %s%n", Chicory.comparability_file); 126 // Runtime.comp_info.dump(); 127 } 128 } 129 130 if (Chicory.doPurity()) { 131 System.err.println("Executing a purity analysis is currently disabled"); 132 System.exit(1); 133 } else if (Chicory.get_purity_file() != null) { 134 readPurityFile(Chicory.get_purity_file(), Chicory.config_dir); 135 doPurity = true; 136 } 137 138 initializeDeclAndDTraceWriters(); 139 140 // Setup the transformer 141 ClassFileTransformer transformer; 142 // use a special classloader to ensure correct version of BCEL is used 143 ClassLoader loader = new ChicoryLoader(); 144 try { 145 transformer = 146 (ClassFileTransformer) 147 loader.loadClass("daikon.chicory.Instrument").getDeclaredConstructor().newInstance(); 148 } catch (Exception e) { 149 throw new RuntimeException("Unexpected error loading Instrument", e); 150 } 151 if (Chicory.debug) { 152 System.out.printf( 153 "Classloader of transformer = %s%n", transformer.getClass().getClassLoader()); 154 } 155 156 // now turn on instrumentation 157 if (verbose) { 158 System.out.println("call addTransformer"); 159 } 160 inst.addTransformer(transformer); 161 162 if (verbose) { 163 System.out.println("exit premain"); 164 } 165 } 166 167 /** Set up the declaration and dtrace writer. */ 168 // Runtime.dtrace is @GuardedBy("<self>") because in the Runtime class, 169 // the printing of final lines and then closing of dtrace only happens 170 // when the monitor of dtrace is held in order for the closing of the 171 // trace to happen only once. See Runtime.noMoreOutput() and 172 // Runtime.addShutdownHook() for more details. DeclWriter and DTraceWriter 173 // never perform this operation (print final lines and close) on the 174 // value of dtrace passed in, therefore they do not need to make use 175 // of synchronization and their references to dtrace do not need to 176 // be annotated with @GuardedBy("<self>"). 177 @SuppressWarnings("lock:argument") 178 private static void initializeDeclAndDTraceWriters() { 179 // The include/exclude filter are implemented in the transform, 180 // so they don't need to be handled here. 181 // (It looks like these can be called even if Runtime.dtrace is null...) 182 Runtime.decl_writer = new DeclWriter(Runtime.dtrace); 183 Runtime.dtrace_writer = new DTraceWriter(Runtime.dtrace); 184 } 185 186 /** 187 * Reads purity file. Each line should contain exactly one method. Care must be taken to supply 188 * the correct format. 189 * 190 * <p>From the Sun JDK API: 191 * 192 * <p>"The string is formatted as the method access modifiers, if any, followed by the method 193 * return type, followed by a space, followed by the class declaring the method, followed by a 194 * period, followed by the method name, followed by a parenthesized, comma-separated list of the 195 * method's formal parameter types. If the method throws checked exceptions, the parameter list is 196 * followed by a space, followed by the word throws followed by a comma-separated list of the 197 * thrown exception types. For example: 198 * 199 * <p>public boolean java.lang.Object.equals(java.lang.Object) 200 * 201 * <p>The access modifiers are placed in canonical order as specified by "The Java Language 202 * Specification". This is public, protected or private first, and then other modifiers in the 203 * following order: abstract, static, final, synchronized native. 204 * 205 * @param purityFileName the purity file 206 * @param pathLoc the relative path; interpret {@code purityFileName} with respect to it 207 */ 208 private static void readPurityFile(File purityFileName, @Nullable File pathLoc) { 209 pureMethods = new HashSet<String>(); 210 File purityFile = new File(pathLoc, purityFileName.getPath()); 211 String purityFileAbsolutePath = purityFile.getAbsolutePath(); 212 213 BufferedReader reader; 214 try { 215 reader = FilesPlume.newBufferedFileReader(purityFile); 216 } catch (FileNotFoundException e) { 217 System.err.printf( 218 "%nCould not find purity file %s = %s%n", purityFileName, purityFileAbsolutePath); 219 Runtime.chicoryLoaderInstantiationError = true; 220 System.exit(1); 221 throw new Error("Unreachable control flow"); 222 } catch (IOException e) { 223 throw new UncheckedIOException( 224 "Problem reading purity file " + purityFileName + " = " + purityFileAbsolutePath, e); 225 } 226 227 if (verbose) { 228 System.out.printf("Reading '%s' for pure methods %n", purityFileName); 229 } 230 231 String line = null; 232 do { 233 try { 234 line = reader.readLine(); 235 } catch (IOException e) { 236 try { 237 reader.close(); 238 } catch (IOException e2) { 239 // Do nothing 240 } 241 throw new UncheckedIOException( 242 "Error reading file " + purityFileName + " = " + purityFileAbsolutePath, e); 243 } 244 245 if (line != null) { 246 pureMethods.add(line.trim()); 247 // System.out.printf("Adding '%s' to list of pure methods%n", 248 // line); 249 } 250 } while (line != null); 251 252 try { 253 reader.close(); 254 } catch (IOException e) { 255 System.err.println("Error while closing " + purityFileName + " after reading."); 256 System.exit(1); 257 } 258 259 // System.out.printf("leaving purify file%n"); 260 261 } 262 263 /** Return true iff Chicory has run a purity analysis or read a {@code *.pure} file. */ 264 @SuppressWarnings("nullness") // dependent: pureMethods is non-null if doPurity is true 265 // @EnsuresNonNullIf(result=true, expression="ChicoryPremain.pureMethods") 266 @EnsuresNonNullIf(result = true, expression = "pureMethods") 267 public static boolean shouldDoPurity() { 268 return doPurity; 269 } 270 271 /** 272 * Checks if member is one of the pure methods found in a purity analysis or supplied from a 273 * {@code *.pure} file. 274 * 275 * @return true iff member is a pure method 276 */ 277 // @RequiresNonNull("ChicoryPremain.pureMethods") 278 @RequiresNonNull("pureMethods") 279 public static boolean isMethodPure(Member member) { 280 assert shouldDoPurity() : "Can't query for purity if no purity analysis was executed"; 281 282 // TODO just use Set.contains(member.toString()) ? 283 for (String methName : pureMethods) { 284 if (methName.equals(member.toString())) { 285 return true; 286 } 287 } 288 289 return false; 290 } 291 292 /** Return an unmodifiable Set of the pure methods. */ 293 // @RequiresNonNull("ChicoryPremain.pureMethods") 294 @RequiresNonNull("pureMethods") 295 public static Set<String> getPureMethods() { 296 return Collections.unmodifiableSet(pureMethods); 297 } 298 299 /** 300 * Classloader for the BCEL code. Using this classloader guarantees that we get the correct 301 * version of BCEL and not a possible incompatible version from elsewhere on the user's classpath. 302 * We also load daikon.chicory.Instrument via this (since that class is the user of all of the 303 * BCEL classes). All references to BCEL must be within that class (so that all references to BCEL 304 * will get resolved by this classloader). 305 * 306 * <p>There are several versions of BCEL that have been released: 307 * 308 * <ul> 309 * <li>the original 5.2 version 310 * <li>an interim 6.0 version 311 * <li>the offical 6.0 release version 312 * <li>the offical 6.1 release version 313 * <li>the PLSE 6.1 release version (includes LocalVariableGen fix) 314 * <li>the offical 6.2 release version (includes LocalVariableGen fix) 315 * <li>the offical 6.3 release version 316 * <li>the offical 6.3.1 release version 317 * <li>the offical 6.4.1 release version 318 * <li>the PLSE 6.4.1.1 release version (includes JDK 11 support) 319 * </ul> 320 * 321 * <p>Note that both Chicory and DynComp use the ChicoryLoader to load BCEL and to verify that the 322 * version loaded is acceptable. However, the official 6.1 release version is sufficient for 323 * Chicory while DynComp requires the latest PLSE 6.4.1.1 version. Hence, this loader only checks 324 * for the official 6.1 release version (or newer). After loading BCEL, DynComp will make an 325 * additional check to verify that the 6.4.1.1 version has been loaded. 326 * 327 * <p>There are two classes present in 6.1 and subsequent releases that are not in previous 328 * versions. Thus, we can identify version 6.1 (and later) of BCEL by the presence of the class: 329 * org.apache.bcel.classfile.ConstantModule.class. 330 * 331 * <p>Earlier versions of Chicory inspected all version of BCEL found on the path and selected the 332 * correct one, if present. We now (9/15/16) simplify this to say the first BCEL found must be the 333 * correct one. This allows us to use the normal loader for all of the classes. 334 */ 335 public static class ChicoryLoader extends ClassLoader { 336 337 /** Log file if verbose is enabled. */ 338 public static final SimpleLog debug = new SimpleLog(ChicoryPremain.verbose); 339 340 /** 341 * Constructor for special BCEL class loader. 342 * 343 * @throws IOException if unable to load class 344 */ 345 @SuppressWarnings("StaticAssignmentInConstructor") // sets static variable only if aborting 346 public ChicoryLoader() throws IOException { 347 348 String bcel_classname = "org.apache.bcel.Constants"; 349 String plse_marker_classname = "org.apache.bcel.classfile.ConstantModule"; 350 351 List<URL> bcel_urls = get_resource_list(bcel_classname); 352 List<URL> plse_urls = get_resource_list(plse_marker_classname); 353 354 if (plse_urls.size() == 0) { 355 System.err.printf( 356 "%nBCEL 6.1 or newer must be on the classpath. Normally it is found in daikon.jar.%n"); 357 Runtime.chicoryLoaderInstantiationError = true; 358 System.exit(1); 359 } 360 if (bcel_urls.size() < plse_urls.size()) { 361 System.err.printf("%nCorrupted BCEL library, bcel %s, plse %s%n", bcel_urls, plse_urls); 362 Runtime.chicoryLoaderInstantiationError = true; 363 System.exit(1); 364 } 365 366 // No need to do anything if only our versions of bcel are present 367 if (bcel_urls.size() == plse_urls.size()) { 368 return; 369 } 370 371 URL bcel = bcel_urls.get(0); 372 URL plse = plse_urls.get(0); 373 if (!plse.getProtocol().equals("jar")) { 374 System.err.printf("%nDaikon BCEL must be in jar file. Found at %s%n", plse); 375 Runtime.chicoryLoaderInstantiationError = true; 376 System.exit(1); 377 } 378 if (!same_location(bcel, plse)) { 379 System.err.printf( 380 "%nDaikon BCEL (%s) is not first BCEL on the classpath (%s).%n", plse, bcel); 381 Runtime.chicoryLoaderInstantiationError = true; 382 System.exit(1); 383 } else { 384 try (JarFile bcel_jar = new JarFile(extract_jar_path(plse))) { 385 debug.log("Daikon BCEL found in jar %s%n", bcel_jar.getName()); 386 } 387 } 388 } 389 390 /** 391 * Returns whether or not the two URL represent the same location for org.apache.bcel. Two 392 * locations match if they refer to the same jar file or the same directory in the filesystem. 393 */ 394 private static boolean same_location(URL url1, URL url2) { 395 if (!url1.getProtocol().equals(url2.getProtocol())) { 396 return false; 397 } 398 399 if (url1.getProtocol().equals("jar")) { 400 // System.out.printf("url1 = %s, file=%s, path=%s, protocol=%s, %s%n", 401 // url1, url1.getFile(), url1.getPath(), 402 // url1.getProtocol(), url1.getClass()); 403 // System.out.printf("url2 = %s, file=%s, path=%s, protocol=%s, %s%n", 404 // url2, url2.getFile(), url2.getPath(), 405 // url2.getProtocol(), url1.getClass()); 406 String jar1 = extract_jar_path(url1); 407 String jar2 = extract_jar_path(url2); 408 return jar1.equals(jar2); 409 } else if (url1.getProtocol().equals("file")) { 410 String loc1 = url1.getFile().replaceFirst("org\\.apache\\.bcel\\..*$", ""); 411 String loc2 = url2.getFile().replaceFirst("org\\.apache\\.bcel\\..*$", ""); 412 return loc1.equals(loc2); 413 } else { 414 throw new Error("unexpected protocol " + url1.getProtocol()); 415 } 416 } 417 418 /** 419 * Returns the pathname of a jar file specified in the URL. The protocol must be 'jar'. Only 420 * file jars are supported. 421 */ 422 private static String extract_jar_path(URL url) { 423 assert url.getProtocol().equals("jar") : url.toString(); 424 425 // Remove the preceeding 'file:' and trailing '!filename' 426 String path = url.getFile(); 427 path = path.replaceFirst("^[^:]*:", ""); 428 path = path.replaceFirst("![^!]*$", ""); 429 430 return path; 431 } 432 433 /** 434 * Get all of the URLs that match the specified name in the classpath. The name should be in 435 * normal classname format (eg, org.apache.bcel.Const). An empty list is returned if no names 436 * match. 437 */ 438 @SuppressWarnings("JdkObsolete") // ClassLoader.getSystemResources returns an Enumeration 439 static List<URL> get_resource_list(String classname) throws IOException { 440 441 String name = classname_to_resource_name(classname); 442 Enumeration<URL> enum_urls = ClassLoader.getSystemResources(name); 443 List<URL> urls = new ArrayList<>(); 444 while (enum_urls.hasMoreElements()) { 445 urls.add(enum_urls.nextElement()); 446 } 447 return urls; 448 } 449 450 /** 451 * Changes a class name in the normal format (eg, org.apache.bcel.Const) to that used to lookup 452 * resources (eg, org/apache/bcel/Const.class). 453 */ 454 private static String classname_to_resource_name(String name) { 455 return (name.replace(".", "/") + ".class"); 456 } 457 458 @Override 459 protected Class<?> loadClass(@BinaryName String name, boolean resolve) 460 throws java.lang.ClassNotFoundException { 461 462 return super.loadClass(name, resolve); 463 } 464 } 465}