fix: ScanFiles skips bad entries; failed Realize latches engineError - #77
Merged
Conversation
Two robustness bugs found driving the DasherWatch spike (2026-08-31): 1. FileUtils::ScanFiles used the throwing directory_entry overloads - a single bad entry (dangling symlink, permission race, bundle metadata) aborted the whole alphabet scan with a filesystem_error, which propagated out of CAlphIO::ScanNameIndex and Realize(). 2. dasher_set_screen_size caught that Realize exception, logged it, and set realized=true anyway - leaving a half-built interface (null node model) that the next dasher_frame hit with DASHER_ASSERT(m_Root != NULL). Reproduced deterministically on the watchOS simulator (lldb: throw -> swallow -> assert) and with a host-side harness. ScanFiles now uses error_code overloads and skips failing entries; iteration failures end that search path instead of throwing. A failed Realize latches the RFC 0009 engineError state (frame/input no-op, dasher_has_engine_error reports it) and leaves realized=false so a later resize retries rather than continuing into a null-model assert. Signed-off-by: will wade <willwade@gmail.com>
willwade
added a commit
to dasher-project/Dasher-Apple
that referenced
this pull request
Aug 31, 2026
Root cause of the simulator crash (reproduced under lldb + host harness): SwiftUI multi-pass layout reports transient/fractional canvas sizes; realizing or resizing the engine at a degenerate geometry, plus a filesystem throw from the alphabet scan (fixed DasherCore-side in dasher-project/DasherCore#77), left realized=true with a null node model - dasher_frame then asserted on it. - setCanvasSize ignores sizes below a 32pt floor (transient passes); the boot realize only proceeds at a sane size, and orientation/input filter now follow a genuine realize (setting LP_ORIENTATION against an unrealized engine segfaults ChangeView - verified in the harness). - WatchBridge serializes every engine call behind engineLock (the TimelineView drives dasher_frame at 60fps from view-appear; realize runs detached) and exposes hasEngineError (RFC 0009). - The boot task surfaces engineError as the overlay error state instead of a dead canvas. - DasherCore pinned to dasher-project/DasherCore#77 (ScanFiles error_code overloads + failed-Realize latches engineError). Verified on the watchOS simulator: deterministic crash-on-launch -> 35s+ stable, multiple processes, canvas rendering (49 colours, 42% lit), zero new crash reports. Signed-off-by: will wade <willwade@gmail.com>
An empty/missing data directory (frontend passed a bad bundle path) made ScanFiles walk std::filesystem::current_path() - the user home directory - regexing every file for minutes while the caller held its engine lock. No data dir now means nothing to scan; realize proceeds with engine defaults and dasher_has_engine_error stays clean. Found on the watch spike: a nil Bundle resource path produced an empty dataDir, the fallback scanned the user Music folder, and the app sat on its loading spinner with the render loop blocked on the lock. Signed-off-by: will wade <willwade@gmail.com>
…can restart Greptile P1s (round 2) on #77: - A screen-size call that retries a previously failed Realize now clears engineError when the retry succeeds - the interface was rebuilt from scratch, so the latched fault is obsolete. Failed- Realize is the only latch path while !realized (mid-frame throws never re-enter the block), so this cannot mask a live fault. - ScanFiles: one bounded restart of a directory scan on a transient iteration error, resuming past the last successfully read entry, so later resources in that directory are not silently dropped. A persistently failing directory is abandoned; siblings and later directories are unaffected. Signed-off-by: will wade <willwade@gmail.com>
The bounded restart sat after the for-header increment, so a set error_code failed the loop condition before the recovery branch could execute - dead code (review P1, round 3 on #77). Increment and its recovery now live inside the loop body: a transient increment failure re-opens the directory once past the last successfully read entry; a second failure abandons that directory only. Verified against a hostile tree (locked subdirectory between valid resources): all later files still discovered. Signed-off-by: will wade <willwade@gmail.com>
Review follow-up (round 4 on #77) - both recovery paths made generally correct, not just regression-closed: - Realize retry: a failed Realize() leaves the interface incrementally mutated (CreateModules registers onto the existing module manager; other components rebuild in place), so retrying could retain or duplicate state. The retry now recreates dasher_ctx::Interface from scratch first (PointerInput is owned by the old interface module manager and is re-established by CreateModules; the settings store, screen, callbacks and pending alphabet live on the ctx and survive). Only then does Realize run on a provably fresh interface, making the post-retry engineError clear sound. - Directory recovery: the skip-past-last-name restart assumed a stable enumeration order across two opens of the same directory, which std::filesystem does not guarantee. The recovery now rescans the whole directory with an identity-based visited set (filename-keyed) - complete coverage regardless of order, and the rescan is idempotent (no double ParseFile). Hostile-tree verified (locked subdirectory mid-scan): all later resources still discovered, no engineError, steering frames survive. Signed-off-by: will wade <willwade@gmail.com>
dasher_set_low_memory_mode applied the flag to the interface object, so the transactional realize retry (which deletes and recreates it) silently reverted to normal-memory mode - a low-memory host could get the full filter set registered on retry. The flag is now retained on the dasher_ctx and reapplied immediately after recreation. Signed-off-by: will wade <willwade@gmail.com>
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.
Found while driving the DasherWatch spike (Dasher-Apple spike/apple-watch), reproduced under lldb and with a host-side harness.
Bug 1 - ScanFiles could abort the alphabet scan
FileUtils::ScanFilesused the throwingdirectory_entry::is_regular_file()inside the recursive scan. One bad entry (dangling symlink, permission race, bundle metadata) threw astd::filesystem::filesystem_errorout ofCAlphIO::ScanNameIndex->Realize()- killing engine startup. Now uses theerror_codeoverloads: failing entries are skipped, iteration failures end that search path cleanly.Bug 2 - a failed Realize left a live-looking engine
dasher_set_screen_sizewrappedRealize()in a catch that logged and set realized = true anyway. The nextdasher_framethen ranNewFrame -> Redraw -> RenderToViewagainst the half-built interface and died onDASHER_ASSERT(m_Root != NULL)(watchOS simulator, deterministic). Now a failed Realize latches the RFC 0009 engineError state (frame/input no-op,dasher_has_engine_error()reports it, frontends surface it) and leaves realized = false so a subsequent resize retries the realize instead of asserting.Verification
Greptile Summary
The PR makes resource scanning resilient to individual filesystem failures and makes failed realization retry against a fresh interface while preserving low-memory configuration.
Confidence Score: 5/5
The PR appears safe to merge.
No blocking failure remains.
Important Files Changed
Reviews (6): Last reviewed commit: "fix: retry reapplies low-memory mode to ..." | Re-trigger Greptile