Skip to content

fix: ScanFiles skips bad entries; failed Realize latches engineError - #77

Merged
willwade merged 6 commits into
mainfrom
fix/scanfiles-ec-realize-error
Aug 31, 2026
Merged

fix: ScanFiles skips bad entries; failed Realize latches engineError#77
willwade merged 6 commits into
mainfrom
fix/scanfiles-ec-realize-error

Conversation

@willwade

@willwade willwade commented Aug 31, 2026

Copy link
Copy Markdown

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::ScanFiles used the throwing directory_entry::is_regular_file() inside the recursive scan. One bad entry (dangling symlink, permission race, bundle metadata) threw a std::filesystem::filesystem_error out of CAlphIO::ScanNameIndex -> Realize() - killing engine startup. Now uses the error_code overloads: failing entries are skipped, iteration failures end that search path cleanly.

Bug 2 - a failed Realize left a live-looking engine

dasher_set_screen_size wrapped Realize() in a catch that logged and set realized = true anyway. The next dasher_frame then ran NewFrame -> Redraw -> RenderToView against the half-built interface and died on DASHER_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

  • watchOS simulator: crash-on-launch (assert) -> 3 stable processes, canvas rendering, no new crash reports
  • Host harness (macOS static libs, trimmed watch data bundle, German alphabet restore): previously-reproducing paths survive

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.

  • Replaces recursive filesystem traversal with an error-code-based, per-directory scan and bounded order-independent recovery.
  • Latches realization failures as engine errors so frame and input calls remain safe.
  • Recreates the interface transactionally on retry and reapplies the screen and low-memory request.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/CAPI.cpp Failed realization now latches the engine fault, while retry rebuilds the interface and restores low-memory and screen configuration before clearing the fault.
src/DasherCore/FileUtils.cpp Resource discovery now isolates entry and directory errors and performs one order-independent rescan without parsing already visited entries twice.

Reviews (6): Last reviewed commit: "fix: retry reapplies low-memory mode to ..." | Re-trigger Greptile

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>
Comment thread src/CAPI.cpp
Comment thread src/DasherCore/FileUtils.cpp Outdated
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>
Comment thread src/CAPI.cpp
Comment thread src/DasherCore/FileUtils.cpp Outdated
…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>
Comment thread src/DasherCore/FileUtils.cpp Outdated
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>
Comment thread src/DasherCore/FileUtils.cpp Outdated
Comment thread src/CAPI.cpp
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>
Comment thread src/CAPI.cpp
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>
@willwade
willwade merged commit fc33180 into main Aug 31, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant