@logbrew/react-native 0.1.23 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -5
- package/android/CMakeLists.txt +7 -0
- package/android/build.gradle +12 -0
- package/android/src/main/cpp/android_diagnostics.cpp +363 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidDiagnosticsRuntime.java +214 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidNativeDiagnostics.java +363 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidNativeSignalStore.java +233 -0
- package/android/src/main/java/co/logbrew/reactnative/AndroidParentDirectorySync.java +12 -12
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +15 -5
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +111 -156
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +15 -20
- package/android-native-diagnostics.d.ts +29 -0
- package/android-native-diagnostics.js +176 -0
- package/fatal-replay.cjs +35 -3
- package/global-errors.d.cts +3 -3
- package/global-errors.d.ts +3 -3
- package/global-errors.native.js +1 -12
- package/index.cjs +1 -1
- package/index.native.d.ts +4 -0
- package/index.native.js +21 -3
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +1 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +2 -10
- package/ios/GeneratedAppleDiagnostics/LogBrew/OperationTrace.swift +56 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +48 -56
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +6 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +0 -1
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +5 -4
- package/ios/LBRNFatalStoreModule.mm +15 -86
- package/package.json +17 -5
- package/persistent-delivery.native.js +4 -3
- package/src/NativeLogBrewFatalStore.ts +5 -4
- package/android/src/main/java/co/logbrew/reactnative/FatalRecordStore.java +0 -623
- package/ios/LBRNFatalRecordStore.h +0 -25
- package/ios/LBRNFatalRecordStore.m +0 -542
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
React Native helpers for the public LogBrew JavaScript SDK.
|
|
8
8
|
|
|
9
|
-
This package keeps event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`. It adds mobile helpers for screen views, app-state changes, product actions, API milestones, bounded issue breadcrumbs, typed handled and unhandled JavaScript errors, Promise rejection tracking, provider and hook usage, W3C trace correlation and propagation, lifecycle spans, resource spans, reversible instrumentation, and opt-in
|
|
9
|
+
This package keeps event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`. It adds mobile helpers for screen views, app-state changes, product actions, API milestones, bounded issue breadcrumbs, typed handled and unhandled JavaScript errors, Promise rejection tracking, provider and hook usage, W3C trace correlation and propagation, lifecycle spans, resource spans, reversible instrumentation, and opt-in native crash and app-hang diagnostics.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -267,16 +267,73 @@ errorHandler.remove();
|
|
|
267
267
|
|
|
268
268
|
Installation is idempotent for the active React Native `ErrorUtils` object. The wrapper captures a fixed-content, path-bounded issue for nonfatal global JavaScript errors and then calls the handler that was installed before it. Capture and diagnostic callback failures cannot prevent that prior handler from running. `remove()` reinstates the previous handler only while LogBrew still owns the global slot, so a later integration is not overwritten.
|
|
269
269
|
|
|
270
|
-
The React Native conditional export
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
The React Native conditional export admits fatal JavaScript reports directly
|
|
271
|
+
into the same app-private persistent queue used by normal delivery, then calls
|
|
272
|
+
the previous handler. Queue admission is synchronous and must increase the
|
|
273
|
+
pending count without increasing the dropped count. Otherwise capture fails
|
|
274
|
+
closed and reports a fixed diagnostic code. The next process provides
|
|
275
|
+
stable-ID at-least-once replay. Its acknowledgement happens only after local queue admission
|
|
276
|
+
is observable through the SDK queue counters. Normal batch acknowledgement
|
|
277
|
+
removes the event only after accepted delivery. Use
|
|
278
|
+
`persistentQueue: "required"` when fatal
|
|
279
|
+
survival is required. `fatalHealth()` exposes frozen bounded counters and
|
|
280
|
+
status. The Node ESM and CommonJS entries never import React Native;
|
|
281
|
+
non-React-Native callers may still inject the documented advanced `fatalStore`
|
|
282
|
+
adapter.
|
|
283
|
+
|
|
284
|
+
Automatic events exclude the original error message, raw stack, arbitrary metadata, full URLs, hosts, query strings, local absolute paths, payloads, and native error text. `onDiagnostic` receives only a fixed code. This error-handler integration does not claim mathematically exactly-once delivery, native crash capture, ANR or hang detection, or symbolication. The client-level persistent queue owns restart delivery.
|
|
273
285
|
|
|
274
286
|
### Apple native crash and app-hang diagnostics
|
|
275
287
|
|
|
276
288
|
Apple native diagnostics are an iOS-only, opt-in build feature. They capture a
|
|
277
289
|
fatal native process crash and replay one privacy-bounded issue after the next
|
|
278
290
|
launch. Set `hangThresholdSeconds` to also capture recovered or ongoing UIKit
|
|
279
|
-
main-thread hangs.
|
|
291
|
+
main-thread hangs.
|
|
292
|
+
|
|
293
|
+
### Android native crash and ANR diagnostics
|
|
294
|
+
|
|
295
|
+
Android diagnostics are opt-in and require a native build. They persist Java
|
|
296
|
+
crashes, one NDK fault frame, and main-thread hangs to the normal app-private
|
|
297
|
+
event queue. NDK evidence enters that queue on the next launch and remains
|
|
298
|
+
until queue admission succeeds. Native signal records retain the project,
|
|
299
|
+
release, environment, and service active at crash time, so an app update before
|
|
300
|
+
relaunch cannot reassign the crash to a newer build.
|
|
301
|
+
|
|
302
|
+
Remove other Android fatal handlers, then install before creating the client or
|
|
303
|
+
registering the React root:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
import {
|
|
307
|
+
createLogBrewReactNativeClient,
|
|
308
|
+
installLogBrewAndroidNativeDiagnostics
|
|
309
|
+
} from "@logbrew/react-native";
|
|
310
|
+
|
|
311
|
+
installLogBrewAndroidNativeDiagnostics({
|
|
312
|
+
clientKey: process.env.EXPO_PUBLIC_LOGBREW_CLIENT_KEY,
|
|
313
|
+
projectId: "550e8400-e29b-41d4-a716-446655440000",
|
|
314
|
+
release: "com.example.app@1.2.3+45",
|
|
315
|
+
environment: "production",
|
|
316
|
+
service: "android-app",
|
|
317
|
+
fatalHandlerOwnership: "logbrew",
|
|
318
|
+
anrThresholdMs: 5000
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const client = createLogBrewReactNativeClient({
|
|
322
|
+
clientKey: process.env.EXPO_PUBLIC_LOGBREW_CLIENT_KEY,
|
|
323
|
+
persistentQueue: "required"
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Capture excludes messages, thread names, console output, local variables,
|
|
328
|
+
request data, authentication values, device identifiers, and raw tombstones. Java and
|
|
329
|
+
hang reports keep at most 32 frames; NDK reports keep one build ID and offset.
|
|
330
|
+
|
|
331
|
+
`uninstallLogBrewAndroidNativeDiagnostics()` reinstates prior
|
|
332
|
+
handlers, stops the watchdog, and leaves queued events available. After closing
|
|
333
|
+
the client, purge only for an explicit discard. Upload exact-release Android
|
|
334
|
+
symbols with `logbrew debug-artifacts upload`, require build-ID/architecture
|
|
335
|
+
lookup, and preserve the old integration until the target app proves crash
|
|
336
|
+
replay, hang capture, and symbolicated hosted readback.
|
|
280
337
|
|
|
281
338
|
For Expo prebuild or development builds, add the config plugin:
|
|
282
339
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.22.1)
|
|
2
|
+
project(logbrew_android_diagnostics LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
add_library(logbrew_android_diagnostics SHARED src/main/cpp/android_diagnostics.cpp)
|
|
5
|
+
target_compile_features(logbrew_android_diagnostics PRIVATE cxx_std_17)
|
|
6
|
+
target_compile_options(logbrew_android_diagnostics PRIVATE -Wall -Wextra -Werror -fno-exceptions -fno-rtti)
|
|
7
|
+
target_link_libraries(logbrew_android_diagnostics PRIVATE dl)
|
package/android/build.gradle
CHANGED
|
@@ -26,6 +26,18 @@ android {
|
|
|
26
26
|
targetSdkVersion rootProject.ext.has("targetSdkVersion")
|
|
27
27
|
? rootProject.ext.get("targetSdkVersion")
|
|
28
28
|
: 35
|
|
29
|
+
externalNativeBuild {
|
|
30
|
+
cmake {
|
|
31
|
+
cppFlags "-std=c++17"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
externalNativeBuild {
|
|
37
|
+
cmake {
|
|
38
|
+
path "CMakeLists.txt"
|
|
39
|
+
version "3.22.1"
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
sourceSets {
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#include <jni.h>
|
|
2
|
+
|
|
3
|
+
#include <array>
|
|
4
|
+
#include <atomic>
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstdio>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
#include <ctime>
|
|
9
|
+
#include <dlfcn.h>
|
|
10
|
+
#include <elf.h>
|
|
11
|
+
#include <fcntl.h>
|
|
12
|
+
#include <link.h>
|
|
13
|
+
#include <signal.h>
|
|
14
|
+
#include <sys/syscall.h>
|
|
15
|
+
#include <unistd.h>
|
|
16
|
+
#include <ucontext.h>
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
constexpr uint32_t kMagic = 0x4c424e53;
|
|
20
|
+
constexpr uint32_t kVersion = 1;
|
|
21
|
+
constexpr size_t kIdBytes = 96;
|
|
22
|
+
constexpr size_t kUuidBytes = 36;
|
|
23
|
+
constexpr size_t kArchBytes = 16;
|
|
24
|
+
constexpr size_t kOffsetBytes = 16;
|
|
25
|
+
constexpr size_t kProjectChars = 36;
|
|
26
|
+
constexpr size_t kReleaseChars = 256;
|
|
27
|
+
constexpr size_t kEnvironmentChars = 128;
|
|
28
|
+
constexpr size_t kServiceChars = 128;
|
|
29
|
+
constexpr size_t kMaxModules = 192;
|
|
30
|
+
constexpr std::array<int, 6> kSignals = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP};
|
|
31
|
+
|
|
32
|
+
#pragma pack(push, 1)
|
|
33
|
+
struct SignalRecord {
|
|
34
|
+
uint32_t magic;
|
|
35
|
+
uint32_t version;
|
|
36
|
+
uint32_t signal;
|
|
37
|
+
uint64_t timestamp_ms;
|
|
38
|
+
char event_id[kIdBytes];
|
|
39
|
+
char image_uuid[kUuidBytes];
|
|
40
|
+
char architecture[kArchBytes];
|
|
41
|
+
char instruction_offset[kOffsetBytes];
|
|
42
|
+
uint16_t project_id[kProjectChars];
|
|
43
|
+
uint16_t release[kReleaseChars];
|
|
44
|
+
uint16_t environment[kEnvironmentChars];
|
|
45
|
+
uint16_t service[kServiceChars];
|
|
46
|
+
uint32_t checksum;
|
|
47
|
+
};
|
|
48
|
+
#pragma pack(pop)
|
|
49
|
+
static_assert(sizeof(SignalRecord) == 1284, "signal record layout drifted");
|
|
50
|
+
|
|
51
|
+
struct Module {
|
|
52
|
+
uintptr_t start;
|
|
53
|
+
uintptr_t end;
|
|
54
|
+
std::array<uint8_t, 16> build_id;
|
|
55
|
+
bool has_build_id;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
std::array<Module, kMaxModules> g_modules{};
|
|
59
|
+
size_t g_module_count = 0;
|
|
60
|
+
std::array<struct sigaction, kSignals.size()> g_previous_actions{};
|
|
61
|
+
std::array<char, 1024> g_record_path{};
|
|
62
|
+
SignalRecord g_record_template{};
|
|
63
|
+
std::atomic<bool> g_installed{false};
|
|
64
|
+
std::atomic_flag g_handling = ATOMIC_FLAG_INIT;
|
|
65
|
+
|
|
66
|
+
uint32_t crc32(const uint8_t* bytes, size_t length) {
|
|
67
|
+
uint32_t value = 0xffffffffU;
|
|
68
|
+
for (size_t index = 0; index < length; ++index) {
|
|
69
|
+
value ^= bytes[index];
|
|
70
|
+
for (int bit = 0; bit < 8; ++bit) {
|
|
71
|
+
value = (value >> 1U) ^ (0xedb88320U & (0U - (value & 1U)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return value ^ 0xffffffffU;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
size_t align4(size_t value) {
|
|
78
|
+
return (value + 3U) & ~size_t{3U};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
bool read_build_id(const dl_phdr_info* info, std::array<uint8_t, 16>* output) {
|
|
82
|
+
for (ElfW(Half) index = 0; index < info->dlpi_phnum; ++index) {
|
|
83
|
+
const ElfW(Phdr)& header = info->dlpi_phdr[index];
|
|
84
|
+
if (header.p_type != PT_NOTE || header.p_memsz < sizeof(ElfW(Nhdr))) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const auto* cursor = reinterpret_cast<const uint8_t*>(info->dlpi_addr + header.p_vaddr);
|
|
88
|
+
const auto* end = cursor + header.p_memsz;
|
|
89
|
+
while (cursor + sizeof(ElfW(Nhdr)) <= end) {
|
|
90
|
+
const auto* note = reinterpret_cast<const ElfW(Nhdr)*>(cursor);
|
|
91
|
+
cursor += sizeof(ElfW(Nhdr));
|
|
92
|
+
const size_t name_bytes = align4(note->n_namesz);
|
|
93
|
+
const size_t value_bytes = align4(note->n_descsz);
|
|
94
|
+
if (cursor + name_bytes + value_bytes > end) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const uint8_t* name = cursor;
|
|
98
|
+
const uint8_t* value = cursor + name_bytes;
|
|
99
|
+
if (note->n_type == NT_GNU_BUILD_ID
|
|
100
|
+
&& note->n_namesz >= 3
|
|
101
|
+
&& std::memcmp(name, "GNU", 3) == 0
|
|
102
|
+
&& note->n_descsz >= output->size()) {
|
|
103
|
+
std::memcpy(output->data(), value, output->size());
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
cursor += name_bytes + value_bytes;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
int collect_module(dl_phdr_info* info, size_t, void*) {
|
|
113
|
+
if (g_module_count == g_modules.size()) {
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
uintptr_t start = UINTPTR_MAX;
|
|
117
|
+
uintptr_t end = 0;
|
|
118
|
+
for (ElfW(Half) index = 0; index < info->dlpi_phnum; ++index) {
|
|
119
|
+
const ElfW(Phdr)& header = info->dlpi_phdr[index];
|
|
120
|
+
if (header.p_type != PT_LOAD) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const uintptr_t segment_start = info->dlpi_addr + header.p_vaddr;
|
|
124
|
+
start = segment_start < start ? segment_start : start;
|
|
125
|
+
const uintptr_t segment_end = segment_start + header.p_memsz;
|
|
126
|
+
end = segment_end > end ? segment_end : end;
|
|
127
|
+
}
|
|
128
|
+
if (start == UINTPTR_MAX || end <= start) {
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
Module module{};
|
|
132
|
+
module.start = start;
|
|
133
|
+
module.end = end;
|
|
134
|
+
module.has_build_id = read_build_id(info, &module.build_id);
|
|
135
|
+
g_modules[g_module_count++] = module;
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
uintptr_t program_counter(void* context) {
|
|
140
|
+
const auto* machine = reinterpret_cast<const ucontext_t*>(context);
|
|
141
|
+
#if defined(__aarch64__)
|
|
142
|
+
return machine->uc_mcontext.pc;
|
|
143
|
+
#elif defined(__arm__)
|
|
144
|
+
return machine->uc_mcontext.arm_pc;
|
|
145
|
+
#elif defined(__x86_64__)
|
|
146
|
+
return machine->uc_mcontext.gregs[REG_RIP];
|
|
147
|
+
#elif defined(__i386__)
|
|
148
|
+
return machine->uc_mcontext.gregs[REG_EIP];
|
|
149
|
+
#else
|
|
150
|
+
return 0;
|
|
151
|
+
#endif
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const char* architecture() {
|
|
155
|
+
#if defined(__aarch64__)
|
|
156
|
+
return "arm64";
|
|
157
|
+
#elif defined(__arm__)
|
|
158
|
+
return "arm";
|
|
159
|
+
#elif defined(__x86_64__)
|
|
160
|
+
return "x86_64";
|
|
161
|
+
#elif defined(__i386__)
|
|
162
|
+
return "x86";
|
|
163
|
+
#else
|
|
164
|
+
return "";
|
|
165
|
+
#endif
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const Module* module_for(uintptr_t address) {
|
|
169
|
+
for (size_t index = 0; index < g_module_count; ++index) {
|
|
170
|
+
const Module& module = g_modules[index];
|
|
171
|
+
if (module.has_build_id && address >= module.start && address < module.end) {
|
|
172
|
+
return &module;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return nullptr;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
void uuid_string(const std::array<uint8_t, 16>& bytes, char* output) {
|
|
179
|
+
static constexpr char hex[] = "0123456789abcdef";
|
|
180
|
+
size_t destination = 0;
|
|
181
|
+
for (size_t index = 0; index < bytes.size(); ++index) {
|
|
182
|
+
if (index == 4 || index == 6 || index == 8 || index == 10) {
|
|
183
|
+
output[destination++] = '-';
|
|
184
|
+
}
|
|
185
|
+
output[destination++] = hex[bytes[index] >> 4U];
|
|
186
|
+
output[destination++] = hex[bytes[index] & 0x0fU];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void offset_string(uintptr_t value, char* output) {
|
|
191
|
+
static constexpr char hex[] = "0123456789abcdef";
|
|
192
|
+
for (int index = 15; index >= 0; --index) {
|
|
193
|
+
output[index] = hex[value & 0x0fU];
|
|
194
|
+
value >>= 4U;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void write_record(int signal_number, void* context) {
|
|
199
|
+
const uintptr_t address = program_counter(context);
|
|
200
|
+
const Module* module = module_for(address);
|
|
201
|
+
if (module == nullptr || address < module->start) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
SignalRecord record = g_record_template;
|
|
205
|
+
record.signal = static_cast<uint32_t>(signal_number);
|
|
206
|
+
timespec now{};
|
|
207
|
+
if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
record.timestamp_ms = static_cast<uint64_t>(now.tv_sec) * 1000U
|
|
211
|
+
+ static_cast<uint64_t>(now.tv_nsec / 1000000U);
|
|
212
|
+
uuid_string(module->build_id, record.image_uuid);
|
|
213
|
+
const char* arch = architecture();
|
|
214
|
+
size_t arch_length = 0;
|
|
215
|
+
while (arch[arch_length] != '\0') {
|
|
216
|
+
++arch_length;
|
|
217
|
+
}
|
|
218
|
+
std::memcpy(record.architecture, arch, arch_length);
|
|
219
|
+
offset_string(address - module->start, record.instruction_offset);
|
|
220
|
+
record.checksum = crc32(reinterpret_cast<const uint8_t*>(&record), sizeof(record) - 4);
|
|
221
|
+
|
|
222
|
+
const int descriptor = open(
|
|
223
|
+
g_record_path.data(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0600);
|
|
224
|
+
if (descriptor < 0) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&record);
|
|
228
|
+
size_t written = 0;
|
|
229
|
+
while (written < sizeof(record)) {
|
|
230
|
+
const ssize_t count = write(descriptor, bytes + written, sizeof(record) - written);
|
|
231
|
+
if (count <= 0) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
written += static_cast<size_t>(count);
|
|
235
|
+
}
|
|
236
|
+
if (written == sizeof(record)) {
|
|
237
|
+
fsync(descriptor);
|
|
238
|
+
}
|
|
239
|
+
close(descriptor);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
void chain_signal(int signal_number) {
|
|
243
|
+
for (size_t index = 0; index < kSignals.size(); ++index) {
|
|
244
|
+
if (kSignals[index] == signal_number) {
|
|
245
|
+
sigaction(signal_number, &g_previous_actions[index], nullptr);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
syscall(SYS_tgkill, getpid(), gettid(), signal_number);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
void signal_handler(int signal_number, siginfo_t*, void* context) {
|
|
253
|
+
if (!g_handling.test_and_set()) {
|
|
254
|
+
write_record(signal_number, context);
|
|
255
|
+
}
|
|
256
|
+
chain_signal(signal_number);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
bool copy_jstring(JNIEnv* environment, jstring value, char* output, size_t capacity) {
|
|
260
|
+
if (value == nullptr) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const char* bytes = environment->GetStringUTFChars(value, nullptr);
|
|
264
|
+
if (bytes == nullptr) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
const size_t length = std::strlen(bytes);
|
|
268
|
+
const bool valid = length > 0 && length < capacity;
|
|
269
|
+
if (valid) {
|
|
270
|
+
std::memset(output, 0, capacity);
|
|
271
|
+
std::memcpy(output, bytes, length);
|
|
272
|
+
}
|
|
273
|
+
environment->ReleaseStringUTFChars(value, bytes);
|
|
274
|
+
return valid;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
bool copy_jstring_utf16(
|
|
278
|
+
JNIEnv* environment, jstring value, uint16_t* output, size_t capacity) {
|
|
279
|
+
if (value == nullptr) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
const jsize length = environment->GetStringLength(value);
|
|
283
|
+
if (length <= 0 || static_cast<size_t>(length) > capacity) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
std::memset(output, 0, capacity * sizeof(uint16_t));
|
|
287
|
+
environment->GetStringRegion(value, 0, length, reinterpret_cast<jchar*>(output));
|
|
288
|
+
return !environment->ExceptionCheck();
|
|
289
|
+
}
|
|
290
|
+
} // namespace
|
|
291
|
+
|
|
292
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
293
|
+
Java_co_logbrew_reactnative_AndroidDiagnosticsRuntime_nativeInstall(
|
|
294
|
+
JNIEnv* environment,
|
|
295
|
+
jclass,
|
|
296
|
+
jstring record_path,
|
|
297
|
+
jstring event_id,
|
|
298
|
+
jstring project_id,
|
|
299
|
+
jstring release,
|
|
300
|
+
jstring deployment_environment,
|
|
301
|
+
jstring service) {
|
|
302
|
+
bool expected = false;
|
|
303
|
+
if (!g_installed.compare_exchange_strong(expected, true)) {
|
|
304
|
+
return JNI_FALSE;
|
|
305
|
+
}
|
|
306
|
+
if (!copy_jstring(environment, record_path, g_record_path.data(), g_record_path.size())
|
|
307
|
+
|| !copy_jstring(
|
|
308
|
+
environment,
|
|
309
|
+
event_id,
|
|
310
|
+
g_record_template.event_id,
|
|
311
|
+
sizeof(g_record_template.event_id))
|
|
312
|
+
|| !copy_jstring_utf16(
|
|
313
|
+
environment,
|
|
314
|
+
project_id,
|
|
315
|
+
g_record_template.project_id,
|
|
316
|
+
kProjectChars)
|
|
317
|
+
|| !copy_jstring_utf16(
|
|
318
|
+
environment, release, g_record_template.release, kReleaseChars)
|
|
319
|
+
|| !copy_jstring_utf16(
|
|
320
|
+
environment,
|
|
321
|
+
deployment_environment,
|
|
322
|
+
g_record_template.environment,
|
|
323
|
+
kEnvironmentChars)
|
|
324
|
+
|| !copy_jstring_utf16(
|
|
325
|
+
environment, service, g_record_template.service, kServiceChars)) {
|
|
326
|
+
g_installed.store(false);
|
|
327
|
+
return JNI_FALSE;
|
|
328
|
+
}
|
|
329
|
+
g_record_template.magic = kMagic;
|
|
330
|
+
g_record_template.version = kVersion;
|
|
331
|
+
g_module_count = 0;
|
|
332
|
+
dl_iterate_phdr(collect_module, nullptr);
|
|
333
|
+
struct sigaction action {};
|
|
334
|
+
sigemptyset(&action.sa_mask);
|
|
335
|
+
action.sa_sigaction = signal_handler;
|
|
336
|
+
action.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
|
337
|
+
for (size_t index = 0; index < kSignals.size(); ++index) {
|
|
338
|
+
if (sigaction(kSignals[index], &action, &g_previous_actions[index]) != 0) {
|
|
339
|
+
for (size_t rollback = 0; rollback < index; ++rollback) {
|
|
340
|
+
sigaction(kSignals[rollback], &g_previous_actions[rollback], nullptr);
|
|
341
|
+
}
|
|
342
|
+
g_installed.store(false);
|
|
343
|
+
return JNI_FALSE;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return JNI_TRUE;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
extern "C" JNIEXPORT void JNICALL
|
|
350
|
+
Java_co_logbrew_reactnative_AndroidDiagnosticsRuntime_nativeUninstall(JNIEnv*, jclass) {
|
|
351
|
+
if (!g_installed.exchange(false)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (size_t index = 0; index < kSignals.size(); ++index) {
|
|
355
|
+
struct sigaction current {};
|
|
356
|
+
if (sigaction(kSignals[index], nullptr, ¤t) == 0
|
|
357
|
+
&& (current.sa_flags & SA_SIGINFO) != 0
|
|
358
|
+
&& current.sa_sigaction == signal_handler) {
|
|
359
|
+
sigaction(kSignals[index], &g_previous_actions[index], nullptr);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
g_handling.clear();
|
|
363
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
package co.logbrew.reactnative;
|
|
2
|
+
|
|
3
|
+
import android.app.ActivityManager;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.os.Handler;
|
|
6
|
+
import android.os.Looper;
|
|
7
|
+
import android.os.SystemClock;
|
|
8
|
+
import java.io.File;
|
|
9
|
+
import java.util.concurrent.Executors;
|
|
10
|
+
import java.util.concurrent.ScheduledExecutorService;
|
|
11
|
+
import java.util.concurrent.TimeUnit;
|
|
12
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
13
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
14
|
+
import java.util.function.LongConsumer;
|
|
15
|
+
|
|
16
|
+
final class AndroidDiagnosticsRuntime implements Thread.UncaughtExceptionHandler {
|
|
17
|
+
private static final boolean SIGNAL_CAPTURE_AVAILABLE = loadSignalCapture();
|
|
18
|
+
|
|
19
|
+
private final AndroidNativeDiagnostics diagnostics;
|
|
20
|
+
private final Thread.UncaughtExceptionHandler previousHandler;
|
|
21
|
+
private final AndroidNativeDiagnostics.Configuration configuration;
|
|
22
|
+
private final String signalRecordPath;
|
|
23
|
+
|
|
24
|
+
AndroidDiagnosticsRuntime(
|
|
25
|
+
Context context,
|
|
26
|
+
File storageRoot,
|
|
27
|
+
EventRecordStore eventStore,
|
|
28
|
+
AndroidNativeDiagnostics.Configuration configuration) {
|
|
29
|
+
this.configuration = configuration;
|
|
30
|
+
previousHandler = Thread.getDefaultUncaughtExceptionHandler();
|
|
31
|
+
MainThreadScheduler scheduler = new MainThreadScheduler(context);
|
|
32
|
+
AndroidNativeSignalStore signalStore =
|
|
33
|
+
new AndroidNativeSignalStore(new File(storageRoot, "logbrew-android-native-v1.signal"));
|
|
34
|
+
diagnostics =
|
|
35
|
+
new AndroidNativeDiagnostics(
|
|
36
|
+
eventStore,
|
|
37
|
+
signalStore,
|
|
38
|
+
configuration,
|
|
39
|
+
scheduler,
|
|
40
|
+
() -> {},
|
|
41
|
+
this::chain,
|
|
42
|
+
scheduler.mainThread::getStackTrace);
|
|
43
|
+
signalRecordPath = signalStore.path();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
synchronized String install() {
|
|
47
|
+
if (diagnostics.installed) {
|
|
48
|
+
return "already_installed";
|
|
49
|
+
}
|
|
50
|
+
if (pending() < 0) {
|
|
51
|
+
throw new IllegalStateException("event queue unavailable");
|
|
52
|
+
}
|
|
53
|
+
String status = diagnostics.install();
|
|
54
|
+
Thread.setDefaultUncaughtExceptionHandler(this);
|
|
55
|
+
if (!diagnostics.signalStore.prepare(new AndroidParentDirectorySync())
|
|
56
|
+
|| !SIGNAL_CAPTURE_AVAILABLE
|
|
57
|
+
|| !nativeInstall(
|
|
58
|
+
signalRecordPath,
|
|
59
|
+
diagnostics.nextNativeEventId(),
|
|
60
|
+
configuration.projectId,
|
|
61
|
+
configuration.release,
|
|
62
|
+
configuration.environment,
|
|
63
|
+
configuration.service)) {
|
|
64
|
+
Thread.setDefaultUncaughtExceptionHandler(previousHandler);
|
|
65
|
+
diagnostics.uninstall();
|
|
66
|
+
throw new IllegalStateException("signal capture unavailable");
|
|
67
|
+
}
|
|
68
|
+
return status;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
synchronized String uninstall() {
|
|
72
|
+
if (!diagnostics.installed) {
|
|
73
|
+
return "not_installed";
|
|
74
|
+
}
|
|
75
|
+
if (SIGNAL_CAPTURE_AVAILABLE) {
|
|
76
|
+
nativeUninstall();
|
|
77
|
+
}
|
|
78
|
+
if (Thread.getDefaultUncaughtExceptionHandler() == this) {
|
|
79
|
+
Thread.setDefaultUncaughtExceptionHandler(previousHandler);
|
|
80
|
+
}
|
|
81
|
+
diagnostics.uninstall();
|
|
82
|
+
return "uninstalled";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
synchronized int pending() {
|
|
86
|
+
EventRecordStore.Result result = diagnostics.eventStore.load();
|
|
87
|
+
return "loaded".equals(result.status) ? result.records.size() : -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
synchronized boolean installed() {
|
|
91
|
+
return diagnostics.installed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
synchronized boolean matches(
|
|
95
|
+
EventRecordStore eventStore, AndroidNativeDiagnostics.Configuration configuration) {
|
|
96
|
+
return diagnostics.eventStore == eventStore && diagnostics.configurationMatches(configuration);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Override
|
|
100
|
+
public void uncaughtException(Thread thread, Throwable error) {
|
|
101
|
+
diagnostics.handleUncaught(thread, error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private void chain(Thread thread, Throwable error) {
|
|
105
|
+
if (previousHandler != null && previousHandler != this) {
|
|
106
|
+
previousHandler.uncaughtException(thread, error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
android.os.Process.killProcess(android.os.Process.myPid());
|
|
110
|
+
System.exit(10);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static boolean loadSignalCapture() {
|
|
114
|
+
try {
|
|
115
|
+
System.loadLibrary("logbrew_android_diagnostics");
|
|
116
|
+
return true;
|
|
117
|
+
} catch (LinkageError error) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static native boolean nativeInstall(
|
|
123
|
+
String recordPath,
|
|
124
|
+
String eventId,
|
|
125
|
+
String projectId,
|
|
126
|
+
String release,
|
|
127
|
+
String environment,
|
|
128
|
+
String service);
|
|
129
|
+
|
|
130
|
+
private static native void nativeUninstall();
|
|
131
|
+
|
|
132
|
+
private static final class MainThreadScheduler implements AndroidNativeDiagnostics.Scheduler {
|
|
133
|
+
private final Context context;
|
|
134
|
+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
135
|
+
private final Thread mainThread = Looper.getMainLooper().getThread();
|
|
136
|
+
private final AtomicLong heartbeatMs = new AtomicLong(SystemClock.elapsedRealtime());
|
|
137
|
+
private final AtomicBoolean reported = new AtomicBoolean();
|
|
138
|
+
private ScheduledExecutorService executor;
|
|
139
|
+
|
|
140
|
+
MainThreadScheduler(Context context) {
|
|
141
|
+
this.context = context.getApplicationContext();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@Override
|
|
145
|
+
public synchronized void start(
|
|
146
|
+
long thresholdMs, LongConsumer report) {
|
|
147
|
+
stop();
|
|
148
|
+
heartbeatMs.set(SystemClock.elapsedRealtime());
|
|
149
|
+
mainHandler.post(this::heartbeat);
|
|
150
|
+
executor =
|
|
151
|
+
Executors.newSingleThreadScheduledExecutor(
|
|
152
|
+
runnable -> {
|
|
153
|
+
Thread thread = new Thread(runnable, "LogBrew-ANR-Watchdog");
|
|
154
|
+
thread.setDaemon(true);
|
|
155
|
+
return thread;
|
|
156
|
+
});
|
|
157
|
+
long intervalMs = Math.max(500, thresholdMs / 2);
|
|
158
|
+
executor.scheduleWithFixedDelay(
|
|
159
|
+
() -> {
|
|
160
|
+
long now = SystemClock.elapsedRealtime();
|
|
161
|
+
long elapsedMs = now - heartbeatMs.get();
|
|
162
|
+
boolean debugging = android.os.Debug.isDebuggerConnected()
|
|
163
|
+
|| android.os.Debug.waitingForDebugger();
|
|
164
|
+
if (AndroidNativeDiagnostics.shouldReportHang(
|
|
165
|
+
elapsedMs, thresholdMs, debugging, processNotResponding())
|
|
166
|
+
&& reported.compareAndSet(false, true)) {
|
|
167
|
+
report.accept(elapsedMs);
|
|
168
|
+
}
|
|
169
|
+
mainHandler.post(this::heartbeat);
|
|
170
|
+
},
|
|
171
|
+
thresholdMs,
|
|
172
|
+
intervalMs,
|
|
173
|
+
TimeUnit.MILLISECONDS);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Override
|
|
177
|
+
public synchronized void stop() {
|
|
178
|
+
if (executor != null) {
|
|
179
|
+
executor.shutdownNow();
|
|
180
|
+
executor = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private void heartbeat() {
|
|
185
|
+
heartbeatMs.set(SystemClock.elapsedRealtime());
|
|
186
|
+
reported.set(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private boolean processNotResponding() {
|
|
190
|
+
ActivityManager manager =
|
|
191
|
+
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
192
|
+
if (manager == null) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
java.util.List<ActivityManager.ProcessErrorStateInfo> states =
|
|
197
|
+
manager.getProcessesInErrorState();
|
|
198
|
+
if (states != null) {
|
|
199
|
+
int processId = android.os.Process.myPid();
|
|
200
|
+
for (ActivityManager.ProcessErrorStateInfo state : states) {
|
|
201
|
+
if (state != null
|
|
202
|
+
&& state.pid == processId
|
|
203
|
+
&& state.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (RuntimeException ignored) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|