Fix EXC_BREAKPOINT reading a deleted entity's timestamp in the console - #377
Merged
Merged
Conversation
`LoggerMessageEntity.createdAt` and `NetworkTaskEntity.createdAt` are
non-optional `@NSManaged Date`s. `LoggerStore.deleteEntities` batch-deletes
rows and merges the deletions into `viewContext`, after which Core Data
reports every attribute of the affected objects as NULL. Any console cell
still holding one of those entities then force-bridges `nil` into `Date`
and traps:
Date._unconditionallyBridgeFromObjectiveC(_:)
closure kean#1 in ConsoleTaskCell.makeHeader(settings:) (ConsoleTaskCell.swift:78)
ConsoleTaskCell.body.getter
...
EXC_BREAKPOINT
Read `createdAt` through KVC in `formattedTimestamp` so a NULL value
surfaces as `nil` instead of trapping, matching how `state(in:)` already
guards `session` (kean#242).
Also observe the entity in `ConsoleEntityCell` so its existing
`if entity.isDeleted { EmptyView() }` guard is actually re-evaluated when
the deletion merges. With `entity` as a plain `let`, that body never
re-runs on deletion, and the list keeps deleted entities on screen
(`ConsoleListViewModel.visibleEntities` is deliberately not refreshed
while the list is scrolled away from the top) — which would otherwise
move the same trap onto `text`, `session`, `taskId` or `httpHeaders`.
Reproduced with `removeAll()` (the console's "Remove Logs" action) and
with the automatic sweep: both leave a fetched entity `isDeleted` with
`value(forKey: "createdAt") == nil`, and reading `formattedTimestamp`
raises SIGTRAP before this change, returning "–" after it.
Owner
|
Good catch, thank you! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The console crashes with
EXC_BREAKPOINTwhen it renders an entity that the store has just deleted:Caught in the wild on 5.2.3 (iPhone SE 3, iOS 26.5), and
mainis identical here.Cause
LoggerMessageEntity.createdAtandNetworkTaskEntity.createdAtare non-optional@NSManaged Dates.deleteEntities(for:)batch-deletes rows and merges the deletions intoviewContextwithmergeChanges(fromRemoteContextSave:). Core Data then reports every attribute of those objects as NULL, soformattedTimestamp—timestampFormatter.string(from: createdAt)— force-bridgesnilintoDateand traps.Two things let a deleted entity reach a body evaluation:
ConsoleListViewModel.visibleEntitiesis a snapshot that is deliberately not refreshed while the list is scrolled away from the top (case .middle: break // Don't reload: too expensive and ruins gestures), so the list keeps rendering entities that no longer exist.ConsoleEntityCellalready guards this withif entity.isDeleted { EmptyView() }, butentityis a plainlet, so that body is never re-evaluated when the deletion merges. Only the inner cell — which holds the task as@ObservedObject— re-runs, and it re-runs straight into the trap.formattedTimestampis alazy var, so the trap needs a body evaluation that happens after the delete and before the timestamp was ever cached — a row materialising or laying out at that moment, which is what theDynamicLayoutComputerframes in the stack show.Fix
formattedTimestampreadscreatedAtthrough KVC, so a NULL value surfaces asniland renders"–"instead of trapping. This mirrors howstate(in:)already guardssessionafter Crash when hitting "Remove Logs" #242.ConsoleEntityCell.entitybecomes@ObservedObject, so the existingisDeletedguard is actually re-evaluated when the deletion merges and the deleted row leaves the screen. Without this, the same trap simply moves to another non-optional attribute (text,session,taskId,httpHeaders).Verification
Minimal harness (no UI): record 20 tasks in a
LoggerStore, fetch them onviewContext, delete, then read whatConsoleTaskCell:78reads.store.removeAll()— the console's Remove Logs actionisDeleted=true,value(forKey: "createdAt") = nil, readingformattedTimestamp→ SIGTRAP (exit 133)formattedTimestamp == "–", exit 0formattedTimestamp == "–", exit 0swift buildis clean, andxcodebuild -scheme PulseUI -destination 'generic/platform=iOS'succeeds.Unrelated to #373/#374: that one is an
NSInternalInconsistencyExceptionbecauseNSBatchDeleteRequestis unsupported on.inMemorystores, where the batch delete never executes. This one is a Swift trap on a SQLite-backed store where the delete succeeded. They're complementary — once #374 lands and in-memory stores also merge deletions into the view context, they will hit this same trap.