@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.
Files changed (61) hide show
  1. package/LogBrewReactNative.podspec +25 -3
  2. package/README.md +106 -3
  3. package/apple-native-diagnostics.d.ts +52 -0
  4. package/apple-native-diagnostics.js +286 -0
  5. package/expo.cjs +46 -0
  6. package/expo.d.ts +16 -0
  7. package/expo.js +5 -0
  8. package/fatal-replay.cjs +12 -1
  9. package/global-errors.cjs +3 -1
  10. package/index.cjs +84 -2
  11. package/index.d.cts +4 -0
  12. package/index.d.ts +4 -0
  13. package/index.js +84 -2
  14. package/index.native.d.ts +4 -0
  15. package/index.native.js +14 -1
  16. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
  17. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
  18. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
  19. package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
  20. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
  21. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
  22. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
  29. package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
  30. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
  31. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
  32. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
  33. package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
  34. package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
  35. package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
  36. package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
  37. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
  38. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
  39. package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
  40. package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
  41. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
  42. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
  43. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
  44. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
  45. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
  46. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
  47. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
  48. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
  49. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
  50. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
  51. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
  52. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
  53. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
  54. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
  55. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
  56. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
  57. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
  58. package/package.json +43 -5
  59. package/promise-rejections.cjs +3 -1
  60. package/scripts/sync-apple-native-sources.mjs +93 -0
  61. 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
+ }