Add agent for printing breakpoints

This can be used to test performance of breakpointed code in a
lightweight way or to ensure that code is reaching expected locations.

See tools/breakpoint-logger/README.md for information on how to use.

Also added the ability to pass explicit agents for run-test to load
before running tests.

Test: ./test/run-test                                                 \
        --host                                                        \
        --dev                                                         \
        --with-agent                                                  \
        'libbreakpointlogger.so=LMain;->main([Ljava/lang/String;)V@0' \
        001-HelloWorld

Bug: 68259370

Change-Id: I409c940a34b1e823ec50649cdb3c51e5a81a09ab
diff --git a/Android.bp b/Android.bp
index 5e3a8d8..295ae4c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -41,6 +41,7 @@
     "sigchainlib",
     "simulator",
     "test",
+    "tools/breakpoint-logger",
     "tools/cpp-define-generator",
     "tools/dmtracedump",
     "tools/titrace",
diff --git a/test/etc/run-test-jar b/test/etc/run-test-jar
index 2fda494..bf964a6 100755
--- a/test/etc/run-test-jar
+++ b/test/etc/run-test-jar
@@ -16,6 +16,7 @@
 COMPILE_FLAGS=""
 DALVIKVM="dalvikvm32"
 DEBUGGER="n"
+WITH_AGENT=""
 DEBUGGER_AGENT=""
 WRAP_DEBUGGER_AGENT="n"
 DEV_MODE="n"
@@ -228,6 +229,11 @@
         FLAGS="${FLAGS} -Xcompiler-option --dump-cfg-append"
         COMPILE_FLAGS="${COMPILE_FLAGS} --dump-cfg-append"
         shift
+    elif [ "x$1" = "x--with-agent" ]; then
+        shift
+        USE_JVMTI="y"
+        WITH_AGENT="$1"
+        shift
     elif [ "x$1" = "x--debug-wrap-agent" ]; then
         WRAP_DEBUGGER_AGENT="y"
         shift
@@ -442,6 +448,10 @@
   DEBUGGER_OPTS="-agentpath:${AGENTPATH}=transport=dt_socket,address=$PORT,server=y,suspend=y"
 fi
 
+if [ "x$WITH_AGENT" != "x" ]; then
+  FLAGS="${FLAGS} -agentpath:${WITH_AGENT}"
+fi
+
 if [ "$USE_JVMTI" = "y" ]; then
   if [ "$USE_JVM" = "n" ]; then
     plugin=libopenjdkjvmtid.so
diff --git a/test/run-test b/test/run-test
index 09a70e5..fdb2ee4 100755
--- a/test/run-test
+++ b/test/run-test
@@ -291,6 +291,11 @@
     elif [ "x$1" = "x--debug-wrap-agent" ]; then
         run_args="${run_args} --debug-wrap-agent"
         shift
+    elif [ "x$1" = "x--with-agent" ]; then
+        shift
+        option="$1"
+        run_args="${run_args} --with-agent $1"
+        shift
     elif [ "x$1" = "x--debug-agent" ]; then
         shift
         option="$1"
@@ -661,6 +666,7 @@
         echo "                          only supported on host."
         echo "    --debug-wrap-agent    use libwrapagentproperties and tools/libjdwp-compat.props"
         echo "                          to load the debugger agent specified by --debug-agent."
+        echo "    --with-agent <agent>  Run the test with the given agent loaded with -agentpath:"
         echo "    --debuggable          Whether to compile Java code for a debugger."
         echo "    --gdb                 Run under gdb; incompatible with some tests."
         echo "    --gdb-arg             Pass an option to gdb."
