From f1051591864b6da3d79b0225e3301aac518fdcee Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 7 Oct 2026 01:54:53 +0200 Subject: [PATCH] fix(cli,fuse): keep answering the systemd watchdog while the daemon retries The watchdog is armed when the process starts, but only the mount's supervisor pinged it. A daemon waiting for a login or backing off after a failed mount (#26: key unlock failing) was aborted after WatchdogSec and left a coredump instead of its error. Sleep through idle_sleep in those stretches; the mounted ping still has to pass the control-socket probe. --- crates/pdfs-cli/src/main.rs | 6 +++--- crates/pdfs-fuse/src/lib.rs | 1 + crates/pdfs-fuse/src/systemd.rs | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/pdfs-cli/src/main.rs b/crates/pdfs-cli/src/main.rs index 425a03a..4d54a8d 100644 --- a/crates/pdfs-cli/src/main.rs +++ b/crates/pdfs-cli/src/main.rs @@ -2027,7 +2027,7 @@ fn cmd_daemon(mountpoint: Option) -> Result<()> { loop { // Wait until a session is stored. The GUI enables this service on login, // but the service may also start at boot before the user has logged in. - wait_for_session(|| auth::load().map(|_| ()), std::thread::sleep)?; + wait_for_session(|| auth::load().map(|_| ()), pdfs_fuse::idle_sleep)?; match mount_once(mountpoint.clone()) { Ok(pdfs_fuse::MountOutcome::Shutdown) => { @@ -2036,13 +2036,13 @@ fn cmd_daemon(mountpoint: Option) -> Result<()> { } Ok(pdfs_fuse::MountOutcome::Unmounted) => { tracing::warn!("mount ended externally; remounting in 2s"); - std::thread::sleep(std::time::Duration::from_secs(2)); + pdfs_fuse::idle_sleep(std::time::Duration::from_secs(2)); } Err(e) => { // `{:#}` so the whole anyhow chain lands in the log — the top // context alone ("mount failed") never says *why*. tracing::error!(error = format!("{e:#}"), "mount failed; retrying in 5s"); - std::thread::sleep(std::time::Duration::from_secs(5)); + pdfs_fuse::idle_sleep(std::time::Duration::from_secs(5)); } } } diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index f53bea0..7e75e3b 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -123,6 +123,7 @@ use mount::{ }; use reads::{BlockFlight, BlockRing, PREFETCH_BUDGET, Prefetch, ReaderSlot, STREAM_BYPASS_MIN}; use state::{Entry, Intervals, PendingRevision, State, StateGuard, WriteHandle}; +pub use systemd::idle_sleep; use tracing::{debug, error, info, warn}; use transfers::{CountingWriter, JobGuard, OwnedCountingReader, TransferRegistry}; use r#virtual::{ diff --git a/crates/pdfs-fuse/src/systemd.rs b/crates/pdfs-fuse/src/systemd.rs index 741df5c..22683bc 100644 --- a/crates/pdfs-fuse/src/systemd.rs +++ b/crates/pdfs-fuse/src/systemd.rs @@ -74,6 +74,26 @@ pub(crate) fn watchdog_ping() { let _ = notify("WATCHDOG=1\n"); } +/// Sleep for `duration`, pinging the watchdog on the way. +/// +/// For the stretches where the daemon is alive but has no mount, and so no +/// supervisor: waiting for a login, or backing off after a failed mount. The +/// manager arms the watchdog when the process starts, so without these pings a +/// daemon that is merely retrying is aborted after `WatchdogSec` and leaves a +/// coredump instead of the error it was logging. Never use this while mounted: +/// there the ping must prove the control socket answers. +pub fn idle_sleep(duration: Duration) { + const CHUNK: Duration = Duration::from_secs(10); + let mut left = duration; + watchdog_ping(); + while !left.is_zero() { + let step = left.min(CHUNK); + std::thread::sleep(step); + left -= step; + watchdog_ping(); + } +} + /// Tell the manager the daemon is stopping, so a slow teardown is not mistaken /// for a hang by the watchdog it is racing. pub(crate) fn stopping() {