diff --git a/test-app/app/src/main/assets/app/tests/exceptionHandlingTests.js b/test-app/app/src/main/assets/app/tests/exceptionHandlingTests.js index bf9aed120..0c5f8cd9c 100644 --- a/test-app/app/src/main/assets/app/tests/exceptionHandlingTests.js +++ b/test-app/app/src/main/assets/app/tests/exceptionHandlingTests.js @@ -319,19 +319,4 @@ describe("Tests exception handling ", function () { expect(errMsg).toContain("SyntaxError: Unexpected token 'class'"); expect(errMsg).toContain("File: (file:///data/data/com.tns.testapplication/files/app/tests/syntaxErrors.js:3:4)"); }); - - // run this test only for API level bigger than 25 as we have handling there - if(android.os.Build.VERSION.SDK_INT > 25 && android.os.Build.CPU_ABI != "x86" && android.os.Build.CPU_ABI != "x86_64") { - xit("Should handle SIGABRT and throw a NativeScript exception when incorrectly calling JNI methods", function () { - let myClassInstance = new com.tns.tests.MyTestBaseClass3(); - // public void callMeWithAString(java.lang.String[] stringArr, Runnable arbitraryInterface) - try { - myClassInstance.callMeWithAString("stringVal", new java.lang.Runnable({ run: () => {} })) - } catch (e) { - android.util.Log.d("~~~~~", "~~~~~~~~ " + e.toString()); - - expect(e.toString()).toContain("SIGABRT"); - } - }); - } }); \ No newline at end of file diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index a092d3058..0c90d43b8 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -167,6 +167,7 @@ add_library( src/main/cpp/CallbackHandlers.cpp src/main/cpp/ConcurrentQueue.cpp src/main/cpp/Constants.cpp + src/main/cpp/CrashBreadcrumbs.cpp src/main/cpp/DirectBuffer.cpp src/main/cpp/ErrorEvents.cpp src/main/cpp/EventLoop.cpp diff --git a/test-app/runtime/src/main/cpp/CrashBreadcrumbs.cpp b/test-app/runtime/src/main/cpp/CrashBreadcrumbs.cpp new file mode 100644 index 000000000..1e0c4a3b5 --- /dev/null +++ b/test-app/runtime/src/main/cpp/CrashBreadcrumbs.cpp @@ -0,0 +1,303 @@ +#include "CrashBreadcrumbs.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr size_t kMaxRuntimes = 16; +constexpr size_t kFieldMax = 160; +constexpr size_t kBufferMax = 8192; +constexpr size_t kHeaderMax = 128; + +struct Slot { + bool used; + bool isWorker; + int runtimeId; + int tid; + char script[kFieldMax]; + char module[kFieldMax]; +}; + +Slot g_slots[kMaxRuntimes]; +std::mutex g_mutex; + +/* + * Rendered in two buffers alternately, so the signal handler never reads the + * one a running thread is part way through writing. + */ +char g_rendered[2][kBufferMax]; +size_t g_renderedLength[2]; +std::atomic g_active{-1}; + +std::atomic g_storeFd{-1}; +std::atomic_flag g_recorded = ATOMIC_FLAG_INIT; +struct sigaction g_previous[NSIG]; + +thread_local Slot* t_slot = nullptr; + +int CurrentTid() { return static_cast(syscall(__NR_gettid)); } + +void CopyField(char* dst, const char* src) { + if (src == nullptr) { + dst[0] = '\0'; + return; + } + size_t length = strlen(src); + if (length < kFieldMax) { + memcpy(dst, src, length + 1); + return; + } + // Keep the tail: the file name identifies a module, the leading directories + // are shared by every module in the app. + memcpy(dst, "...", 3); + memcpy(dst + 3, src + length - (kFieldMax - 4), kFieldMax - 4); + dst[kFieldMax - 1] = '\0'; +} + +void Append(char* out, size_t& length, const char* format, ...) + __attribute__((format(printf, 3, 4))); + +void Append(char* out, size_t& length, const char* format, ...) { + if (length >= kBufferMax) { + return; + } + va_list args; + va_start(args, format); + int written = vsnprintf(out + length, kBufferMax - length, format, args); + va_end(args); + if (written > 0) { + length += static_cast(written); + if (length > kBufferMax - 1) { + length = kBufferMax - 1; + } + } +} + +void RenderLocked() { + // Once a crash is recorded the handler may be reading either buffer; a + // second flip after that point would rewrite the one it is copying out. + if (g_recorded.test(std::memory_order_acquire)) { + return; + } + int next = g_active.load(std::memory_order_relaxed) == 0 ? 1 : 0; + char* out = g_rendered[next]; + size_t length = 0; + + Append(out, length, "NativeScript runtime state (pid %d):\n", getpid()); + for (const Slot& slot : g_slots) { + if (!slot.used) { + continue; + } + Append(out, length, " runtime=%d tid=%d %s", slot.runtimeId, slot.tid, + slot.isWorker ? "worker" : "main"); + if (slot.script[0] != '\0') { + Append(out, length, " script=%s", slot.script); + } + Append(out, length, " module=%s\n", + slot.module[0] != '\0' ? slot.module : ""); + } + + g_renderedLength[next] = length; + g_active.store(next, std::memory_order_release); +} + +Slot* FindLocked(int runtimeId) { + for (Slot& slot : g_slots) { + if (slot.used && slot.runtimeId == runtimeId) { + return &slot; + } + } + return nullptr; +} + +/* Async-signal-safe integer formatting; snprintf is not usable here. */ +void AppendRaw(char* out, size_t capacity, size_t& length, const char* text) { + while (*text != '\0' && length < capacity) { + out[length++] = *text++; + } +} + +void AppendRawInt(char* out, size_t capacity, size_t& length, int value) { + char digits[16]; + size_t count = 0; + unsigned int magnitude = static_cast(value); + do { + digits[count++] = static_cast('0' + magnitude % 10); + magnitude /= 10; + } while (magnitude != 0 && count < sizeof(digits)); + while (count > 0 && length < capacity) { + out[length++] = digits[--count]; + } +} + +void Handler(int signalNumber, siginfo_t* info, void* context) { + // Only the first thread to fault records; the rest are already doomed. + if (!g_recorded.test_and_set()) { + int fd = g_storeFd.load(std::memory_order_acquire); + if (fd >= 0) { + char header[kHeaderMax]; + size_t length = 0; + AppendRaw(header, sizeof(header), length, "fatal signal "); + AppendRawInt(header, sizeof(header), length, signalNumber); + AppendRaw(header, sizeof(header), length, " on tid "); + AppendRawInt(header, sizeof(header), length, CurrentTid()); + AppendRaw(header, sizeof(header), length, "\n"); + + ssize_t written = pwrite(fd, header, length, 0); + int active = g_active.load(std::memory_order_acquire); + if (written > 0 && active >= 0) { + pwrite(fd, g_rendered[active], g_renderedLength[active], written); + } + } + } + + /* + * Hand the signal to whoever owned it before us -- on Android that is + * debuggerd, which writes the tombstone. + * + * A signal the kernel raised for a real fault arrives again on its own once + * this returns and the faulting instruction re-executes, so debuggerd is + * entered with the kernel's original siginfo instead of anything + * synthesised here. One that was delivered by abort() or kill() (si_code + * <= 0) will not come back, so it has to be re-raised explicitly. + */ + sigaction(signalNumber, &g_previous[signalNumber], nullptr); + if (info == nullptr || info->si_code <= 0) { + raise(signalNumber); + } +} + +} // namespace + +namespace tns { + +void CrashBreadcrumbs::Install() { + static std::once_flag once; + std::call_once(once, [] { + struct sigaction action = {}; + action.sa_sigaction = Handler; + // SA_ONSTACK matters for a stack-overflow SIGSEGV, which has no room left + // on the faulting stack to run a handler. bionic already gives every + // thread an alternate signal stack, so the flag is all that is needed. + action.sa_flags = SA_SIGINFO | SA_ONSTACK; + sigemptyset(&action.sa_mask); + for (int signalNumber : {SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE}) { + sigaction(signalNumber, &action, &g_previous[signalNumber]); + } + }); +} + +void CrashBreadcrumbs::OpenStore(const std::string& filesRoot) { + static std::once_flag once; + std::call_once(once, [&filesRoot] { + std::string path = filesRoot + "/.ns-crash-breadcrumb"; + int fd = open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600); + if (fd < 0) { + return; + } + + char previous[kBufferMax + kHeaderMax]; + ssize_t length = read(fd, previous, sizeof(previous) - 1); + if (length > 0) { + previous[length] = '\0'; + // Deliberately not ANDROID_LOG_FATAL: liblog feeds a fatal record to + // android_set_abort_message, and bionic keeps the first message it is + // given for the life of the process. Claiming that slot here would + // describe the *previous* process in this one's tombstone, and would + // shut out the abort message libc or ART writes for the real fault. + __android_log_print( + ANDROID_LOG_ERROR, "TNS.Native", + "The previous process was killed by a fatal signal. Runtime state " + "recorded at that moment (match tid against the tombstone in " + "/data/tombstones):\n%s", + previous); + ftruncate(fd, 0); + } + + g_storeFd.store(fd, std::memory_order_release); + }); +} + +void CrashBreadcrumbs::RegisterRuntime(int runtimeId) { + std::lock_guard lock(g_mutex); + Slot* slot = FindLocked(runtimeId); + if (slot == nullptr) { + for (Slot& candidate : g_slots) { + if (!candidate.used) { + slot = &candidate; + break; + } + } + } + if (slot == nullptr) { + // Table full. Keep the runtimes already tracked rather than evicting one. + return; + } + + slot->used = true; + slot->isWorker = false; + slot->runtimeId = runtimeId; + slot->tid = CurrentTid(); + slot->script[0] = '\0'; + slot->module[0] = '\0'; + t_slot = slot; + RenderLocked(); +} + +void CrashBreadcrumbs::UnregisterRuntime(int runtimeId) { + std::lock_guard lock(g_mutex); + Slot* slot = FindLocked(runtimeId); + if (slot == nullptr) { + return; + } + if (t_slot == slot) { + t_slot = nullptr; + } + slot->used = false; + RenderLocked(); +} + +void CrashBreadcrumbs::SetWorkerScript(int runtimeId, const char* script) { + std::lock_guard lock(g_mutex); + Slot* slot = FindLocked(runtimeId); + if (slot == nullptr) { + return; + } + slot->isWorker = true; + CopyField(slot->script, script); + RenderLocked(); +} + +CrashBreadcrumbs::ModuleScope::ModuleScope(const char* modulePath) { + Slot* slot = t_slot; + if (slot == nullptr) { + return; + } + std::lock_guard lock(g_mutex); + previous_ = slot->module; + restore_ = true; + CopyField(slot->module, modulePath); + RenderLocked(); +} + +CrashBreadcrumbs::ModuleScope::~ModuleScope() { + Slot* slot = t_slot; + if (!restore_ || slot == nullptr) { + return; + } + std::lock_guard lock(g_mutex); + CopyField(slot->module, previous_.c_str()); + RenderLocked(); +} + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/CrashBreadcrumbs.h b/test-app/runtime/src/main/cpp/CrashBreadcrumbs.h new file mode 100644 index 000000000..deacfa95a --- /dev/null +++ b/test-app/runtime/src/main/cpp/CrashBreadcrumbs.h @@ -0,0 +1,59 @@ +#ifndef CRASHBREADCRUMBS_H_ +#define CRASHBREADCRUMBS_H_ + +#include + +namespace tns { + +/* + * Records what each runtime thread was doing, so a process killed by a fatal + * signal leaves behind more than a native backtrace. + * + * The state is rendered into a plain byte buffer as it changes, on ordinary + * threads. At crash time the only work left is a write(2) of that buffer, + * which is one of the few calls POSIX permits from a signal handler -- + * anything that allocates, takes a lock or formats has already happened. + */ +class CrashBreadcrumbs { + public: + /* + * Installs SIGSEGV/SIGABRT/SIGBUS/SIGILL/SIGFPE handlers that record the + * breadcrumb and then hand the signal back to the handler installed before + * them, so debuggerd still writes the tombstone. Idempotent. + */ + static void Install(); + + /* + * Points the store at the app's files directory and reports whatever a + * previous process left behind. Idempotent, so every runtime may call it. + */ + static void OpenStore(const std::string& filesRoot); + + /* Binds the calling thread to a runtime for that runtime's lifetime. */ + static void RegisterRuntime(int runtimeId); + static void UnregisterRuntime(int runtimeId); + + /* Marks a registered runtime as a worker started from `script`. */ + static void SetWorkerScript(int runtimeId, const char* script); + + /* + * Records the module the calling runtime is executing for the lifetime of + * the scope. Module loads nest (`require` inside a module body), so the + * enclosing module is restored on destruction, on throw paths included. + */ + class ModuleScope { + public: + explicit ModuleScope(const char* modulePath); + ~ModuleScope(); + ModuleScope(const ModuleScope&) = delete; + ModuleScope& operator=(const ModuleScope&) = delete; + + private: + std::string previous_; + bool restore_ = false; + }; +}; + +} // namespace tns + +#endif /* CRASHBREADCRUMBS_H_ */ diff --git a/test-app/runtime/src/main/cpp/ModuleInternal.cpp b/test-app/runtime/src/main/cpp/ModuleInternal.cpp index be8ad4024..16823c48e 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternal.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternal.cpp @@ -13,6 +13,7 @@ #include "V8GlobalHelpers.h" #include "NativeScriptAssert.h" #include "Constants.h" +#include "CrashBreadcrumbs.h" #include "NativeScriptException.h" #include "NsBuiltinModules.h" #include "napi/NapiModules.h" @@ -363,6 +364,7 @@ Local ModuleInternal::LoadImpl(Isolate* isolate, const string& moduleNam Local ModuleInternal::LoadModule(Isolate* isolate, const string& modulePath, const string& moduleCacheKey) { string frameName("LoadModule " + modulePath); tns::instrumentation::Frame frame(frameName); + CrashBreadcrumbs::ModuleScope moduleBreadcrumb(modulePath.c_str()); Local result; auto context = isolate->GetCurrentContext(); diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index c218dcdf0..f82890693 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -15,6 +14,7 @@ #include "BuiltinLoader.h" #include "CallbackHandlers.h" #include "Constants.h" +#include "CrashBreadcrumbs.h" #include "ErrorEvents.h" #include "Events.h" #include "File.h" @@ -62,26 +62,6 @@ using namespace tns; bool tns::LogEnabled = true; SimpleAllocator g_allocator; -void SIG_handler(int sigNumber) { - stringstream msg; - msg << "JNI Exception occurred ("; - switch (sigNumber) { - case SIGABRT: - msg << "SIGABRT"; - break; - case SIGSEGV: - msg << "SIGSEGV"; - break; - default: - // Shouldn't happen, but for completeness - msg << "Signal #" << sigNumber; - break; - } - msg << ").\n=======\nCheck the 'adb logcat' for additional information about " - "the error.\n=======\n"; - throw NativeScriptException(msg.str()); -} - void LogAndAbortUncaught() { try { throw; // rethrow the current unknown @@ -113,15 +93,8 @@ void Runtime::Init(JavaVM* vm, void* reserved) { JEnv::Init(s_jvm); } - // handle SIGABRT/SIGSEGV only on API level > 20 as the handling is not so - // efficient in older versions - if (m_androidVersion > 20) { - struct sigaction action = {}; - action.sa_handler = SIG_handler; - sigemptyset(&action.sa_mask); - sigaction(SIGABRT, &action, NULL); - sigaction(SIGSEGV, &action, NULL); - } + CrashBreadcrumbs::Install(); + // Set terminate handler for uncaught exceptions std::set_terminate(LogAndAbortUncaught); } @@ -257,6 +230,11 @@ void Runtime::Init(JNIEnv* env, jstring filesPath, jstring nativeLibDir, auto packageNameStr = ArgConverter::jstringToString(packageName); auto callingDirStr = ArgConverter::jstringToString(callingDir); + // Runs on this runtime's own thread, for the main runtime and for workers + // alike, so the calling thread is the one the breadcrumb should name. + CrashBreadcrumbs::OpenStore(filesRoot); + CrashBreadcrumbs::RegisterRuntime(m_id); + Constants::APP_ROOT_FOLDER_PATH = filesRoot + "/app/"; // read config options passed from Java // Indices correspond to positions in the com.tns.AppConfig.KnownKeys enum @@ -928,6 +906,7 @@ double Runtime::PerformanceNowMillis() { } void Runtime::DestroyRuntime() { + CrashBreadcrumbs::UnregisterRuntime(m_id); { std::lock_guard lock(s_runtimeCacheMutex); s_id2RuntimeCache.erase(m_id); diff --git a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp index f576b8484..a8ac7bb6f 100644 --- a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp +++ b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp @@ -7,6 +7,7 @@ #include "ArgConverter.h" #include "CallbackHandlers.h" +#include "CrashBreadcrumbs.h" #include "JEnv.h" #include "JniLocalRef.h" #include "NativeScriptAssert.h" @@ -353,6 +354,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { runtimeId = env.CallStaticIntMethod(RUNTIME_CLASS, INIT_WORKER_RUNTIME_METHOD_ID, workerId_, (jstring) callingDir); runtime_ = Runtime::GetRuntime(runtimeId); + CrashBreadcrumbs::SetWorkerScript(runtimeId, workerPath_.c_str()); { std::lock_guard lock(looperMutex_);