@logbrew/react-native 0.1.10 → 0.1.12
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 +174 -4
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- 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 +86 -2
- package/index.d.cts +7 -0
- package/index.d.ts +7 -0
- package/index.js +86 -2
- package/index.native.d.ts +47 -1
- package/index.native.js +81 -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/ios/LBRNEventRecordStore.h +29 -0
- package/ios/LBRNEventRecordStore.m +746 -0
- package/ios/LBRNFatalStoreModule.mm +159 -14
- package/ios/LBRNPrivateStorage.h +7 -0
- package/ios/LBRNPrivateStorage.m +38 -0
- package/package.json +47 -4
- package/persistent-delivery.native.js +282 -0
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
- package/src/NativeLogBrewFatalStore.ts +9 -0
|
@@ -0,0 +1,317 @@
|
|
|
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
|
+
extension NativeCrashCapture {
|
|
6
|
+
func beginReplay() throws {
|
|
7
|
+
lock.lock()
|
|
8
|
+
defer { lock.unlock() }
|
|
9
|
+
try verifyProcessLocked()
|
|
10
|
+
guard store != nil, lifecycle != .stopped else {
|
|
11
|
+
throw NativeCrashError(.notInstalled)
|
|
12
|
+
}
|
|
13
|
+
guard !replaying else {
|
|
14
|
+
throw NativeCrashError(.replayBusy)
|
|
15
|
+
}
|
|
16
|
+
replaying = true
|
|
17
|
+
lifecycle = .replaying
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func performReplay(_ handler: (NativeCrashRecord) -> Bool) throws -> NativeCrashReplayResult {
|
|
21
|
+
var attempted = 0
|
|
22
|
+
var accepted = 0
|
|
23
|
+
var discardedDuringReplay = 0
|
|
24
|
+
replayLoop: while true {
|
|
25
|
+
switch try nextReplayItem() {
|
|
26
|
+
case .none:
|
|
27
|
+
break replayLoop
|
|
28
|
+
case .discarded:
|
|
29
|
+
discardedDuringReplay += 1
|
|
30
|
+
case let .record(record):
|
|
31
|
+
attempted += 1
|
|
32
|
+
guard handler(record) else {
|
|
33
|
+
recordRetained()
|
|
34
|
+
break replayLoop
|
|
35
|
+
}
|
|
36
|
+
try acknowledge(record)
|
|
37
|
+
accepted += 1
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return try NativeCrashReplayResult(
|
|
42
|
+
attempted: attempted,
|
|
43
|
+
acknowledged: accepted,
|
|
44
|
+
discarded: discardedDuringReplay,
|
|
45
|
+
pending: finishReplay(),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func nextReplayItem() throws -> NativeCrashReplayItem {
|
|
50
|
+
lock.lock()
|
|
51
|
+
defer { lock.unlock() }
|
|
52
|
+
guard let store else {
|
|
53
|
+
throw NativeCrashError(.notInstalled)
|
|
54
|
+
}
|
|
55
|
+
try verifyStorageLocked()
|
|
56
|
+
let reportIDs = try validatedReportIDs(in: store)
|
|
57
|
+
let hangItem = try nextHangReplayItem()
|
|
58
|
+
let engineItem = try nextEngineReplayItem(
|
|
59
|
+
reportIDs: reportIDs,
|
|
60
|
+
store: store,
|
|
61
|
+
)
|
|
62
|
+
if hangItem?.isDiscarded == true || engineItem?.isDiscarded == true {
|
|
63
|
+
return .discarded
|
|
64
|
+
}
|
|
65
|
+
return oldestRecord(in: [hangItem, engineItem])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func acknowledge(_ record: NativeCrashRecord) throws {
|
|
69
|
+
lock.lock()
|
|
70
|
+
defer { lock.unlock() }
|
|
71
|
+
guard record.ownerNonce == ownerNonce, let store else {
|
|
72
|
+
throw NativeCrashError(.reportChanged)
|
|
73
|
+
}
|
|
74
|
+
try verifyStorageLocked()
|
|
75
|
+
switch record.source {
|
|
76
|
+
case let .engine(reportID):
|
|
77
|
+
try acknowledgeEngineRecord(record, reportID: reportID, store: store)
|
|
78
|
+
case let .hang(eventID):
|
|
79
|
+
try acknowledgeHangRecord(record, eventID: eventID)
|
|
80
|
+
}
|
|
81
|
+
acknowledged += 1
|
|
82
|
+
lastOutcome = .acknowledged
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func pendingReportsLocked() throws -> [NativeCrashRecord] {
|
|
86
|
+
guard let store else {
|
|
87
|
+
throw NativeCrashError(.notInstalled)
|
|
88
|
+
}
|
|
89
|
+
try verifyStorageLocked()
|
|
90
|
+
let reportIDs = try validatedReportIDs(in: store)
|
|
91
|
+
|
|
92
|
+
var records: [NativeCrashRecord] = []
|
|
93
|
+
if let incident = try hangStore?.read() {
|
|
94
|
+
try records.append(incident.makeRecord(ownerNonce: ownerNonce))
|
|
95
|
+
}
|
|
96
|
+
try records.append(contentsOf: reportIDs.map { reportID in
|
|
97
|
+
guard let rawReport = store.report(for: reportID) else {
|
|
98
|
+
throw NativeCrashError(.reportChanged)
|
|
99
|
+
}
|
|
100
|
+
return try sanitizer.makeRecord(reportID: reportID, rawReport: rawReport)
|
|
101
|
+
})
|
|
102
|
+
try verifyReportIDs(reportIDs, in: store)
|
|
103
|
+
return records.sorted(by: NativeCrashRecord.occursBefore)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func pendingCountLocked() throws -> Int {
|
|
107
|
+
guard let store else {
|
|
108
|
+
throw NativeCrashError(.notInstalled)
|
|
109
|
+
}
|
|
110
|
+
try verifyStorageLocked()
|
|
111
|
+
let reportIDs = try validatedReportIDs(in: store)
|
|
112
|
+
return try reportIDs.count + ((hangStore?.read()) == nil ? 0 : 1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func failReplay() {
|
|
116
|
+
lock.lock()
|
|
117
|
+
replaying = false
|
|
118
|
+
lifecycle = .installed
|
|
119
|
+
lastOutcome = .failed
|
|
120
|
+
lock.unlock()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private extension NativeCrashCapture {
|
|
125
|
+
func nextHangReplayItem() throws -> NativeCrashReplayItem? {
|
|
126
|
+
guard let hangStore else {
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
do {
|
|
130
|
+
guard let incident = try hangStore.read() else {
|
|
131
|
+
return nil
|
|
132
|
+
}
|
|
133
|
+
return try .record(incident.makeRecord(ownerNonce: ownerNonce))
|
|
134
|
+
} catch let error as NativeCrashError where error.code == .reportCorrupt {
|
|
135
|
+
try hangStore.purge()
|
|
136
|
+
recordDiscardedLocked()
|
|
137
|
+
return .discarded
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func nextEngineReplayItem(
|
|
142
|
+
reportIDs: [Int64],
|
|
143
|
+
store: any CrashReportStoring,
|
|
144
|
+
) throws -> NativeCrashReplayItem? {
|
|
145
|
+
var records: [NativeCrashRecord] = []
|
|
146
|
+
for reportID in reportIDs {
|
|
147
|
+
let item = try engineReplayItem(
|
|
148
|
+
reportID: reportID,
|
|
149
|
+
expectedReportIDs: reportIDs,
|
|
150
|
+
store: store,
|
|
151
|
+
)
|
|
152
|
+
switch item {
|
|
153
|
+
case .none:
|
|
154
|
+
continue
|
|
155
|
+
case .discarded:
|
|
156
|
+
return .discarded
|
|
157
|
+
case let .record(record):
|
|
158
|
+
records.append(record)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return records.min(by: NativeCrashRecord.occursBefore).map(NativeCrashReplayItem.record)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func oldestRecord(in items: [NativeCrashReplayItem?]) -> NativeCrashReplayItem {
|
|
165
|
+
let records = items.compactMap { item -> NativeCrashRecord? in
|
|
166
|
+
guard case let .record(record) = item else {
|
|
167
|
+
return nil
|
|
168
|
+
}
|
|
169
|
+
return record
|
|
170
|
+
}
|
|
171
|
+
return records.min(by: NativeCrashRecord.occursBefore).map(NativeCrashReplayItem.record)
|
|
172
|
+
?? .none
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func engineReplayItem(
|
|
176
|
+
reportID: Int64,
|
|
177
|
+
expectedReportIDs: [Int64],
|
|
178
|
+
store: any CrashReportStoring,
|
|
179
|
+
) throws -> NativeCrashReplayItem {
|
|
180
|
+
guard let rawReport = store.report(for: reportID) else {
|
|
181
|
+
throw NativeCrashError(.reportChanged)
|
|
182
|
+
}
|
|
183
|
+
do {
|
|
184
|
+
let record = try sanitizer.makeRecord(reportID: reportID, rawReport: rawReport)
|
|
185
|
+
try verifyReportIDs(expectedReportIDs, in: store)
|
|
186
|
+
return .record(record)
|
|
187
|
+
} catch let error as NativeCrashError where error.code == .reportCorrupt {
|
|
188
|
+
try verifyReportIDs(expectedReportIDs, in: store)
|
|
189
|
+
store.deleteReport(with: reportID)
|
|
190
|
+
guard !store.reportIDs.contains(reportID) else {
|
|
191
|
+
throw NativeCrashError(.reportDeletionFailed)
|
|
192
|
+
}
|
|
193
|
+
recordDiscardedLocked()
|
|
194
|
+
return .discarded
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func validatedReportIDs(in store: any CrashReportStoring) throws -> [Int64] {
|
|
199
|
+
let reportIDs = store.reportIDs.sorted()
|
|
200
|
+
guard reportIDs.count <= configuration.maxStoredReports,
|
|
201
|
+
Set(reportIDs).count == reportIDs.count,
|
|
202
|
+
reportIDs.allSatisfy({ $0 > 0 })
|
|
203
|
+
else {
|
|
204
|
+
throw NativeCrashError(.reportCorrupt)
|
|
205
|
+
}
|
|
206
|
+
return reportIDs
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func verifyReportIDs(
|
|
210
|
+
_ expectedReportIDs: [Int64],
|
|
211
|
+
in store: any CrashReportStoring,
|
|
212
|
+
) throws {
|
|
213
|
+
guard store.reportIDs.sorted() == expectedReportIDs else {
|
|
214
|
+
throw NativeCrashError(.reportChanged)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func acknowledgeEngineRecord(
|
|
219
|
+
_ record: NativeCrashRecord,
|
|
220
|
+
reportID: Int64,
|
|
221
|
+
store: any CrashReportStoring,
|
|
222
|
+
) throws {
|
|
223
|
+
guard store.reportIDs.contains(reportID),
|
|
224
|
+
let rawReport = store.report(for: reportID),
|
|
225
|
+
try sanitizer.digest(rawReport) == record.digest
|
|
226
|
+
else {
|
|
227
|
+
throw NativeCrashError(.reportChanged)
|
|
228
|
+
}
|
|
229
|
+
store.deleteReport(with: reportID)
|
|
230
|
+
guard !store.reportIDs.contains(reportID) else {
|
|
231
|
+
throw NativeCrashError(.reportDeletionFailed)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func acknowledgeHangRecord(
|
|
236
|
+
_ record: NativeCrashRecord,
|
|
237
|
+
eventID: String,
|
|
238
|
+
) throws {
|
|
239
|
+
guard let hangStore,
|
|
240
|
+
let incident = try hangStore.read(),
|
|
241
|
+
incident.eventID == eventID,
|
|
242
|
+
try incident.digest() == record.digest
|
|
243
|
+
else {
|
|
244
|
+
throw NativeCrashError(.reportChanged)
|
|
245
|
+
}
|
|
246
|
+
try hangStore.delete(eventID: eventID)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func recordDiscardedLocked() {
|
|
250
|
+
if discarded < Int.max {
|
|
251
|
+
discarded += 1
|
|
252
|
+
}
|
|
253
|
+
lastOutcome = .discarded
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
func recordRetained() {
|
|
257
|
+
lock.lock()
|
|
258
|
+
lastOutcome = .retained
|
|
259
|
+
lock.unlock()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func finishReplay() throws -> Int {
|
|
263
|
+
lock.lock()
|
|
264
|
+
defer { lock.unlock() }
|
|
265
|
+
let pending = try pendingCountLocked()
|
|
266
|
+
replaying = false
|
|
267
|
+
lifecycle = .installed
|
|
268
|
+
return pending
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var sanitizer: CrashReportSanitizer {
|
|
272
|
+
CrashReportSanitizer(
|
|
273
|
+
maxReplayBytes: configuration.maxReplayBytes,
|
|
274
|
+
ownerNonce: ownerNonce,
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
enum NativeCrashReplayItem {
|
|
280
|
+
case none
|
|
281
|
+
case discarded
|
|
282
|
+
case record(NativeCrashRecord)
|
|
283
|
+
|
|
284
|
+
var isDiscarded: Bool {
|
|
285
|
+
if case .discarded = self {
|
|
286
|
+
return true
|
|
287
|
+
}
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private extension NativeCrashRecord {
|
|
293
|
+
static func occursBefore(_ lhs: NativeCrashRecord, _ rhs: NativeCrashRecord) -> Bool {
|
|
294
|
+
let lhsTimestamp = NativeCrashTimestamp(lhs.timestamp)
|
|
295
|
+
let rhsTimestamp = NativeCrashTimestamp(rhs.timestamp)
|
|
296
|
+
guard let lhsTimestamp, let rhsTimestamp else {
|
|
297
|
+
return lhs.source.occursBefore(rhs.source)
|
|
298
|
+
}
|
|
299
|
+
if lhsTimestamp != rhsTimestamp {
|
|
300
|
+
return lhsTimestamp < rhsTimestamp
|
|
301
|
+
}
|
|
302
|
+
return lhs.source.occursBefore(rhs.source)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private extension NativeCrashRecordSource {
|
|
307
|
+
func occursBefore(_ other: NativeCrashRecordSource) -> Bool {
|
|
308
|
+
switch (self, other) {
|
|
309
|
+
case let (.engine(lhsReportID), .engine(rhsReportID)):
|
|
310
|
+
lhsReportID < rhsReportID
|
|
311
|
+
case (.engine, .hang):
|
|
312
|
+
true
|
|
313
|
+
case (.hang, .engine), (.hang, .hang):
|
|
314
|
+
false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import CryptoKit
|
|
4
|
+
import Darwin
|
|
5
|
+
import Foundation
|
|
6
|
+
|
|
7
|
+
enum NativeHangIncidentState: String, Codable {
|
|
8
|
+
case ongoing
|
|
9
|
+
case recovered
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
enum NativeHangDuration {
|
|
13
|
+
static let maxMilliseconds = 24 * 60 * 60 * 1000.0
|
|
14
|
+
|
|
15
|
+
static func validated(_ milliseconds: Double) throws -> Double {
|
|
16
|
+
guard milliseconds.isFinite,
|
|
17
|
+
milliseconds >= 0,
|
|
18
|
+
milliseconds <= maxMilliseconds
|
|
19
|
+
else {
|
|
20
|
+
throw NativeCrashError(.reportCorrupt)
|
|
21
|
+
}
|
|
22
|
+
return milliseconds
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static func milliseconds(
|
|
26
|
+
from monotonicStart: TimeInterval,
|
|
27
|
+
through monotonicEnd: TimeInterval,
|
|
28
|
+
) -> Double? {
|
|
29
|
+
guard monotonicStart.isFinite,
|
|
30
|
+
monotonicEnd.isFinite,
|
|
31
|
+
monotonicEnd >= monotonicStart
|
|
32
|
+
else {
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
return try? validated((monotonicEnd - monotonicStart) * 1000)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct NativeHangIncident: Codable, Equatable {
|
|
40
|
+
private let version: Int
|
|
41
|
+
let eventID: String
|
|
42
|
+
let timestamp: String
|
|
43
|
+
var state: NativeHangIncidentState
|
|
44
|
+
let artifactIdentity: NativeArtifactIdentityValue
|
|
45
|
+
let nativeStackFrames: [NativeStackFrame]
|
|
46
|
+
var durationMs: Double?
|
|
47
|
+
|
|
48
|
+
init(
|
|
49
|
+
eventID: String,
|
|
50
|
+
timestamp: String,
|
|
51
|
+
state: NativeHangIncidentState,
|
|
52
|
+
identity: NativeArtifactIdentity,
|
|
53
|
+
nativeStackFrames: [NativeStackFrame],
|
|
54
|
+
durationMs: Double? = nil,
|
|
55
|
+
) {
|
|
56
|
+
version = 1
|
|
57
|
+
self.eventID = eventID
|
|
58
|
+
self.timestamp = timestamp
|
|
59
|
+
self.state = state
|
|
60
|
+
artifactIdentity = NativeArtifactIdentityValue(identity)
|
|
61
|
+
self.nativeStackFrames = nativeStackFrames
|
|
62
|
+
self.durationMs = durationMs
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func validated() throws -> NativeHangIncident {
|
|
66
|
+
guard version == 1,
|
|
67
|
+
UUID(uuidString: eventID)?.uuidString.lowercased() == eventID,
|
|
68
|
+
normalizedTimestamp(timestamp) == timestamp,
|
|
69
|
+
(1 ... 32).contains(nativeStackFrames.count),
|
|
70
|
+
nativeStackFrames.allSatisfy(validFrame)
|
|
71
|
+
else {
|
|
72
|
+
throw NativeCrashError(.reportCorrupt)
|
|
73
|
+
}
|
|
74
|
+
do {
|
|
75
|
+
_ = try artifactIdentity.validatedIdentity()
|
|
76
|
+
if let durationMs {
|
|
77
|
+
_ = try NativeHangDuration.validated(durationMs)
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
throw NativeCrashError(.reportCorrupt)
|
|
81
|
+
}
|
|
82
|
+
return self
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func digest() throws -> Data {
|
|
86
|
+
try Data(SHA256.hash(data: encoded()))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func makeRecord(ownerNonce: UUID) throws -> NativeCrashRecord {
|
|
90
|
+
try NativeCrashRecord(
|
|
91
|
+
eventID: eventID,
|
|
92
|
+
timestamp: timestamp,
|
|
93
|
+
mechanism: .deadlock,
|
|
94
|
+
nativeStackFrames: nativeStackFrames,
|
|
95
|
+
artifactIdentity: artifactIdentity.validatedIdentity(),
|
|
96
|
+
hangState: state,
|
|
97
|
+
hangDurationMs: durationMs,
|
|
98
|
+
source: .hang(eventID: eventID),
|
|
99
|
+
digest: digest(),
|
|
100
|
+
ownerNonce: ownerNonce,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func encoded() throws -> Data {
|
|
105
|
+
let encoder = JSONEncoder()
|
|
106
|
+
encoder.outputFormatting = [.sortedKeys]
|
|
107
|
+
let data = try encoder.encode(self)
|
|
108
|
+
guard data.count <= NativeHangIncidentFileStore.maxBytes else {
|
|
109
|
+
throw NativeCrashError(.reportCorrupt)
|
|
110
|
+
}
|
|
111
|
+
return data
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private func validFrame(_ frame: NativeStackFrame) -> Bool {
|
|
115
|
+
UUID(uuidString: frame.imageUuid)?.uuidString.lowercased() == frame.imageUuid
|
|
116
|
+
&& frame.instructionOffset.utf8.count == 16
|
|
117
|
+
&& frame.instructionOffset.utf8.allSatisfy {
|
|
118
|
+
($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private func normalizedTimestamp(_ value: String) -> String? {
|
|
123
|
+
let formatter = ISO8601DateFormatter()
|
|
124
|
+
formatter.formatOptions = [.withInternetDateTime]
|
|
125
|
+
guard let date = formatter.date(from: value) else {
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
return formatter.string(from: date)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protocol HangIncidentStoring: AnyObject, Sendable {
|
|
133
|
+
func read() throws -> NativeHangIncident?
|
|
134
|
+
func write(_ incident: NativeHangIncident) throws
|
|
135
|
+
func markRecovered(eventID: String, durationMs: Double) throws
|
|
136
|
+
func delete(eventID: String) throws
|
|
137
|
+
func purge() throws
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
final class NativeHangIncidentFileStore: HangIncidentStoring, @unchecked Sendable {
|
|
141
|
+
static let recordName = "hang-v1.record"
|
|
142
|
+
static let maxBytes = 16 * 1024
|
|
143
|
+
|
|
144
|
+
private let directory: URL
|
|
145
|
+
private let recordURL: URL
|
|
146
|
+
private let temporaryURL: URL
|
|
147
|
+
private let lease: CrashStorageLease
|
|
148
|
+
private let lock = NSLock()
|
|
149
|
+
|
|
150
|
+
init(directory: URL) throws {
|
|
151
|
+
self.directory = CrashStorageDirectory.normalized(directory)
|
|
152
|
+
lease = try CrashStorageDirectory.prepare(self.directory)
|
|
153
|
+
recordURL = self.directory.appendingPathComponent(Self.recordName, isDirectory: false)
|
|
154
|
+
temporaryURL = self.directory.appendingPathComponent("\(Self.recordName).tmp", isDirectory: false)
|
|
155
|
+
try removeStaleTemporary()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func read() throws -> NativeHangIncident? {
|
|
159
|
+
lock.lock()
|
|
160
|
+
defer { lock.unlock() }
|
|
161
|
+
return try readLocked()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func write(_ incident: NativeHangIncident) throws {
|
|
165
|
+
lock.lock()
|
|
166
|
+
defer { lock.unlock() }
|
|
167
|
+
guard try readLocked() == nil else {
|
|
168
|
+
throw NativeCrashError(.reportChanged)
|
|
169
|
+
}
|
|
170
|
+
try writeLocked(incident.validated())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func markRecovered(eventID: String, durationMs: Double) throws {
|
|
174
|
+
lock.lock()
|
|
175
|
+
defer { lock.unlock() }
|
|
176
|
+
guard var incident = try readLocked(), incident.eventID == eventID else {
|
|
177
|
+
throw NativeCrashError(.reportChanged)
|
|
178
|
+
}
|
|
179
|
+
let recoveredDuration = try NativeHangDuration.validated(durationMs)
|
|
180
|
+
guard incident.durationMs.map({ recoveredDuration >= $0 }) ?? true else {
|
|
181
|
+
throw NativeCrashError(.reportChanged)
|
|
182
|
+
}
|
|
183
|
+
incident.state = .recovered
|
|
184
|
+
incident.durationMs = recoveredDuration
|
|
185
|
+
try writeLocked(incident.validated())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func delete(eventID: String) throws {
|
|
189
|
+
lock.lock()
|
|
190
|
+
defer { lock.unlock() }
|
|
191
|
+
guard let incident = try readLocked(), incident.eventID == eventID else {
|
|
192
|
+
throw NativeCrashError(.reportChanged)
|
|
193
|
+
}
|
|
194
|
+
try lease.verify()
|
|
195
|
+
guard unlink(recordURL.path) == 0 else {
|
|
196
|
+
throw NativeCrashError(.reportDeletionFailed)
|
|
197
|
+
}
|
|
198
|
+
try lease.synchronize()
|
|
199
|
+
guard try readLocked() == nil else {
|
|
200
|
+
throw NativeCrashError(.reportDeletionFailed)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func purge() throws {
|
|
205
|
+
lock.lock()
|
|
206
|
+
defer { lock.unlock() }
|
|
207
|
+
try lease.verify()
|
|
208
|
+
if unlink(recordURL.path) != 0, errno != ENOENT {
|
|
209
|
+
throw NativeCrashError(.reportDeletionFailed)
|
|
210
|
+
}
|
|
211
|
+
try lease.synchronize()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private func readLocked() throws -> NativeHangIncident? {
|
|
215
|
+
try lease.verify()
|
|
216
|
+
let descriptor = open(recordURL.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC)
|
|
217
|
+
if descriptor < 0 {
|
|
218
|
+
if errno == ENOENT {
|
|
219
|
+
return nil
|
|
220
|
+
}
|
|
221
|
+
throw NativeCrashError(.storageUnsupported)
|
|
222
|
+
}
|
|
223
|
+
defer { close(descriptor) }
|
|
224
|
+
|
|
225
|
+
var information = stat()
|
|
226
|
+
guard fstat(descriptor, &information) == 0,
|
|
227
|
+
information.st_mode & S_IFMT == S_IFREG,
|
|
228
|
+
information.st_mode & 0o077 == 0,
|
|
229
|
+
information.st_size > 0,
|
|
230
|
+
information.st_size <= Self.maxBytes
|
|
231
|
+
else {
|
|
232
|
+
throw NativeCrashError(.reportCorrupt)
|
|
233
|
+
}
|
|
234
|
+
var data = Data(count: Int(information.st_size))
|
|
235
|
+
let count = data.withUnsafeMutableBytes { buffer -> Int in
|
|
236
|
+
guard let base = buffer.baseAddress else {
|
|
237
|
+
return -1
|
|
238
|
+
}
|
|
239
|
+
var consumed = 0
|
|
240
|
+
while consumed < buffer.count {
|
|
241
|
+
let result = Darwin.read(descriptor, base.advanced(by: consumed), buffer.count - consumed)
|
|
242
|
+
if result <= 0 {
|
|
243
|
+
return -1
|
|
244
|
+
}
|
|
245
|
+
consumed += result
|
|
246
|
+
}
|
|
247
|
+
return consumed
|
|
248
|
+
}
|
|
249
|
+
guard count == data.count else {
|
|
250
|
+
throw NativeCrashError(.reportCorrupt)
|
|
251
|
+
}
|
|
252
|
+
do {
|
|
253
|
+
return try JSONDecoder().decode(NativeHangIncident.self, from: data).validated()
|
|
254
|
+
} catch let error as NativeCrashError {
|
|
255
|
+
throw error
|
|
256
|
+
} catch {
|
|
257
|
+
throw NativeCrashError(.reportCorrupt)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func writeLocked(_ incident: NativeHangIncident) throws {
|
|
262
|
+
try lease.verify()
|
|
263
|
+
let data = try incident.encoded()
|
|
264
|
+
try removeStaleTemporary()
|
|
265
|
+
let descriptor = open(
|
|
266
|
+
temporaryURL.path,
|
|
267
|
+
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC,
|
|
268
|
+
S_IRUSR | S_IWUSR,
|
|
269
|
+
)
|
|
270
|
+
guard descriptor >= 0 else {
|
|
271
|
+
throw NativeCrashError(.storageUnsupported)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var succeeded = false
|
|
275
|
+
defer {
|
|
276
|
+
close(descriptor)
|
|
277
|
+
if !succeeded {
|
|
278
|
+
unlink(temporaryURL.path)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
let written = data.withUnsafeBytes { buffer -> Bool in
|
|
282
|
+
guard let base = buffer.baseAddress else {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
var consumed = 0
|
|
286
|
+
while consumed < buffer.count {
|
|
287
|
+
let result = Darwin.write(descriptor, base.advanced(by: consumed), buffer.count - consumed)
|
|
288
|
+
if result <= 0 {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
consumed += result
|
|
292
|
+
}
|
|
293
|
+
return true
|
|
294
|
+
}
|
|
295
|
+
guard written,
|
|
296
|
+
fsync(descriptor) == 0,
|
|
297
|
+
rename(temporaryURL.path, recordURL.path) == 0
|
|
298
|
+
else {
|
|
299
|
+
throw NativeCrashError(.storageUnsupported)
|
|
300
|
+
}
|
|
301
|
+
try lease.synchronize()
|
|
302
|
+
succeeded = true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private func removeStaleTemporary() throws {
|
|
306
|
+
var information = stat()
|
|
307
|
+
if lstat(temporaryURL.path, &information) != 0 {
|
|
308
|
+
if errno == ENOENT {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
throw NativeCrashError(.storageUnsupported)
|
|
312
|
+
}
|
|
313
|
+
guard information.st_mode & S_IFMT == S_IFREG,
|
|
314
|
+
information.st_mode & 0o077 == 0,
|
|
315
|
+
unlink(temporaryURL.path) == 0
|
|
316
|
+
else {
|
|
317
|
+
throw NativeCrashError(.storageUnsupported)
|
|
318
|
+
}
|
|
319
|
+
try lease.synchronize()
|
|
320
|
+
}
|
|
321
|
+
}
|