Commit Graph

3 Commits

Author SHA1 Message Date
eb69e491b9 fix(startup): surface a "taking too long" notice when the daemon won't come up
F3: the daemon connect loop retried forever with only an animated spinner when the
daemon was reachable-but-never-ready (stuck in RPC warmup / -28, or an external daemon
that never finishes init) -- no error, no guidance, no escape. It now stamps
connect_stall_since_ the moment the daemon first goes "reachable but not ready" (the
warmup branch + applyDaemonInitStatus) and clears it on connect / disconnect /
warmup-complete. A pure, unit-testable util::connectHasStalled() helper (new
util/connect_stall.h, 45s default from ui.toml [screens.loading].stall-timeout-sec)
drives a "Taking longer than expected" notice in renderLoadingOverlay(): a title, a
reassuring body with elapsed seconds, and a full-node hint to Settings > Restart Daemon
or the Console. The background retry keeps running underneath, so the notice self-clears
the instant it connects. Guarded off while the daemon is in State::Error (that case is
owned by the existing crash-count hint).

The overlay is a pure draw-list layer with no interactive widgets, so this follows the
existing crash-hint idiom (guidance text, not injected buttons); the stalled state is
computed locally in the overlay, so the only new App member is connect_stall_since_.

Adds testConnectHasStalled to test_phase4.cpp and three i18n keys to i18n.cpp (English
source of truth; the res/lang/*.json back-fill is deferred to a single
add_missing_translations.py run at the end of the batch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:33:26 -05:00
2675b8ab93 fix(startup): surface filesystem failures and verify Sapling param integrity
Three verified daemon-startup edge-case fixes centered on the config/params
filesystem path:

- F7: new non-throwing Platform::ensureDirectory(dir, outError) with one
  consistent "Cannot create <dir>: <reason>. Check permissions / free space."
  message. Replaces the unchecked/throwing create_directories sites at main.cpp
  (pre-init: log + Windows MessageBox + return 1), connection.cpp's
  autoDetectConfig (was the *throwing* overload -- could raise an uncaught
  filesystem_error through its callers; now sets the new
  ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via
  daemon_status_ + return false). The primary connect path (app_network.cpp)
  checks dir_error and shows it instead of mislabelling it "waiting for config".
  embedded_resources.cpp already checked its error_code, so it is left as-is.

- F6: verifySaplingParams() now hash-verifies each param against its pinned
  canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh)
  instead of only checking existence, so a truncated / corrupt-but-present param
  is rejected up front rather than failing later on a shielded operation. A
  <params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing
  ~48MB on every startup. Logic extracted to the injectable, unit-testable
  verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl).

- F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and
  the previously-dropped copy_file error_code in the daemon-binary fallback loop,
  aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed)
  instead of failing opaquely at spawn. An absent source file stays non-fatal.

Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp.
i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json
back-fill via add_missing_translations.py is deferred to a single run at the end
of the batch. Progress tracked in docs/daemon-startup-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:32 -05:00
b3444e0a89 fix(daemon): harden startup process lifecycle (crash race, exec failure, datadir lock)
Three verified daemon-startup edge-case fixes in the embedded-daemon process
lifecycle (all in embedded_daemon.{cpp,h}):

- F1: EmbeddedDaemon::isRunning() (POSIX) now reads the atomic state_ instead of
  calling waitpid(WNOHANG) from the UI thread, which raced monitorProcess()'s own
  reap. waitpid is one-shot: whichever thread won consumed the exit status; if
  isRunning() won, the monitor never saw the crash, so crash_count_/State::Error
  and the 3-strike restart cap were silently lost. monitorProcess() is now the sole
  reaper (predicate Running || Stopping keeps stop()'s wait loops correct). Mirrors
  the existing XmrigManager::isRunning() fix.

- F2: startProcess() (POSIX) adds a close-on-exec self-pipe exec handshake. On a
  non-executable / wrong-arch / corrupt binary, execv fails in the child and the
  parent now learns synchronously (reads errno vs EOF), reaps the zombie, sets a
  precise last_error_ ("not executable or wrong architecture"), and returns false
  -- instead of reporting State::Running for a daemon that never started. Uses
  pipe()+FD_CLOEXEC (not pipe2) so the branch stays shared with macOS. Parent-side
  setpgid is now best-effort + logged.

- F4: start() gates on a lingering datadir lock after the port check. A graceful
  shutdown releases the RPC port ~90s before the datadir .lock, so a rapid
  stop->start spawned a daemon that died on the lock and, three times in ~12s,
  tripped the 3-strike crash cap before the lock cleared. start() now polls
  isDaemonProcessRunning() with a bounded ~300ms wait and bails with a distinct
  non-crash Error (no crash_count_ bump) that the connect loop retries once the
  lock clears. Isolated migrate-to-seed starts (skip_port_check_ / -datadir
  override) are exempt.

Adds the testDatadirLockGate unit test (pure evaluateDatadirLockGate matrix) to
test_phase4.cpp. Plan and progress tracked in docs/daemon-startup-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 10:32:30 -05:00