Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/filesync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 13 additions & 4 deletions crates/filesync/src/gui/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,13 +85,16 @@ fn session_loop(
}
}

let mut suspend_detector = SuspendDetector::new();

loop {
if stopped.load(Ordering::SeqCst) {
break;
}

if paused.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(250));
let _ = suspend_detector.check_for_resume();
continue;
}

Expand All @@ -100,9 +104,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(
Expand Down Expand Up @@ -140,7 +141,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));
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/filesync/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
71 changes: 71 additions & 0 deletions crates/filesync/src/suspend_detector.rs
Original file line number Diff line number Diff line change
@@ -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<Duration> {
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<Duration> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"system uptime reading not supported on this platform",
))
}
27 changes: 27 additions & 0 deletions crates/filesync/tests/test_suspend_detector.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading