From acc205fb7ef829e5ca55b726d9cf35d901321bf7 Mon Sep 17 00:00:00 2001 From: Guy Dols Date: Wed, 27 May 2026 21:47:13 +0200 Subject: [PATCH] Added suspension detection to filesync client, to restart connection --- crates/filesync/README.md | 2 + crates/filesync/src/gui/manager.rs | 21 +++--- crates/filesync/src/lib.rs | 1 + crates/filesync/src/suspend_detector.rs | 71 +++++++++++++++++++ .../filesync/tests/test_suspend_detector.rs | 27 +++++++ 5 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 crates/filesync/src/suspend_detector.rs create mode 100644 crates/filesync/tests/test_suspend_detector.rs diff --git a/crates/filesync/README.md b/crates/filesync/README.md index cb0cec8..4eb00ab 100644 --- a/crates/filesync/README.md +++ b/crates/filesync/README.md @@ -340,6 +340,8 @@ channel-based send/receive over TLS: - Normal failures: **1 s → 60 s**. - Rejected by server: **300 s**. - Pending approval: **30 s**. +- **Suspend/resume detection** (GUI mode): Monitors system uptime to reconnect + immediately after suspend. - Shutdown check granularity: **100 ms**. - **Initial sync order:** receive server files first, then send local files. - A dedicated **`recv-srv` thread** handles incoming messages during live sync. diff --git a/crates/filesync/src/gui/manager.rs b/crates/filesync/src/gui/manager.rs index 86f469b..38afc10 100644 --- a/crates/filesync/src/gui/manager.rs +++ b/crates/filesync/src/gui/manager.rs @@ -2,6 +2,7 @@ use crate::client::Client; use crate::exclusions::{ExclusionConfig, Exclusions}; use crate::gui::config::GuiConfig; use crate::gui::state::{ConnectionStatus, SharedState}; +use crate::suspend_detector::SuspendDetector; use crate::sync_engine::SyncEngine; use crate::timestamp_id; use std::path::PathBuf; @@ -72,10 +73,6 @@ fn session_loop( let node_id = format!("gui-{:x}", timestamp_id()); let engine = Arc::new(SyncEngine::new(cfg.sync_root.clone(), node_id, exclusions)); - // ── Eager local scan ────────────────────────────────────────────────── - // Show local file/dir/byte counts immediately, even before the first - // server session completes. This way the Stats panel is never stuck - // at zero while waiting for a connection. match engine.scan() { Ok(_) => { refresh_manifest_stats(&engine, &state); @@ -86,6 +83,8 @@ fn session_loop( } } + let mut suspend_detector = SuspendDetector::new(); + loop { if stopped.load(Ordering::SeqCst) { break; @@ -93,6 +92,7 @@ fn session_loop( if paused.load(Ordering::SeqCst) { thread::sleep(Duration::from_millis(250)); + let _ = suspend_detector.check_for_resume(); continue; } @@ -102,9 +102,6 @@ fn session_loop( s.log_event(format!("Connecting to {} …", cfg.server_addr)); } - // The stable identity certificate and known_servers.toml live in a - // "filesync" sub-directory under the GUI's config directory. - // Future path: ~/.config/bytehive/filesync/ let identity_dir: PathBuf = GuiConfig::config_dir().join("filesync"); let client = Client::new_standalone( @@ -142,7 +139,15 @@ fn session_loop( if stopped.load(Ordering::SeqCst) || paused.load(Ordering::SeqCst) { break; } - thread::sleep(Duration::from_millis(200)); + + if suspend_detector.check_for_resume() { + state + .write() + .log_event("System resumed from suspend — reconnecting immediately."); + break; + } + + thread::sleep(Duration::from_millis(1000)); } } diff --git a/crates/filesync/src/lib.rs b/crates/filesync/src/lib.rs index 98b35df..42c1883 100644 --- a/crates/filesync/src/lib.rs +++ b/crates/filesync/src/lib.rs @@ -8,6 +8,7 @@ pub mod known_hosts; pub mod manifest; pub mod protocol; pub mod server; +pub mod suspend_detector; pub mod sync_engine; pub mod transport; pub mod watcher; diff --git a/crates/filesync/src/suspend_detector.rs b/crates/filesync/src/suspend_detector.rs new file mode 100644 index 0000000..1edaaf1 --- /dev/null +++ b/crates/filesync/src/suspend_detector.rs @@ -0,0 +1,71 @@ +use std::fs; +use std::time::{Duration, Instant}; + +/// Detects system suspend/resume by comparing wall-clock time against system uptime. +/// When wall-clock advances significantly more than uptime, the system was suspended. +pub struct SuspendDetector { + last_check: Instant, + last_uptime: Duration, +} + +impl SuspendDetector { + pub fn new() -> Self { + let uptime = read_system_uptime().unwrap_or(Duration::ZERO); + Self { + last_check: Instant::now(), + last_uptime: uptime, + } + } + + pub fn check_for_resume(&mut self) -> bool { + let now = Instant::now(); + let wall_elapsed = now.duration_since(self.last_check); + + let current_uptime = match read_system_uptime() { + Ok(uptime) => uptime, + Err(_) => { + self.last_check = now; + return false; + } + }; + + let uptime_elapsed = current_uptime.saturating_sub(self.last_uptime); + + const SUSPEND_THRESHOLD_SECS: u64 = 5; + let suspended = wall_elapsed.as_secs() > uptime_elapsed.as_secs() + SUSPEND_THRESHOLD_SECS; + + self.last_check = now; + self.last_uptime = current_uptime; + + suspended + } +} + +impl Default for SuspendDetector { + fn default() -> Self { + Self::new() + } +} + +#[cfg(target_os = "linux")] +fn read_system_uptime() -> std::io::Result { + let contents = fs::read_to_string("/proc/uptime")?; + let uptime_str = contents + .split_whitespace() + .next() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "empty uptime"))?; + + let uptime_secs: f64 = uptime_str + .parse() + .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid uptime"))?; + + Ok(Duration::from_secs_f64(uptime_secs)) +} + +#[cfg(not(target_os = "linux"))] +fn read_system_uptime() -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "system uptime reading not supported on this platform", + )) +} diff --git a/crates/filesync/tests/test_suspend_detector.rs b/crates/filesync/tests/test_suspend_detector.rs new file mode 100644 index 0000000..9dba99b --- /dev/null +++ b/crates/filesync/tests/test_suspend_detector.rs @@ -0,0 +1,27 @@ +use bytehive_filesync::suspend_detector::SuspendDetector; +use std::time::Duration; + +#[test] +fn detector_initializes() { + let _detector = SuspendDetector::new(); +} + +#[test] +fn detector_default_trait() { + let _detector = SuspendDetector::default(); +} + +#[test] +fn check_for_resume_does_not_panic() { + let mut detector = SuspendDetector::new(); + std::thread::sleep(Duration::from_millis(10)); + let _result = detector.check_for_resume(); +} + +#[test] +fn check_for_resume_normal_operation() { + let mut detector = SuspendDetector::new(); + std::thread::sleep(Duration::from_millis(50)); + let resumed = detector.check_for_resume(); + assert!(!resumed); +}