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}