Kavi Gupta | 2d84ae7 | 2019-06-11 09:19:35 -0700 | [diff] [blame^] | 1 | // Copyright (C) 2018 The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | // |
| 15 | |
| 16 | #include <android-base/logging.h> |
| 17 | #include <jni.h> |
| 18 | #include <jvmti.h> |
| 19 | #include <string.h> |
| 20 | |
| 21 | #include <atomic> |
| 22 | #include <ctime> |
| 23 | #include <fstream> |
| 24 | #include <iomanip> |
| 25 | #include <iostream> |
| 26 | #include <istream> |
| 27 | #include <memory> |
| 28 | #include <sstream> |
| 29 | #include <string> |
| 30 | #include <vector> |
| 31 | |
| 32 | using std::get; |
| 33 | using std::tuple; |
| 34 | using std::chrono::system_clock; |
| 35 | |
| 36 | namespace dump_coverage { |
| 37 | |
| 38 | #define CHECK_JVMTI(x) CHECK_EQ((x), JVMTI_ERROR_NONE) |
| 39 | #define CHECK_NOTNULL(x) CHECK((x) != nullptr) |
| 40 | #define CHECK_NO_EXCEPTION(env) CHECK(!(env)->ExceptionCheck()); |
| 41 | |
| 42 | static JavaVM* java_vm = nullptr; |
| 43 | |
| 44 | // Get the current JNI environment. |
| 45 | static JNIEnv* GetJNIEnv() { |
| 46 | JNIEnv* env = nullptr; |
| 47 | CHECK_EQ(java_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6), |
| 48 | JNI_OK); |
| 49 | return env; |
| 50 | } |
| 51 | |
| 52 | // Get the JaCoCo Agent class and an instance of the class, given a JNI |
| 53 | // environment. |
| 54 | // Will crash if the Agent isn't found or if any Java Exception occurs. |
| 55 | static tuple<jclass, jobject> GetJavaAgent(JNIEnv* env) { |
| 56 | jclass java_agent_class = |
| 57 | env->FindClass("org/jacoco/agent/rt/internal/Agent"); |
| 58 | CHECK_NOTNULL(java_agent_class); |
| 59 | |
| 60 | jmethodID java_agent_get_instance = |
| 61 | env->GetStaticMethodID(java_agent_class, "getInstance", |
| 62 | "()Lorg/jacoco/agent/rt/internal/Agent;"); |
| 63 | CHECK_NOTNULL(java_agent_get_instance); |
| 64 | |
| 65 | jobject java_agent_instance = |
| 66 | env->CallStaticObjectMethod(java_agent_class, java_agent_get_instance); |
| 67 | CHECK_NO_EXCEPTION(env); |
| 68 | CHECK_NOTNULL(java_agent_instance); |
| 69 | |
| 70 | return tuple(java_agent_class, java_agent_instance); |
| 71 | } |
| 72 | |
| 73 | // Runs equivalent of Agent.getInstance().getExecutionData(false) and returns |
| 74 | // the result. |
| 75 | // Will crash if the Agent isn't found or if any Java Exception occurs. |
| 76 | static jbyteArray GetExecutionData(JNIEnv* env) { |
| 77 | auto java_agent = GetJavaAgent(env); |
| 78 | jmethodID java_agent_get_execution_data = |
| 79 | env->GetMethodID(get<0>(java_agent), "getExecutionData", "(Z)[B"); |
| 80 | CHECK_NO_EXCEPTION(env); |
| 81 | CHECK_NOTNULL(java_agent_get_execution_data); |
| 82 | |
| 83 | jbyteArray java_result_array = (jbyteArray)env->CallObjectMethod( |
| 84 | get<1>(java_agent), java_agent_get_execution_data, false); |
| 85 | CHECK_NO_EXCEPTION(env); |
| 86 | |
| 87 | return java_result_array; |
| 88 | } |
| 89 | |
| 90 | // Gets the filename to write execution data to |
| 91 | // dirname: the directory in which to place the file |
| 92 | // outputs <dirname>/YYYY-MM-DD-HH-MM-SS.SSS.exec |
| 93 | static std::string GetFilename(const std::string& dirname) { |
| 94 | system_clock::time_point time_point = system_clock::now(); |
| 95 | auto seconds = std::chrono::time_point_cast<std::chrono::seconds>(time_point); |
| 96 | auto fractional_time = time_point - seconds; |
| 97 | auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(fractional_time); |
| 98 | |
| 99 | std::time_t time = system_clock::to_time_t(time_point); |
| 100 | auto tm = *std::gmtime(&time); |
| 101 | |
| 102 | std::ostringstream oss; |
| 103 | oss |
| 104 | << dirname |
| 105 | << "/" |
| 106 | << std::put_time(&tm, "%Y-%m-%d-%H-%M-%S.") |
| 107 | << std::setfill('0') << std::setw(3) << millis.count() |
| 108 | << ".ec"; |
| 109 | return oss.str(); |
| 110 | } |
| 111 | |
| 112 | // Writes the execution data to a file |
| 113 | // data, length: represent the data, as a sequence of bytes |
| 114 | // dirname: directory name to contain the file |
| 115 | // returns JNI_ERR if there is an error in writing the file, otherwise JNI_OK. |
| 116 | static jint WriteFile(const char* data, int length, const std::string& dirname) { |
| 117 | auto filename = GetFilename(dirname); |
| 118 | |
| 119 | LOG(INFO) << "Writing file of length " << length << " to '" << filename |
| 120 | << "'"; |
| 121 | std::ofstream file(filename, std::ios::binary); |
| 122 | |
| 123 | if (!file.is_open()) { |
| 124 | LOG(ERROR) << "Could not open file: '" << filename << "'"; |
| 125 | return JNI_ERR; |
| 126 | } |
| 127 | file.write(data, length); |
| 128 | file.close(); |
| 129 | |
| 130 | if (!file) { |
| 131 | LOG(ERROR) << "I/O error in reading file"; |
| 132 | return JNI_ERR; |
| 133 | } |
| 134 | |
| 135 | LOG(INFO) << "Done writing file"; |
| 136 | return JNI_OK; |
| 137 | } |
| 138 | |
| 139 | // Grabs execution data and writes it to a file |
| 140 | // dirname: directory name to contain the file |
| 141 | // returns JNI_ERR if there is an error writing the file. |
| 142 | // Will crash if the Agent isn't found or if any Java Exception occurs. |
| 143 | static jint Dump(const std::string& dirname) { |
| 144 | LOG(INFO) << "Dumping file"; |
| 145 | |
| 146 | JNIEnv* env = GetJNIEnv(); |
| 147 | jbyteArray java_result_array = GetExecutionData(env); |
| 148 | CHECK_NOTNULL(java_result_array); |
| 149 | |
| 150 | jbyte* result_ptr = env->GetByteArrayElements(java_result_array, 0); |
| 151 | CHECK_NOTNULL(result_ptr); |
| 152 | |
| 153 | int result_len = env->GetArrayLength(java_result_array); |
| 154 | |
| 155 | return WriteFile((const char*) result_ptr, result_len, dirname); |
| 156 | } |
| 157 | |
| 158 | // Resets execution data, performing the equivalent of |
| 159 | // Agent.getInstance().reset(); |
| 160 | // args: should be empty |
| 161 | // returns JNI_ERR if the arguments are invalid. |
| 162 | // Will crash if the Agent isn't found or if any Java Exception occurs. |
| 163 | static jint Reset(const std::string& args) { |
| 164 | if (args != "") { |
| 165 | LOG(ERROR) << "reset takes no arguments, but received '" << args << "'"; |
| 166 | return JNI_ERR; |
| 167 | } |
| 168 | |
| 169 | JNIEnv* env = GetJNIEnv(); |
| 170 | auto java_agent = GetJavaAgent(env); |
| 171 | |
| 172 | jmethodID java_agent_reset = |
| 173 | env->GetMethodID(get<0>(java_agent), "reset", "()V"); |
| 174 | CHECK_NOTNULL(java_agent_reset); |
| 175 | |
| 176 | env->CallVoidMethod(get<1>(java_agent), java_agent_reset); |
| 177 | CHECK_NO_EXCEPTION(env); |
| 178 | return JNI_OK; |
| 179 | } |
| 180 | |
| 181 | // Given a string of the form "<a>:<b>" returns (<a>, <b>). |
| 182 | // Given a string <a> that doesn't contain a colon, returns (<a>, ""). |
| 183 | static tuple<std::string, std::string> SplitOnColon(const std::string& options) { |
| 184 | size_t loc_delim = options.find(':'); |
| 185 | std::string command, args; |
| 186 | |
| 187 | if (loc_delim == std::string::npos) { |
| 188 | command = options; |
| 189 | } else { |
| 190 | command = options.substr(0, loc_delim); |
| 191 | args = options.substr(loc_delim + 1, options.length()); |
| 192 | } |
| 193 | return tuple(command, args); |
| 194 | } |
| 195 | |
| 196 | // Parses and executes a command specified by options of the form |
| 197 | // "<command>:<args>" where <command> is either "dump" or "reset". |
| 198 | static jint ParseOptionsAndExecuteCommand(const std::string& options) { |
| 199 | auto split = SplitOnColon(options); |
| 200 | auto command = get<0>(split), args = get<1>(split); |
| 201 | |
| 202 | LOG(INFO) << "command: '" << command << "' args: '" << args << "'"; |
| 203 | |
| 204 | if (command == "dump") { |
| 205 | return Dump(args); |
| 206 | } |
| 207 | |
| 208 | if (command == "reset") { |
| 209 | return Reset(args); |
| 210 | } |
| 211 | |
| 212 | LOG(ERROR) << "Invalid command: expected 'dump' or 'reset' but was '" |
| 213 | << command << "'"; |
| 214 | return JNI_ERR; |
| 215 | } |
| 216 | |
| 217 | static jint AgentStart(JavaVM* vm, char* options) { |
| 218 | android::base::InitLogging(/* argv= */ nullptr); |
| 219 | java_vm = vm; |
| 220 | |
| 221 | return ParseOptionsAndExecuteCommand(options); |
| 222 | } |
| 223 | |
| 224 | // Late attachment (e.g. 'am attach-agent'). |
| 225 | extern "C" JNIEXPORT jint JNICALL |
| 226 | Agent_OnAttach(JavaVM* vm, char* options, void* reserved ATTRIBUTE_UNUSED) { |
| 227 | return AgentStart(vm, options); |
| 228 | } |
| 229 | |
| 230 | // Early attachment. |
| 231 | extern "C" JNIEXPORT jint JNICALL |
| 232 | Agent_OnLoad(JavaVM* jvm ATTRIBUTE_UNUSED, char* options ATTRIBUTE_UNUSED, void* reserved ATTRIBUTE_UNUSED) { |
| 233 | LOG(ERROR) |
| 234 | << "The dumpcoverage agent will not work on load," |
| 235 | << " as it does not have access to the runtime."; |
| 236 | return JNI_ERR; |
| 237 | } |
| 238 | |
| 239 | } // namespace dump_coverage |