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