| // Copyright (C) 2018 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 <jni.h> | |
| #include <jvmti.h> | |
| #include <string.h> | |
| #include <fstream> | |
| using std::get; | |
| using std::tuple; | |
| namespace dump_coverage { | |
| #define CHECK_JVMTI(x) CHECK_EQ((x), JVMTI_ERROR_NONE) | |
| #define CHECK_NOTNULL(x) CHECK((x) != nullptr) | |
| #define CHECK_NO_EXCEPTION(env) CHECK(!(env)->ExceptionCheck()); | |
| static JavaVM* java_vm = nullptr; | |
| // Get the current JNI environment. | |
| static JNIEnv* GetJNIEnv() { | |
| JNIEnv* env = nullptr; | |
| CHECK_EQ(java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6), | |
| JNI_OK); | |
| return env; | |
| } | |
| // Get the JaCoCo Agent class and an instance of the class, given a JNI | |
| // environment. | |
| // Will crash if the Agent isn't found or if any Java Exception occurs. | |
| static tuple<jclass, jobject> GetJavaAgent(JNIEnv* env) { | |
| jclass java_agent_class = | |
| env->FindClass("org/jacoco/agent/rt/internal/Agent"); | |
| CHECK_NOTNULL(java_agent_class); | |
| jmethodID java_agent_get_instance = | |
| env->GetStaticMethodID(java_agent_class, "getInstance", | |
| "()Lorg/jacoco/agent/rt/internal/Agent;"); | |
| CHECK_NOTNULL(java_agent_get_instance); | |
| jobject java_agent_instance = | |
| env->CallStaticObjectMethod(java_agent_class, java_agent_get_instance); | |
| CHECK_NO_EXCEPTION(env); | |
| CHECK_NOTNULL(java_agent_instance); | |
| return tuple(java_agent_class, java_agent_instance); | |
| } | |
| // Runs equivalent of Agent.getInstance().getExecutionData(false) and returns | |
| // the result. | |
| // Will crash if the Agent isn't found or if any Java Exception occurs. | |
| static jbyteArray GetExecutionData(JNIEnv* env) { | |
| auto java_agent = GetJavaAgent(env); | |
| jmethodID java_agent_get_execution_data = | |
| env->GetMethodID(get<0>(java_agent), "getExecutionData", "(Z)[B"); | |
| CHECK_NO_EXCEPTION(env); | |
| CHECK_NOTNULL(java_agent_get_execution_data); | |
| jbyteArray java_result_array = (jbyteArray)env->CallObjectMethod( | |
| get<1>(java_agent), java_agent_get_execution_data, false); | |
| CHECK_NO_EXCEPTION(env); | |
| return java_result_array; | |
| } | |
| // Writes the execution data to a file. | |
| // data, length: represent the data, as a sequence of bytes. | |
| // filename: file to write coverage data to. | |
| // returns JNI_ERR if there is an error in writing the file, otherwise JNI_OK. | |
| static jint WriteFile(const char* data, int length, const std::string& filename) { | |
| LOG(INFO) << "Writing file of length " << length << " to '" << filename | |
| << "'"; | |
| std::ofstream file(filename, std::ios::binary); | |
| if (!file.is_open()) { | |
| LOG(ERROR) << "Could not open file: '" << filename << "'"; | |
| return JNI_ERR; | |
| } | |
| file.write(data, length); | |
| file.close(); | |
| if (!file) { | |
| LOG(ERROR) << "I/O error in reading file"; | |
| return JNI_ERR; | |
| } | |
| LOG(INFO) << "Done writing file"; | |
| return JNI_OK; | |
| } | |
| // Grabs execution data and writes it to a file. | |
| // filename: file to write coverage data to. | |
| // returns JNI_ERR if there is an error writing the file. | |
| // Will crash if the Agent isn't found or if any Java Exception occurs. | |
| static jint Dump(const std::string& filename) { | |
| LOG(INFO) << "Dumping file"; | |
| JNIEnv* env = GetJNIEnv(); | |
| jbyteArray java_result_array = GetExecutionData(env); | |
| CHECK_NOTNULL(java_result_array); | |
| jbyte* result_ptr = env->GetByteArrayElements(java_result_array, 0); | |
| CHECK_NOTNULL(result_ptr); | |
| int result_len = env->GetArrayLength(java_result_array); | |
| return WriteFile((const char*) result_ptr, result_len, filename); | |
| } | |
| // Resets execution data, performing the equivalent of | |
| // Agent.getInstance().reset(); | |
| // args: should be empty. | |
| // returns JNI_ERR if the arguments are invalid. | |
| // Will crash if the Agent isn't found or if any Java Exception occurs. | |
| static jint Reset(const std::string& args) { | |
| if (args != "") { | |
| LOG(ERROR) << "reset takes no arguments, but received '" << args << "'"; | |
| return JNI_ERR; | |
| } | |
| JNIEnv* env = GetJNIEnv(); | |
| auto java_agent = GetJavaAgent(env); | |
| jmethodID java_agent_reset = | |
| env->GetMethodID(get<0>(java_agent), "reset", "()V"); | |
| CHECK_NOTNULL(java_agent_reset); | |
| env->CallVoidMethod(get<1>(java_agent), java_agent_reset); | |
| CHECK_NO_EXCEPTION(env); | |
| return JNI_OK; | |
| } | |
| // Given a string of the form "<a>:<b>" returns (<a>, <b>). | |
| // Given a string <a> that doesn't contain a colon, returns (<a>, ""). | |
| static tuple<std::string, std::string> SplitOnColon(const std::string& options) { | |
| size_t loc_delim = options.find(':'); | |
| std::string command, args; | |
| if (loc_delim == std::string::npos) { | |
| command = options; | |
| } else { | |
| command = options.substr(0, loc_delim); | |
| args = options.substr(loc_delim + 1, options.length()); | |
| } | |
| return tuple(command, args); | |
| } | |
| // Parses and executes a command specified by options of the form | |
| // "<command>:<args>" where <command> is either "dump" or "reset". | |
| static jint ParseOptionsAndExecuteCommand(const std::string& options) { | |
| auto split = SplitOnColon(options); | |
| auto command = get<0>(split), args = get<1>(split); | |
| LOG(INFO) << "command: '" << command << "' args: '" << args << "'"; | |
| if (command == "dump") { | |
| return Dump(args); | |
| } | |
| if (command == "reset") { | |
| return Reset(args); | |
| } | |
| LOG(ERROR) << "Invalid command: expected 'dump' or 'reset' but was '" | |
| << command << "'"; | |
| return JNI_ERR; | |
| } | |
| static jint AgentStart(JavaVM* vm, char* options) { | |
| android::base::InitLogging(/* argv= */ nullptr); | |
| java_vm = vm; | |
| return ParseOptionsAndExecuteCommand(options); | |
| } | |
| // Late attachment (e.g. 'am attach-agent'). | |
| extern "C" JNIEXPORT jint JNICALL | |
| Agent_OnAttach(JavaVM* vm, char* options, void* reserved ATTRIBUTE_UNUSED) { | |
| return AgentStart(vm, options); | |
| } | |
| // Early attachment. | |
| extern "C" JNIEXPORT jint JNICALL | |
| Agent_OnLoad(JavaVM* jvm ATTRIBUTE_UNUSED, char* options ATTRIBUTE_UNUSED, void* reserved ATTRIBUTE_UNUSED) { | |
| LOG(ERROR) | |
| << "The dumpcoverage agent will not work on load," | |
| << " as it does not have access to the runtime."; | |
| return JNI_ERR; | |
| } | |
| } // namespace dump_coverage |