Skip to content

Commit 42c9705

Browse files
committed
Harden UI lease updates against transient file loss
1 parent 0985b02 commit 42c9705

2 files changed

Lines changed: 38 additions & 17 deletions

File tree

src/service.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -731,25 +731,36 @@ fn spawn_ui_lease_watchdog(
731731
return;
732732
}
733733
let stale_after = Duration::from_secs(8);
734+
let mut missing_since: Option<u64> = None;
734735
loop {
735736
tokio::time::sleep(Duration::from_secs(2)).await;
736737

738+
let now = SystemTime::now()
739+
.duration_since(UNIX_EPOCH)
740+
.map(|duration| duration.as_secs())
741+
.unwrap_or(0);
737742
let Some(lease) = state::read_ui_lease(&paths).ok().flatten() else {
743+
let first_missing_at = *missing_since.get_or_insert(now);
744+
let missing_for = now.saturating_sub(first_missing_at);
745+
if missing_for < stale_after.as_secs() {
746+
continue;
747+
}
738748
ui_managed_shutdown.store(true, Ordering::SeqCst);
739749
let _ = runtime_log::append(
740750
&paths,
741751
"WARN",
742752
"ui-watchdog",
743-
"ui lease missing while daemon is ui-managed; requesting shutdown",
753+
&format!(
754+
"ui lease missing for {}s while daemon is ui-managed; requesting shutdown",
755+
missing_for
756+
),
744757
);
745758
let _ = shutdown_tx.send(true);
746759
break;
747760
};
761+
missing_since = None;
748762

749-
let now = SystemTime::now()
750-
.duration_since(UNIX_EPOCH)
751-
.map(|duration| duration.as_secs())
752-
.unwrap_or(lease.updated_at);
763+
let now = now.max(lease.updated_at);
753764
let stale = now.saturating_sub(lease.updated_at) >= stale_after.as_secs();
754765
let owner_dead = !is_process_running(lease.owner_pid);
755766
if stale || owner_dead {

src/state.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use std::fs;
2+
#[cfg(target_os = "windows")]
3+
use std::os::windows::ffi::OsStrExt;
24
use std::path::Path;
35
use std::time::{SystemTime, UNIX_EPOCH};
46

@@ -8,6 +10,8 @@ use serde::{Deserialize, Serialize};
810
use crate::paths::AppPaths;
911
use crate::platform::is_process_running;
1012
use crate::platform::sync_user_ownership;
13+
#[cfg(target_os = "windows")]
14+
use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MoveFileExW};
1115

1216
#[derive(Debug, Clone, Serialize, Deserialize)]
1317
pub struct ServiceState {
@@ -229,18 +233,24 @@ fn replace_file(path: &Path, content: &[u8]) -> Result<()> {
229233
fs::write(&tmp_path, content)
230234
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
231235

232-
if path.exists() {
233-
match fs::remove_file(path) {
234-
Ok(()) => {}
235-
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
236-
Err(error) => {
237-
let _ = fs::remove_file(&tmp_path);
238-
return Err(error).with_context(|| format!("failed to replace {}", path.display()));
239-
}
240-
}
241-
}
236+
move_file_replace(&tmp_path, path).with_context(|| {
237+
format!("failed to move {} to {}", tmp_path.display(), path.display())
238+
})?;
239+
Ok(())
240+
}
242241

243-
fs::rename(&tmp_path, path)
244-
.with_context(|| format!("failed to move {} to {}", tmp_path.display(), path.display()))?;
242+
#[cfg(target_os = "windows")]
243+
fn move_file_replace(src: &Path, dst: &Path) -> Result<()> {
244+
let src_wide: Vec<u16> = src.as_os_str().encode_wide().chain(Some(0)).collect();
245+
let dst_wide: Vec<u16> = dst.as_os_str().encode_wide().chain(Some(0)).collect();
246+
let ok = unsafe { MoveFileExW(src_wide.as_ptr(), dst_wide.as_ptr(), MOVEFILE_REPLACE_EXISTING) };
247+
if ok == 0 {
248+
return Err(std::io::Error::last_os_error()).context("MoveFileExW failed");
249+
}
245250
Ok(())
246251
}
252+
253+
#[cfg(not(target_os = "windows"))]
254+
fn move_file_replace(src: &Path, dst: &Path) -> Result<()> {
255+
fs::rename(src, dst).context("rename failed")
256+
}

0 commit comments

Comments
 (0)