diff --git a/tools/breakpoint-logger/Android.bp b/tools/breakpoint-logger/Android.bp
new file mode 100644
index 0000000..67b423a
--- /dev/null
+++ b/tools/breakpoint-logger/Android.bp
@@ -0,0 +1,66 @@
+//
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+// Build variants {target,host} x {debug,ndebug} x {32,64}
+
+cc_defaults {
+    name: "breakpointlogger-defaults",
+    host_supported: true,
+    srcs: ["breakpoint_logger.cc"],
+    defaults: ["art_defaults"],
+
+    // Note that this tool needs to be built for both 32-bit and 64-bit since it requires
+    // to be same ISA as what it is attached to.
+    compile_multilib: "both",
+
+    shared_libs: [
+        "libbase",
+    ],
+    target: {
+        android: {
+        },
+        host: {
+        },
+    },
+    header_libs: [
+        "libopenjdkjvmti_headers",
+    ],
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+    symlink_preferred_arch: true,
+}
+
+art_cc_library {
+    name: "libbreakpointlogger",
+    defaults: ["breakpointlogger-defaults"],
+    shared_libs: [
+    ],
+}
+
+art_cc_library {
+    name: "libbreakpointloggerd",
+    defaults: [
+        "art_debug_defaults",
+        "breakpointlogger-defaults",
+    ],
+    shared_libs: [],
+}
diff --git a/tools/breakpoint-logger/README.md b/tools/breakpoint-logger/README.md
new file mode 100644
index 0000000..d7ffb34
--- /dev/null
+++ b/tools/breakpoint-logger/README.md
@@ -0,0 +1,54 @@
+# breakpointlogger
+
+breakpointlogger is a JVMTI agent that lets one set breakpoints that are logged
+when they are hit.
+
+# Usage
+### Build
+>    `make libbreakpointlogger`  # or 'make libbreakpointloggerd' with debugging checks enabled
+
+The libraries will be built for 32-bit, 64-bit, host and target. Below examples
+assume you want to use the 64-bit version.
+
+### Command Line
+
+The agent is loaded using -agentpath like normal. It takes arguments in the
+following format:
+>     `:class_descriptor:->:methodName::method_sig:@:breakpoint_location:,[...]`
+
+* The breakpoint\_location is a number that's a valid jlocation for the runtime
+  being used. On ART this is a dex-pc. Dex-pcs can be found using tools such as
+  dexdump and are uint16\_t-offsets from the start of the method. On other
+  runtimes jlocations might represent other things.
+
+* Multiple breakpoints can be included in the options, separated with ','s.
+
+* Unlike with most normal debuggers the agent will load the class immediately to
+  set the breakpoint. This means that classes might be initialized earlier than
+  one might expect. This also means that one cannot set breakpoints on classes
+  that cannot be found using standard or bootstrap classloader at startup.
+
+* Deviating from this format or including a breakpoint that cannot be found at
+  startup will cause the runtime to abort.
+
+#### ART
+>    `art -Xplugin:$ANDROID_HOST_OUT/lib64/libopenjdkjvmti.so '-agentpath:libbreakpointlogger.so=Lclass/Name;->methodName()V@0' -cp tmp/java/helloworld.dex -Xint helloworld`
+
+* `-Xplugin` and `-agentpath` need to be used, otherwise the agent will fail during init.
+* If using `libartd.so`, make sure to use the debug version of jvmti.
+
+#### RI
+>    `java '-agentpath:libbreakpointlogger.so=Lclass/Name;->methodName()V@0' -cp tmp/helloworld/classes helloworld`
+
+### Output
+A normal run will look something like this:
+
+    % ./test/run-test --host --dev --with-agent 'libbreakpointlogger.so=LMain;->main([Ljava/lang/String;)V@0' 001-HelloWorld
+    <normal output removed>
+    dalvikvm32 W 10-25 10:39:09 18063 18063 breakpointlogger.cc:277] Breakpoint at location: 0x00000000 in method LMain;->main([Ljava/lang/String;)V (source: Main.java:13) thread: main
+    Hello, world!
+
+    % ./test/run-test --jvm --dev --with-agent 'libbreakpointlogger.so=LMain;->main([Ljava/lang/String;)V@0' 001-HelloWorld
+    <normal output removed>
+    java W 10-25 10:39:09 18063 18063 breakpointlogger.cc:277] Breakpoint at location: 0x00000000 in method LMain;->main([Ljava/lang/String;)V (source: Main.java:13) thread: main
+    Hello, world!
diff --git a/tools/breakpoint-logger/breakpoint_logger.cc b/tools/breakpoint-logger/breakpoint_logger.cc
new file mode 100644
index 0000000..b48a178
--- /dev/null
+++ b/tools/breakpoint-logger/breakpoint_logger.cc
@@ -0,0 +1,447 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <android-base/logging.h>
+#include <atomic>
+#include <iostream>
+#include <iomanip>
+#include <jni.h>
+#include <jvmti.h>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace breakpoint_logger {
+
+struct SingleBreakpointTarget {
+  std::string class_name;
+  std::string method_name;
+  std::string method_sig;
+  jlocation location;
+};
+
+struct BreakpointTargets {
+  std::vector<SingleBreakpointTarget> bps;
+};
+
+static void VMInitCB(jvmtiEnv* jvmti, JNIEnv* env, jthread thr ATTRIBUTE_UNUSED) {
+  BreakpointTargets* all_targets = nullptr;
+  jvmtiError err = jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&all_targets));
+  if (err != JVMTI_ERROR_NONE || all_targets == nullptr) {
+    env->FatalError("unable to get breakpoint targets");
+  }
+  for (const SingleBreakpointTarget& target : all_targets->bps) {
+    jclass k = env->FindClass(target.class_name.c_str());
+    if (env->ExceptionCheck()) {
+      env->ExceptionDescribe();
+      env->FatalError("Could not find class!");
+      return;
+    }
+    jmethodID m = env->GetMethodID(k, target.method_name.c_str(), target.method_sig.c_str());
+    if (env->ExceptionCheck()) {
+      env->ExceptionClear();
+      m = env->GetStaticMethodID(k, target.method_name.c_str(), target.method_sig.c_str());
+      if (env->ExceptionCheck()) {
+        env->ExceptionDescribe();
+        env->FatalError("Could not find method!");
+        return;
+      }
+    }
+    err = jvmti->SetBreakpoint(m, target.location);
+    if (err != JVMTI_ERROR_NONE) {
+      env->FatalError("unable to set breakpoint");
+      return;
+    }
+    env->DeleteLocalRef(k);
+  }
+}
+
+class ScopedThreadInfo {
+ public:
+  ScopedThreadInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jthread thread)
+      : jvmti_env_(jvmti_env), env_(env), free_name_(false) {
+    memset(&info_, 0, sizeof(info_));
+    if (thread == nullptr) {
+      info_.name = const_cast<char*>("<NULLPTR>");
+    } else if (jvmti_env->GetThreadInfo(thread, &info_) != JVMTI_ERROR_NONE) {
+      info_.name = const_cast<char*>("<UNKNOWN THREAD>");
+    } else {
+      free_name_ = true;
+    }
+  }
+
+  ~ScopedThreadInfo() {
+    if (free_name_) {
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(info_.name));
+    }
+    env_->DeleteLocalRef(info_.thread_group);
+    env_->DeleteLocalRef(info_.context_class_loader);
+  }
+
+  const char* GetName() const {
+    return info_.name;
+  }
+
+ private:
+  jvmtiEnv* jvmti_env_;
+  JNIEnv* env_;
+  bool free_name_;
+  jvmtiThreadInfo info_;
+};
+
+class ScopedClassInfo {
+ public:
+  ScopedClassInfo(jvmtiEnv* jvmti_env, jclass c)
+      : jvmti_env_(jvmti_env),
+        class_(c),
+        name_(nullptr),
+        generic_(nullptr),
+        file_(nullptr),
+        debug_ext_(nullptr) {}
+
+  ~ScopedClassInfo() {
+    if (class_ != nullptr) {
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_));
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_));
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(file_));
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(debug_ext_));
+    }
+  }
+
+  bool Init() {
+    if (class_ == nullptr) {
+      name_ = const_cast<char*>("<NONE>");
+      generic_ = const_cast<char*>("<NONE>");
+      return true;
+    } else {
+      jvmtiError ret1 = jvmti_env_->GetSourceFileName(class_, &file_);
+      jvmtiError ret2 = jvmti_env_->GetSourceDebugExtension(class_, &debug_ext_);
+      return jvmti_env_->GetClassSignature(class_, &name_, &generic_) == JVMTI_ERROR_NONE &&
+          ret1 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY &&
+          ret1 != JVMTI_ERROR_INVALID_CLASS &&
+          ret2 != JVMTI_ERROR_MUST_POSSESS_CAPABILITY &&
+          ret2 != JVMTI_ERROR_INVALID_CLASS;
+    }
+  }
+
+  jclass GetClass() const {
+    return class_;
+  }
+  const char* GetName() const {
+    return name_;
+  }
+  // Generic type parameters, whatever is in the <> for a class
+  const char* GetGeneric() const {
+    return generic_;
+  }
+  const char* GetSourceDebugExtension() const {
+    if (debug_ext_ == nullptr) {
+      return "<UNKNOWN_SOURCE_DEBUG_EXTENSION>";
+    } else {
+      return debug_ext_;
+    }
+  }
+  const char* GetSourceFileName() const {
+    if (file_ == nullptr) {
+      return "<UNKNOWN_FILE>";
+    } else {
+      return file_;
+    }
+  }
+
+ private:
+  jvmtiEnv* jvmti_env_;
+  jclass class_;
+  char* name_;
+  char* generic_;
+  char* file_;
+  char* debug_ext_;
+};
+
+class ScopedMethodInfo {
+ public:
+  ScopedMethodInfo(jvmtiEnv* jvmti_env, JNIEnv* env, jmethodID method)
+      : jvmti_env_(jvmti_env),
+        env_(env),
+        method_(method),
+        declaring_class_(nullptr),
+        class_info_(nullptr),
+        name_(nullptr),
+        signature_(nullptr),
+        generic_(nullptr),
+        first_line_(-1) {}
+
+  ~ScopedMethodInfo() {
+    env_->DeleteLocalRef(declaring_class_);
+    jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(name_));
+    jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(signature_));
+    jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(generic_));
+  }
+
+  bool Init() {
+    if (jvmti_env_->GetMethodDeclaringClass(method_, &declaring_class_) != JVMTI_ERROR_NONE) {
+      return false;
+    }
+    class_info_.reset(new ScopedClassInfo(jvmti_env_, declaring_class_));
+    jint nlines;
+    jvmtiLineNumberEntry* lines;
+    jvmtiError err = jvmti_env_->GetLineNumberTable(method_, &nlines, &lines);
+    if (err == JVMTI_ERROR_NONE) {
+      if (nlines > 0) {
+        first_line_ = lines[0].line_number;
+      }
+      jvmti_env_->Deallocate(reinterpret_cast<unsigned char*>(lines));
+    } else if (err != JVMTI_ERROR_ABSENT_INFORMATION &&
+               err != JVMTI_ERROR_NATIVE_METHOD) {
+      return false;
+    }
+    return class_info_->Init() &&
+        (jvmti_env_->GetMethodName(method_, &name_, &signature_, &generic_) == JVMTI_ERROR_NONE);
+  }
+
+  const ScopedClassInfo& GetDeclaringClassInfo() const {
+    return *class_info_;
+  }
+
+  jclass GetDeclaringClass() const {
+    return declaring_class_;
+  }
+
+  const char* GetName() const {
+    return name_;
+  }
+
+  const char* GetSignature() const {
+    return signature_;
+  }
+
+  const char* GetGeneric() const {
+    return generic_;
+  }
+
+  jint GetFirstLine() const {
+    return first_line_;
+  }
+
+ private:
+  jvmtiEnv* jvmti_env_;
+  JNIEnv* env_;
+  jmethodID method_;
+  jclass declaring_class_;
+  std::unique_ptr<ScopedClassInfo> class_info_;
+  char* name_;
+  char* signature_;
+  char* generic_;
+  jint first_line_;
+
+  friend std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method);
+};
+
+std::ostream& operator<<(std::ostream& os, const ScopedMethodInfo* method) {
+  return os << *method;
+}
+
+std::ostream& operator<<(std::ostream& os, ScopedMethodInfo const& method) {
+  return os << method.GetDeclaringClassInfo().GetName() << "->" << method.GetName()
+            << method.GetSignature() << " (source: "
+            << method.GetDeclaringClassInfo().GetSourceFileName() << ":" << method.GetFirstLine()
+            << ")";
+}
+
+static void BreakpointCB(jvmtiEnv* jvmti_env,
+                         JNIEnv* env,
+                         jthread thread,
+                         jmethodID method,
+                         jlocation location) {
+  ScopedThreadInfo info(jvmti_env, env, thread);
+  ScopedMethodInfo method_info(jvmti_env, env, method);
+  if (!method_info.Init()) {
+    LOG(ERROR) << "Unable to get method info!";
+    return;
+  }
+  LOG(WARNING) << "Breakpoint at location: 0x" << std::setw(8) << std::setfill('0') << std::hex
+            << location << " in method " << method_info << " thread: " << info.GetName();
+}
+
+static std::string SubstrOf(const std::string& s, size_t start, size_t end) {
+  if (end == std::string::npos) {
+    end = s.size();
+  }
+  if (end == start) {
+    return "";
+  }
+  CHECK_GT(end, start) << "cannot get substr of " << s;
+  return s.substr(start, end - start);
+}
+
+static bool ParseSingleBreakpoint(const std::string& bp, /*out*/SingleBreakpointTarget* target) {
+  std::string option = bp;
+  if (option.empty() || option[0] != 'L' || option.find(';') == std::string::npos) {
+    LOG(ERROR) << option << " doesn't look like it has a class name";
+    return false;
+  }
+  target->class_name = SubstrOf(option, 1, option.find(';'));
+
+  option = SubstrOf(option, option.find(';') + 1, std::string::npos);
+  if (option.size() < 2 || option[0] != '-' || option[1] != '>') {
+    LOG(ERROR) << bp << " doesn't seem to indicate a method, expected ->";
+    return false;
+  }
+  option = SubstrOf(option, 2, std::string::npos);
+  size_t sig_start = option.find('(');
+  size_t loc_start = option.find('@');
+  if (option.empty() || sig_start == std::string::npos) {
+    LOG(ERROR) << bp << " doesn't seem to have a method sig!";
+    return false;
+  } else if (loc_start == std::string::npos ||
+             loc_start < sig_start ||
+             loc_start + 1 >= option.size()) {
+    LOG(ERROR) << bp << " doesn't seem to have a valid location!";
+    return false;
+  }
+  target->method_name = SubstrOf(option, 0, sig_start);
+  target->method_sig = SubstrOf(option, sig_start, loc_start);
+  target->location = std::stol(SubstrOf(option, loc_start + 1, std::string::npos));
+  return true;
+}
+
+static std::string RemoveLastOption(const std::string& op) {
+  if (op.find(',') == std::string::npos) {
+    return "";
+  } else {
+    return SubstrOf(op, op.find(',') + 1, std::string::npos);
+  }
+}
+
+// Fills targets with the breakpoints to add.
+// Lname/of/Klass;->methodName(Lsig/of/Method)Lreturn/Type;@location,<...>
+static bool ParseArgs(const std::string& start_options,
+                      /*out*/BreakpointTargets* targets) {
+  for (std::string options = start_options;
+       !options.empty();
+       options = RemoveLastOption(options)) {
+    SingleBreakpointTarget target;
+    std::string next = SubstrOf(options, 0, options.find(','));
+    if (!ParseSingleBreakpoint(next, /*out*/ &target)) {
+      LOG(ERROR) << "Unable to parse breakpoint from " << next;
+      return false;
+    }
+    targets->bps.push_back(target);
+  }
+  return true;
+}
+
+enum class StartType {
+  OnAttach, OnLoad,
+};
+
+static jint AgentStart(StartType start,
+                       JavaVM* vm,
+                       char* options,
+                       void* reserved ATTRIBUTE_UNUSED) {
+  jvmtiEnv* jvmti = nullptr;
+  jvmtiError error = JVMTI_ERROR_NONE;
+  {
+    jint res = 0;
+    res = vm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1);
+
+    if (res != JNI_OK || jvmti == nullptr) {
+      LOG(ERROR) << "Unable to access JVMTI, error code " << res;
+      return JNI_ERR;
+    }
+  }
+
+  void* bp_target_mem = nullptr;
+  error = jvmti->Allocate(sizeof(BreakpointTargets),
+                          reinterpret_cast<unsigned char**>(&bp_target_mem));
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to alloc memory for breakpoint target data";
+    return JNI_ERR;
+  }
+
+  BreakpointTargets* data = new(bp_target_mem) BreakpointTargets;
+  error = jvmti->SetEnvironmentLocalStorage(data);
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to set local storage";
+    return JNI_ERR;
+  }
+
+  if (!ParseArgs(options, /*out*/data)) {
+    LOG(ERROR) << "failed to parse breakpoint list!";
+    return JNI_ERR;
+  }
+
+  jvmtiCapabilities caps {};  // NOLINT [readability/braces]
+  caps.can_generate_breakpoint_events = JNI_TRUE;
+  caps.can_get_line_numbers           = JNI_TRUE;
+  caps.can_get_source_file_name       = JNI_TRUE;
+  caps.can_get_source_debug_extension = JNI_TRUE;
+  error = jvmti->AddCapabilities(&caps);
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to set caps";
+    return JNI_ERR;
+  }
+
+  jvmtiEventCallbacks callbacks {};  // NOLINT [readability/braces]
+  callbacks.Breakpoint = &BreakpointCB;
+  callbacks.VMInit = &VMInitCB;
+
+  error = jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
+
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to set event callbacks.";
+    return JNI_ERR;
+  }
+
+  error = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
+                                          JVMTI_EVENT_BREAKPOINT,
+                                          nullptr /* all threads */);
+  if (error != JVMTI_ERROR_NONE) {
+    LOG(ERROR) << "Unable to enable breakpoint event";
+    return JNI_ERR;
+  }
+  if (start == StartType::OnAttach) {
+    JNIEnv* env = nullptr;
+    jint res = 0;
+    res = vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_2);
+    if (res != JNI_OK || env == nullptr) {
+      LOG(ERROR) << "Unable to get jnienv";
+      return JNI_ERR;
+    }
+    VMInitCB(jvmti, env, nullptr);
+  } else {
+    error = jvmti->SetEventNotificationMode(JVMTI_ENABLE,
+                                            JVMTI_EVENT_VM_INIT,
+                                            nullptr /* all threads */);
+    if (error != JVMTI_ERROR_NONE) {
+      LOG(ERROR) << "Unable to set event vminit";
+      return JNI_ERR;
+    }
+  }
+  return JNI_OK;
+}
+
+// Late attachment (e.g. 'am attach-agent').
+extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) {
+  return AgentStart(StartType::OnAttach, vm, options, reserved);
+}
+
+// Early attachment
+extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) {
+  return AgentStart(StartType::OnLoad, jvm, options, reserved);
+}
+
+}  // namespace breakpoint_logger
+