fix(daemon): 串行化 bat-status.json 的读-改-写

watch 循环线程(update_daemon_status/update_daemon_progress)与 RPC 处理线程
(update_daemon_state_only)都会对状态文件做“读→改字段→写回”。单次写虽原子,
但整段读-改-写非原子,无互斥时并发线程会 last-writer-wins 丢失对方的字段更新。

新增进程级 DAEMON_STATUS_FILE_LOCK,三处读-改-写函数入口统一获取该锁串行化
(锁中毒时取 into_inner 继续,不 panic)。

对应 issue #18 维护清单 3-4。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 09:38:46 -07:00
co-authored by Claude Fable 5
parent 0656ad05c7
commit 232d74b617
@@ -3042,6 +3042,19 @@ fn read_daemon_status_file(path: &Path) -> anyhow::Result<Option<DaemonStatusFil
Ok(Some(serde_json::from_slice(&bytes)?))
}
/// 串行化 `bat-status.json` 的读-改-写。
///
/// watch 循环线程(`update_daemon_status`/`update_daemon_progress`)与 RPC 处理线程
/// `update_daemon_state_only`)都会更新状态文件;单次写虽原子,但“读→改字段→写回”
/// 整体非原子,无此锁会因 last-writer-wins 丢失并发线程的字段更新。
static DAEMON_STATUS_FILE_LOCK: Mutex<()> = Mutex::new(());
fn lock_daemon_status_file() -> std::sync::MutexGuard<'static, ()> {
DAEMON_STATUS_FILE_LOCK
.lock()
.unwrap_or_else(|poison| poison.into_inner())
}
fn write_daemon_status_file(path: &Path, status: &DaemonStatusFile) -> anyhow::Result<()> {
write_file_atomic(
path,
@@ -3054,6 +3067,7 @@ fn write_daemon_status_file(path: &Path, status: &DaemonStatusFile) -> anyhow::R
}
fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> anyhow::Result<()> {
let _guard = lock_daemon_status_file();
let status_path = daemon_status_path(state_dir);
let mut status = read_daemon_status_file(&status_path)?.unwrap_or_else(|| DaemonStatusFile {
version: DAEMON_STATUS_VERSION,
@@ -3099,6 +3113,7 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
}
fn update_daemon_progress(state_dir: &Path, event: &OfficialUpdateProgress) -> anyhow::Result<()> {
let _guard = lock_daemon_status_file();
let status_path = daemon_status_path(state_dir);
let Some(mut status) = read_daemon_status_file(&status_path)? else {
return Ok(());
@@ -3128,6 +3143,7 @@ fn update_daemon_progress(state_dir: &Path, event: &OfficialUpdateProgress) -> a
}
fn update_daemon_state_only(state_dir: &Path, state: &str) -> anyhow::Result<()> {
let _guard = lock_daemon_status_file();
let status_path = daemon_status_path(state_dir);
let Some(mut status) = read_daemon_status_file(&status_path)? else {
return Ok(());