@logbrew/react-native 0.1.11 → 0.1.13
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/LogBrewReactNative.podspec +25 -3
- package/README.md +106 -3
- package/apple-native-diagnostics.d.ts +52 -0
- package/apple-native-diagnostics.js +286 -0
- package/expo.cjs +46 -0
- package/expo.d.ts +16 -0
- package/expo.js +5 -0
- package/fatal-replay.cjs +12 -1
- package/global-errors.cjs +3 -1
- package/index.cjs +84 -2
- package/index.d.cts +4 -0
- package/index.d.ts +4 -0
- package/index.js +84 -2
- package/index.native.d.ts +4 -0
- package/index.native.js +14 -1
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
- package/package.json +43 -5
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
final class OrderedHangDiagnosticDelivery: HangDiagnosticDelivering, @unchecked Sendable {
|
|
6
|
+
private let queue = DispatchQueue(label: "co.logbrew.native-hang-diagnostics")
|
|
7
|
+
private let handler: @Sendable (NativeHangDiagnostic) -> Void
|
|
8
|
+
|
|
9
|
+
init(_ handler: @escaping @Sendable (NativeHangDiagnostic) -> Void) {
|
|
10
|
+
self.handler = handler
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
func enqueue(_ diagnostic: NativeHangDiagnostic) {
|
|
14
|
+
queue.async { [handler] in
|
|
15
|
+
handler(diagnostic)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
final class HangWatchdogController: @unchecked Sendable {
|
|
21
|
+
private let threshold: TimeInterval
|
|
22
|
+
private let identity: NativeArtifactIdentity
|
|
23
|
+
private let store: any HangIncidentStoring
|
|
24
|
+
private let stackCapture: any MainThreadStackCapturing
|
|
25
|
+
private let safetyPolicy: any HangSafetyChecking
|
|
26
|
+
private let clock: any HangWatchdogClock
|
|
27
|
+
private let mainPinger: any MainThreadPinging
|
|
28
|
+
private let diagnosticDelivery: any HangDiagnosticDelivering
|
|
29
|
+
private let lock = NSLock()
|
|
30
|
+
|
|
31
|
+
private var active = false
|
|
32
|
+
private var running = false
|
|
33
|
+
private var heartbeat: HangHeartbeatState = .idle
|
|
34
|
+
|
|
35
|
+
init(
|
|
36
|
+
threshold: TimeInterval,
|
|
37
|
+
identity: NativeArtifactIdentity,
|
|
38
|
+
store: any HangIncidentStoring,
|
|
39
|
+
stackCapture: any MainThreadStackCapturing,
|
|
40
|
+
safetyPolicy: any HangSafetyChecking,
|
|
41
|
+
clock: any HangWatchdogClock,
|
|
42
|
+
mainPinger: any MainThreadPinging,
|
|
43
|
+
diagnosticDelivery: any HangDiagnosticDelivering,
|
|
44
|
+
) {
|
|
45
|
+
self.threshold = threshold
|
|
46
|
+
self.identity = identity
|
|
47
|
+
self.store = store
|
|
48
|
+
self.stackCapture = stackCapture
|
|
49
|
+
self.safetyPolicy = safetyPolicy
|
|
50
|
+
self.clock = clock
|
|
51
|
+
self.mainPinger = mainPinger
|
|
52
|
+
self.diagnosticDelivery = diagnosticDelivery
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func start(active: Bool) {
|
|
56
|
+
lock.lock()
|
|
57
|
+
guard !running else {
|
|
58
|
+
lock.unlock()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
running = true
|
|
62
|
+
self.active = active
|
|
63
|
+
heartbeat = .idle
|
|
64
|
+
let shouldPing = active
|
|
65
|
+
lock.unlock()
|
|
66
|
+
if shouldPing {
|
|
67
|
+
beginHeartbeat()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func setActive(_ active: Bool) {
|
|
72
|
+
if !active {
|
|
73
|
+
lock.lock()
|
|
74
|
+
guard running else {
|
|
75
|
+
lock.unlock()
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
self.active = false
|
|
79
|
+
let captured = heartbeat.captured
|
|
80
|
+
heartbeat = .idle
|
|
81
|
+
lock.unlock()
|
|
82
|
+
markRecovered(captured)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lock.lock()
|
|
87
|
+
guard running else {
|
|
88
|
+
lock.unlock()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
self.active = active
|
|
92
|
+
let shouldPing = heartbeat == .idle
|
|
93
|
+
lock.unlock()
|
|
94
|
+
if shouldPing {
|
|
95
|
+
beginHeartbeat()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func poll() {
|
|
100
|
+
guard let sentAt = waitingHeartbeatForPoll() else {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
let elapsed = clock.monotonicNow() - sentAt
|
|
104
|
+
guard elapsed >= threshold else {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if let suppression = suppressionCode(elapsed: elapsed) {
|
|
108
|
+
suppress(suppression, sentAt: sentAt)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
guard let durationMs = NativeHangDuration.milliseconds(
|
|
112
|
+
from: sentAt,
|
|
113
|
+
through: sentAt + elapsed,
|
|
114
|
+
) else {
|
|
115
|
+
suppress(.storageFailed, sentAt: sentAt)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let frames = stackCapture.capture()
|
|
120
|
+
guard !frames.isEmpty else {
|
|
121
|
+
suppress(.stackUnavailable, sentAt: sentAt)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
captureIncident(
|
|
125
|
+
frames: frames,
|
|
126
|
+
sentAt: sentAt,
|
|
127
|
+
durationMs: durationMs,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func stop() {
|
|
132
|
+
lock.lock()
|
|
133
|
+
let captured = heartbeat.captured
|
|
134
|
+
running = false
|
|
135
|
+
active = false
|
|
136
|
+
heartbeat = .idle
|
|
137
|
+
lock.unlock()
|
|
138
|
+
markRecovered(captured)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func suppressionCode(elapsed: TimeInterval) -> NativeHangDiagnosticCode? {
|
|
142
|
+
if elapsed > threshold * 1.5 {
|
|
143
|
+
return .skippedSchedulerStall
|
|
144
|
+
}
|
|
145
|
+
if safetyPolicy.isDebuggerAttached() {
|
|
146
|
+
return .skippedDebugger
|
|
147
|
+
}
|
|
148
|
+
if safetyPolicy.isThermallyRestricted() {
|
|
149
|
+
return .skippedThermal
|
|
150
|
+
}
|
|
151
|
+
do {
|
|
152
|
+
guard try store.read() == nil else {
|
|
153
|
+
return .pendingRetained
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
return .storageFailed
|
|
157
|
+
}
|
|
158
|
+
return nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func waitingHeartbeatForPoll() -> TimeInterval? {
|
|
162
|
+
lock.lock()
|
|
163
|
+
guard running, active else {
|
|
164
|
+
lock.unlock()
|
|
165
|
+
return nil
|
|
166
|
+
}
|
|
167
|
+
guard case let .waiting(sentAt) = heartbeat else {
|
|
168
|
+
let shouldBegin = heartbeat == .idle
|
|
169
|
+
lock.unlock()
|
|
170
|
+
if shouldBegin {
|
|
171
|
+
beginHeartbeat()
|
|
172
|
+
}
|
|
173
|
+
return nil
|
|
174
|
+
}
|
|
175
|
+
lock.unlock()
|
|
176
|
+
return sentAt
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func captureIncident(
|
|
180
|
+
frames: [NativeStackFrame],
|
|
181
|
+
sentAt: TimeInterval,
|
|
182
|
+
durationMs: Double,
|
|
183
|
+
) {
|
|
184
|
+
let eventID = UUID().uuidString.lowercased()
|
|
185
|
+
let incident = NativeHangIncident(
|
|
186
|
+
eventID: eventID,
|
|
187
|
+
timestamp: clock.timestamp(),
|
|
188
|
+
state: .ongoing,
|
|
189
|
+
identity: identity,
|
|
190
|
+
nativeStackFrames: frames,
|
|
191
|
+
durationMs: durationMs,
|
|
192
|
+
)
|
|
193
|
+
do {
|
|
194
|
+
lock.lock()
|
|
195
|
+
guard running, active, heartbeat == .waiting(sentAt: sentAt) else {
|
|
196
|
+
lock.unlock()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
try store.write(incident)
|
|
200
|
+
heartbeat = .captured(eventID: eventID, sentAt: sentAt)
|
|
201
|
+
diagnosticDelivery.enqueue(NativeHangDiagnostic(.captured))
|
|
202
|
+
lock.unlock()
|
|
203
|
+
} catch {
|
|
204
|
+
let persisted = try? store.read()
|
|
205
|
+
heartbeat = persisted == incident
|
|
206
|
+
? .captured(eventID: eventID, sentAt: sentAt)
|
|
207
|
+
: .suppressed
|
|
208
|
+
diagnosticDelivery.enqueue(NativeHangDiagnostic(.storageFailed))
|
|
209
|
+
lock.unlock()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func beginHeartbeat() {
|
|
214
|
+
let sentAt = clock.monotonicNow()
|
|
215
|
+
lock.lock()
|
|
216
|
+
guard running, active, heartbeat == .idle else {
|
|
217
|
+
lock.unlock()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
heartbeat = .waiting(sentAt: sentAt)
|
|
221
|
+
lock.unlock()
|
|
222
|
+
mainPinger.ping { [weak self] in
|
|
223
|
+
self?.mainDidRespond()
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private func mainDidRespond() {
|
|
228
|
+
lock.lock()
|
|
229
|
+
let captured = heartbeat.captured
|
|
230
|
+
heartbeat = .idle
|
|
231
|
+
lock.unlock()
|
|
232
|
+
markRecovered(captured)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func markRecovered(
|
|
236
|
+
_ captured: (eventID: String, sentAt: TimeInterval)?,
|
|
237
|
+
) {
|
|
238
|
+
guard let captured else {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
guard let durationMs = NativeHangDuration.milliseconds(
|
|
242
|
+
from: captured.sentAt,
|
|
243
|
+
through: clock.monotonicNow(),
|
|
244
|
+
) else {
|
|
245
|
+
emit(.storageFailed)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
do {
|
|
249
|
+
try store.markRecovered(
|
|
250
|
+
eventID: captured.eventID,
|
|
251
|
+
durationMs: durationMs,
|
|
252
|
+
)
|
|
253
|
+
emit(.recovered)
|
|
254
|
+
} catch {
|
|
255
|
+
emit(.storageFailed)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private func suppress(_ code: NativeHangDiagnosticCode, sentAt: TimeInterval) {
|
|
260
|
+
lock.lock()
|
|
261
|
+
guard heartbeat == .waiting(sentAt: sentAt) else {
|
|
262
|
+
lock.unlock()
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
heartbeat = .suppressed
|
|
266
|
+
diagnosticDelivery.enqueue(NativeHangDiagnostic(code))
|
|
267
|
+
lock.unlock()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private func emit(_ code: NativeHangDiagnosticCode) {
|
|
271
|
+
diagnosticDelivery.enqueue(NativeHangDiagnostic(code))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
extension HangWatchdogController: HangWatchdogControlling {}
|
|
276
|
+
|
|
277
|
+
private enum HangHeartbeatState: Equatable {
|
|
278
|
+
case idle
|
|
279
|
+
case waiting(sentAt: TimeInterval)
|
|
280
|
+
case captured(eventID: String, sentAt: TimeInterval)
|
|
281
|
+
case suppressed
|
|
282
|
+
|
|
283
|
+
var captured: (eventID: String, sentAt: TimeInterval)? {
|
|
284
|
+
guard case let .captured(eventID, sentAt) = self else {
|
|
285
|
+
return nil
|
|
286
|
+
}
|
|
287
|
+
return (eventID, sentAt)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private enum HangTimerLifecycleState {
|
|
292
|
+
case prepared
|
|
293
|
+
case active
|
|
294
|
+
case stopped
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
final class NativeHangWatchdogRuntime: @unchecked Sendable {
|
|
298
|
+
private let controller: any HangWatchdogControlling
|
|
299
|
+
private let timer: any HangTimerControlling
|
|
300
|
+
private let lifecycle: any HangLifecycleObserving
|
|
301
|
+
private let lock = NSLock()
|
|
302
|
+
private var timerState: HangTimerLifecycleState = .prepared
|
|
303
|
+
|
|
304
|
+
init(
|
|
305
|
+
configuration: NativeHangWatchdogConfiguration,
|
|
306
|
+
identity: NativeArtifactIdentity,
|
|
307
|
+
store: any HangIncidentStoring,
|
|
308
|
+
) throws {
|
|
309
|
+
guard Thread.isMainThread else {
|
|
310
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
311
|
+
}
|
|
312
|
+
let stackCapture = try NativeMainThreadStackCapture()
|
|
313
|
+
let lifecycle = try NativeHangLifecycleObserver()
|
|
314
|
+
let diagnosticDelivery = OrderedHangDiagnosticDelivery(
|
|
315
|
+
configuration.diagnosticsHandler ?? { _ in },
|
|
316
|
+
)
|
|
317
|
+
let controller = HangWatchdogController(
|
|
318
|
+
threshold: configuration.threshold,
|
|
319
|
+
identity: identity,
|
|
320
|
+
store: store,
|
|
321
|
+
stackCapture: stackCapture,
|
|
322
|
+
safetyPolicy: NativeHangSafetyPolicy(),
|
|
323
|
+
clock: SystemHangWatchdogClock(),
|
|
324
|
+
mainPinger: DispatchMainThreadPinger(),
|
|
325
|
+
diagnosticDelivery: diagnosticDelivery,
|
|
326
|
+
)
|
|
327
|
+
let timer = DispatchHangTimer(
|
|
328
|
+
interval: configuration.threshold / 5,
|
|
329
|
+
controller: controller,
|
|
330
|
+
)
|
|
331
|
+
self.controller = controller
|
|
332
|
+
self.timer = timer
|
|
333
|
+
self.lifecycle = lifecycle
|
|
334
|
+
lifecycle.onActiveChange = { [weak controller] active in
|
|
335
|
+
controller?.setActive(active)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
init(
|
|
340
|
+
controller: any HangWatchdogControlling,
|
|
341
|
+
timer: any HangTimerControlling,
|
|
342
|
+
lifecycle: any HangLifecycleObserving,
|
|
343
|
+
) {
|
|
344
|
+
self.controller = controller
|
|
345
|
+
self.timer = timer
|
|
346
|
+
self.lifecycle = lifecycle
|
|
347
|
+
lifecycle.onActiveChange = { [weak controller] active in
|
|
348
|
+
controller?.setActive(active)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
deinit {
|
|
353
|
+
stop()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
func start() {
|
|
357
|
+
lock.lock()
|
|
358
|
+
guard timerState == .prepared else {
|
|
359
|
+
lock.unlock()
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
controller.start(active: lifecycle.isActive)
|
|
363
|
+
lifecycle.start()
|
|
364
|
+
timer.activate()
|
|
365
|
+
timerState = .active
|
|
366
|
+
lock.unlock()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
func stop() {
|
|
370
|
+
lock.lock()
|
|
371
|
+
switch timerState {
|
|
372
|
+
case .prepared:
|
|
373
|
+
timer.activate()
|
|
374
|
+
timer.cancel()
|
|
375
|
+
case .active:
|
|
376
|
+
timer.cancel()
|
|
377
|
+
case .stopped:
|
|
378
|
+
lock.unlock()
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
timerState = .stopped
|
|
382
|
+
lock.unlock()
|
|
383
|
+
lifecycle.stop()
|
|
384
|
+
controller.stop()
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
public enum NativeHangDiagnosticCode: String, Sendable {
|
|
6
|
+
case skippedDebugger = "hang_skipped_debugger"
|
|
7
|
+
case skippedThermal = "hang_skipped_thermal"
|
|
8
|
+
case skippedSchedulerStall = "hang_skipped_scheduler_stall"
|
|
9
|
+
case stackUnavailable = "hang_stack_unavailable"
|
|
10
|
+
case storageFailed = "hang_storage_failed"
|
|
11
|
+
case pendingRetained = "hang_pending_retained"
|
|
12
|
+
case captured = "hang_captured"
|
|
13
|
+
case recovered = "hang_recovered"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public struct NativeHangDiagnostic: Sendable, CustomStringConvertible {
|
|
17
|
+
public let code: NativeHangDiagnosticCode
|
|
18
|
+
|
|
19
|
+
init(_ code: NativeHangDiagnosticCode) {
|
|
20
|
+
self.code = code
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public var description: String {
|
|
24
|
+
"NativeHangDiagnostic(code: \(code.rawValue))"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public struct NativeHangWatchdogConfiguration: @unchecked Sendable {
|
|
29
|
+
public let threshold: TimeInterval
|
|
30
|
+
public let diagnosticsHandler: (@Sendable (NativeHangDiagnostic) -> Void)?
|
|
31
|
+
|
|
32
|
+
public init(
|
|
33
|
+
threshold: TimeInterval = 2,
|
|
34
|
+
diagnosticsHandler: (@Sendable (NativeHangDiagnostic) -> Void)? = nil,
|
|
35
|
+
) throws {
|
|
36
|
+
guard threshold.isFinite, (1 ... 30).contains(threshold) else {
|
|
37
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
38
|
+
}
|
|
39
|
+
self.threshold = threshold
|
|
40
|
+
self.diagnosticsHandler = diagnosticsHandler
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protocol HangWatchdogClock: AnyObject, Sendable {
|
|
45
|
+
func monotonicNow() -> TimeInterval
|
|
46
|
+
func timestamp() -> String
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
protocol HangSafetyChecking: AnyObject, Sendable {
|
|
50
|
+
func isDebuggerAttached() -> Bool
|
|
51
|
+
func isThermallyRestricted() -> Bool
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protocol MainThreadStackCapturing: AnyObject, Sendable {
|
|
55
|
+
func capture() -> [NativeStackFrame]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protocol MainThreadPinging: AnyObject, Sendable {
|
|
59
|
+
func ping(_ response: @escaping @Sendable () -> Void)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
protocol HangDiagnosticDelivering: AnyObject, Sendable {
|
|
63
|
+
func enqueue(_ diagnostic: NativeHangDiagnostic)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protocol HangWatchdogControlling: AnyObject, Sendable {
|
|
67
|
+
func start(active: Bool)
|
|
68
|
+
func setActive(_ active: Bool)
|
|
69
|
+
func poll()
|
|
70
|
+
func stop()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protocol HangTimerControlling: AnyObject, Sendable {
|
|
74
|
+
func activate()
|
|
75
|
+
func cancel()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protocol HangLifecycleObserving: AnyObject, Sendable {
|
|
79
|
+
var onActiveChange: (@Sendable (Bool) -> Void)? { get set }
|
|
80
|
+
var isActive: Bool { get }
|
|
81
|
+
|
|
82
|
+
func start()
|
|
83
|
+
func stop()
|
|
84
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Darwin
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
#if canImport(UIKit)
|
|
7
|
+
import UIKit
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
final class DispatchHangTimer: HangTimerControlling, @unchecked Sendable {
|
|
11
|
+
private let source: DispatchSourceTimer
|
|
12
|
+
|
|
13
|
+
init(interval: TimeInterval, controller: any HangWatchdogControlling) {
|
|
14
|
+
source = DispatchSource.makeTimerSource(
|
|
15
|
+
queue: DispatchQueue(label: "co.logbrew.native-hang-watchdog", qos: .utility),
|
|
16
|
+
)
|
|
17
|
+
source.schedule(
|
|
18
|
+
deadline: .now() + interval,
|
|
19
|
+
repeating: interval,
|
|
20
|
+
leeway: .milliseconds(20),
|
|
21
|
+
)
|
|
22
|
+
source.setEventHandler { [weak controller] in
|
|
23
|
+
controller?.poll()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func activate() {
|
|
28
|
+
source.activate()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func cancel() {
|
|
32
|
+
source.cancel()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
final class SystemHangWatchdogClock: HangWatchdogClock, @unchecked Sendable {
|
|
37
|
+
func monotonicNow() -> TimeInterval {
|
|
38
|
+
ProcessInfo.processInfo.systemUptime
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func timestamp() -> String {
|
|
42
|
+
let formatter = ISO8601DateFormatter()
|
|
43
|
+
formatter.formatOptions = [.withInternetDateTime]
|
|
44
|
+
return formatter.string(from: Date())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
final class DispatchMainThreadPinger: MainThreadPinging, @unchecked Sendable {
|
|
49
|
+
func ping(_ response: @escaping @Sendable () -> Void) {
|
|
50
|
+
DispatchQueue.main.async(execute: response)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
final class NativeHangSafetyPolicy: HangSafetyChecking, @unchecked Sendable {
|
|
55
|
+
func isDebuggerAttached() -> Bool {
|
|
56
|
+
var information = kinfo_proc()
|
|
57
|
+
var size = MemoryLayout<kinfo_proc>.stride
|
|
58
|
+
var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
|
|
59
|
+
guard sysctl(&mib, u_int(mib.count), &information, &size, nil, 0) == 0 else {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
return information.kp_proc.p_flag & P_TRACED != 0
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func isThermallyRestricted() -> Bool {
|
|
66
|
+
switch ProcessInfo.processInfo.thermalState {
|
|
67
|
+
case .serious, .critical:
|
|
68
|
+
true
|
|
69
|
+
default:
|
|
70
|
+
false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
final class NativeHangLifecycleObserver: HangLifecycleObserving, @unchecked Sendable {
|
|
76
|
+
var onActiveChange: (@Sendable (Bool) -> Void)?
|
|
77
|
+
private let lock = NSLock()
|
|
78
|
+
private var observers: [NSObjectProtocol] = []
|
|
79
|
+
private var active = false
|
|
80
|
+
|
|
81
|
+
var isActive: Bool {
|
|
82
|
+
lock.lock()
|
|
83
|
+
defer { lock.unlock() }
|
|
84
|
+
return active
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
init() throws {
|
|
88
|
+
#if !canImport(UIKit)
|
|
89
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
90
|
+
#else
|
|
91
|
+
active = MainActor.assumeIsolated {
|
|
92
|
+
UIApplication.shared.applicationState == .active
|
|
93
|
+
}
|
|
94
|
+
#endif
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func start() {
|
|
98
|
+
#if canImport(UIKit)
|
|
99
|
+
let center = NotificationCenter.default
|
|
100
|
+
observers = [
|
|
101
|
+
center.addObserver(
|
|
102
|
+
forName: UIApplication.didBecomeActiveNotification,
|
|
103
|
+
object: nil,
|
|
104
|
+
queue: .main,
|
|
105
|
+
) { [weak self] _ in
|
|
106
|
+
self?.updateActive(true)
|
|
107
|
+
},
|
|
108
|
+
center.addObserver(
|
|
109
|
+
forName: UIApplication.willResignActiveNotification,
|
|
110
|
+
object: nil,
|
|
111
|
+
queue: .main,
|
|
112
|
+
) { [weak self] _ in
|
|
113
|
+
self?.updateActive(false)
|
|
114
|
+
},
|
|
115
|
+
center.addObserver(
|
|
116
|
+
forName: UIApplication.didEnterBackgroundNotification,
|
|
117
|
+
object: nil,
|
|
118
|
+
queue: .main,
|
|
119
|
+
) { [weak self] _ in
|
|
120
|
+
self?.updateActive(false)
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
#endif
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func stop() {
|
|
127
|
+
let center = NotificationCenter.default
|
|
128
|
+
observers.forEach(center.removeObserver)
|
|
129
|
+
observers.removeAll()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private func updateActive(_ active: Bool) {
|
|
133
|
+
lock.lock()
|
|
134
|
+
self.active = active
|
|
135
|
+
let handler = onActiveChange
|
|
136
|
+
lock.unlock()
|
|
137
|
+
handler?(active)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Darwin
|
|
4
|
+
import Foundation
|
|
5
|
+
@preconcurrency import KSCrash
|
|
6
|
+
|
|
7
|
+
final class NativeMainThreadStackCapture: MainThreadStackCapturing, @unchecked Sendable {
|
|
8
|
+
private static let maxFrames = 32
|
|
9
|
+
private static let cpuTypeArm64: Int32 = 0x0100_000C
|
|
10
|
+
private static let cpuTypeX8664: Int32 = 0x0100_0007
|
|
11
|
+
private static let cpuSubtypeMask: UInt32 = 0x00FF_FFFF
|
|
12
|
+
private let mainThread: pthread_t
|
|
13
|
+
|
|
14
|
+
init() throws {
|
|
15
|
+
guard Thread.isMainThread else {
|
|
16
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
17
|
+
}
|
|
18
|
+
mainThread = pthread_self()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func capture() -> [NativeStackFrame] {
|
|
22
|
+
#if os(watchOS)
|
|
23
|
+
return []
|
|
24
|
+
#else
|
|
25
|
+
var addresses = [UInt](repeating: 0, count: Self.maxFrames)
|
|
26
|
+
let count = captureBacktrace(
|
|
27
|
+
thread: mainThread,
|
|
28
|
+
addresses: &addresses,
|
|
29
|
+
count: Int32(Self.maxFrames),
|
|
30
|
+
)
|
|
31
|
+
guard count > 0 else {
|
|
32
|
+
return []
|
|
33
|
+
}
|
|
34
|
+
return addresses.prefix(Int(count)).compactMap(frame)
|
|
35
|
+
#endif
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private func frame(for address: UInt) -> NativeStackFrame? {
|
|
39
|
+
var information = SymbolInformation()
|
|
40
|
+
guard symbolicate(address: address, result: &information),
|
|
41
|
+
let uuidBytes = information.imageUUID,
|
|
42
|
+
let architecture = architecture(
|
|
43
|
+
cpuType: information.imageCpuType,
|
|
44
|
+
cpuSubtype: information.imageCpuSubType,
|
|
45
|
+
),
|
|
46
|
+
information.callInstruction >= information.imageAddress
|
|
47
|
+
else {
|
|
48
|
+
return nil
|
|
49
|
+
}
|
|
50
|
+
let imageEnd = information.imageAddress.addingReportingOverflow(UInt(information.imageSize))
|
|
51
|
+
guard !imageEnd.overflow, information.callInstruction < imageEnd.partialValue else {
|
|
52
|
+
return nil
|
|
53
|
+
}
|
|
54
|
+
let bytes = UnsafeBufferPointer(start: uuidBytes, count: 16)
|
|
55
|
+
let uuid = UUID(uuid: (
|
|
56
|
+
bytes[0], bytes[1], bytes[2], bytes[3],
|
|
57
|
+
bytes[4], bytes[5], bytes[6], bytes[7],
|
|
58
|
+
bytes[8], bytes[9], bytes[10], bytes[11],
|
|
59
|
+
bytes[12], bytes[13], bytes[14], bytes[15],
|
|
60
|
+
)).uuidString.lowercased()
|
|
61
|
+
return NativeStackFrame(
|
|
62
|
+
imageUuid: uuid,
|
|
63
|
+
architecture: architecture,
|
|
64
|
+
instructionOffset: String(
|
|
65
|
+
format: "%016llx",
|
|
66
|
+
UInt64(information.callInstruction - information.imageAddress),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private func architecture(cpuType: Int32, cpuSubtype: Int32) -> NativeStackArchitecture? {
|
|
72
|
+
let subtype = UInt32(bitPattern: cpuSubtype) & Self.cpuSubtypeMask
|
|
73
|
+
return switch (cpuType, subtype) {
|
|
74
|
+
case (Self.cpuTypeArm64, 0), (Self.cpuTypeArm64, 1):
|
|
75
|
+
.arm64
|
|
76
|
+
case (Self.cpuTypeArm64, 2):
|
|
77
|
+
.arm64e
|
|
78
|
+
case (Self.cpuTypeX8664, 3):
|
|
79
|
+
.x8664
|
|
80
|
+
default:
|
|
81
|
+
nil
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|