From 0656ad05c7aedfcd6cd90a6348800b3bdcbb9476 Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Thu, 16 Jul 2026 09:35:31 -0700 Subject: [PATCH] =?UTF-8?q?refactor(official-sync):=20CLI=20=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E5=8D=AB=E7=94=9F=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - doctor daemon_status 检查(3-3):ok 与 message 原分别取自解析结果和文件存在性, 文件存在但解析失败时会“ok=false 却提示可解析”。改为从同一次解析结果派生两者。 - localize_error_message(3-1a):空壳直通占位函数,删除并在 4 处调用点直接用消息。 - daemon 路径 helper 形参(3-1b):output_root 实际语义是 state_dir,统一改名消除误导。 - stop_daemon 非 RPC 路径(3-1c):删除无意义的 let-else(_pid 读后丢弃、两分支 逐字重复 stop_daemon_inner+print_report),stop_daemon_inner 内已做 PID 校验。 - parse_http_status(3-1d):改为锚定 curl stderr 中 "error" 之后再取三位状态码, 避免误取端口/字节数等无关三位数字;新增锚定测试。 - 非 Unix process_exists(3-1e):补注释说明保守返回 true 的理由(无法探测存活, 避免误回收可能仍在运行的 daemon)。 对应 issue #18 维护清单 3-1、3-3。 Co-Authored-By: Claude Fable 5 --- infrastructure/src/bin/bat_official_sync.rs | 63 +++++++++------------ infrastructure/src/curl_transfer.rs | 25 ++++++-- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/infrastructure/src/bin/bat_official_sync.rs b/infrastructure/src/bin/bat_official_sync.rs index 89c2746..47943eb 100644 --- a/infrastructure/src/bin/bat_official_sync.rs +++ b/infrastructure/src/bin/bat_official_sync.rs @@ -73,7 +73,7 @@ fn main() { let payload = ErrorReport { status: "error", exit_code, - error: localize_error_message(&error_message), + error: error_message, next_retry_seconds: None, }; eprintln!( @@ -396,7 +396,7 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> { DaemonStatusUpdate { state: "error", last_update_status: None, - last_error: Some(localize_error_message(&error.to_string())), + last_error: Some(error.to_string()), next_retry_seconds: Some(sleep_for.as_secs()), last_success_unix_seconds: None, next_check_unix_seconds: Some(unix_seconds_after(sleep_for)), @@ -409,13 +409,13 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> { format!( "本轮失败;将在 {} 后重试:{}", format_duration(sleep_for), - localize_error_message(&error.to_string()) + error ), ); let payload = ErrorReport { status: "error", exit_code: EXIT_ERROR, - error: localize_error_message(&error.to_string()), + error: error.to_string(), next_retry_seconds: Some(sleep_for.as_secs()), }; eprintln!("{}", serde_json::to_string_pretty(&payload)?); @@ -1533,13 +1533,8 @@ fn stop_daemon(state_dir: &Path, output_format: OutputFormat) -> anyhow::Result< } } - let pid_path = daemon_pid_path(state_dir); - let Some(_pid) = read_pid_file(&pid_path)? else { - let report = stop_daemon_inner(state_dir)?; - print_report(output_format, &report)?; - return Ok(()); - }; - + // 非 RPC 路径:stop_daemon_inner 内部会通过 classify_pid_lock_file 校验并分类 + // PID 文件(含 symlink 拒绝),无需在此重复读取。 let report = stop_daemon_inner(state_dir)?; print_report(output_format, &report)?; Ok(()) @@ -2810,16 +2805,15 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result { }); let status_path = daemon_status_path(&options.state_dir); + // ok 与 message 从同一次解析结果派生,避免“ok=false 却提示可解析”的自相矛盾。 + let daemon_status_result = read_daemon_status_file(&status_path); checks.push(DoctorCheck { name: "daemon_status", - ok: match read_daemon_status_file(&status_path) { - Ok(Some(_)) | Ok(None) => true, - Err(_) => false, - }, - message: if status_path.exists() { - format!("后台状态文件可解析:{}", status_path.display()) - } else { - "后台状态文件尚未创建".to_string() + ok: daemon_status_result.is_ok(), + message: match &daemon_status_result { + Ok(Some(_)) => format!("后台状态文件可解析:{}", status_path.display()), + Ok(None) => "后台状态文件尚未创建".to_string(), + Err(error) => format!("后台状态文件无法解析:{};{error}", status_path.display()), }, }); @@ -3154,20 +3148,20 @@ fn read_pid_file(path: &Path) -> anyhow::Result> { Ok(contents.trim().parse::().ok().filter(|pid| *pid > 0)) } -fn daemon_pid_path(output_root: &Path) -> PathBuf { - output_root.join(DAEMON_PID_FILE) +fn daemon_pid_path(state_dir: &Path) -> PathBuf { + state_dir.join(DAEMON_PID_FILE) } -fn daemon_status_path(output_root: &Path) -> PathBuf { - output_root.join(DAEMON_STATUS_FILE) +fn daemon_status_path(state_dir: &Path) -> PathBuf { + state_dir.join(DAEMON_STATUS_FILE) } -fn daemon_log_path(output_root: &Path) -> PathBuf { - output_root.join(DAEMON_LOG_FILE) +fn daemon_log_path(state_dir: &Path) -> PathBuf { + state_dir.join(DAEMON_LOG_FILE) } -fn daemon_structured_log_path(output_root: &Path) -> PathBuf { - output_root.join(DAEMON_STRUCTURED_LOG_FILE) +fn daemon_structured_log_path(state_dir: &Path) -> PathBuf { + state_dir.join(DAEMON_STRUCTURED_LOG_FILE) } fn rotated_structured_log_path(path: &Path, index: usize) -> PathBuf { @@ -3185,12 +3179,12 @@ fn rotated_structured_log_paths(path: &Path) -> Vec { .collect() } -fn daemon_socket_path(output_root: &Path) -> PathBuf { - output_root.join(DAEMON_SOCKET_FILE) +fn daemon_socket_path(state_dir: &Path) -> PathBuf { + state_dir.join(DAEMON_SOCKET_FILE) } -fn daemon_control_lock_path(output_root: &Path) -> PathBuf { - output_root.join(DAEMON_CONTROL_LOCK_FILE) +fn daemon_control_lock_path(state_dir: &Path) -> PathBuf { + state_dir.join(DAEMON_CONTROL_LOCK_FILE) } fn daemon_proxy_secret_path(state_dir: &Path) -> PathBuf { @@ -3436,6 +3430,9 @@ fn process_exists(pid: u32) -> bool { } } +// 非 Unix 平台无法用 kill(pid, 0) 探测进程存活;生产链路只支持 Linux。 +// 这里保守返回 true(假定进程仍存活),避免误把可能仍在运行的 daemon 的 +// PID/锁文件当作 stale 而回收——宁可要求手动清理,也不冒重复启动的风险。 #[cfg(not(unix))] fn process_exists(_pid: u32) -> bool { true @@ -3762,10 +3759,6 @@ fn is_locked_error(message: &str) -> bool { message.contains("locked") || message.contains("锁定") } -fn localize_error_message(message: &str) -> String { - message.to_string() -} - fn print_startup_banner() { eprintln!("{STARTUP_BANNER}"); } diff --git a/infrastructure/src/curl_transfer.rs b/infrastructure/src/curl_transfer.rs index 49f0e58..40dc88c 100644 --- a/infrastructure/src/curl_transfer.rs +++ b/infrastructure/src/curl_transfer.rs @@ -500,7 +500,12 @@ fn classify_failure( } fn parse_http_status(stderr: &str) -> Option { - let bytes = stderr.as_bytes(); + // curl 的 HTTP 状态错误形如 "curl: (22) The requested URL returned error: 404"。 + // 锚定到最后一个 "error" 之后再取三位状态码,避免误取 stderr 中其它三位数字 + // (如字节数、IP 片段)而错判 HTTP 状态。 + let anchor = stderr.rfind("error")?; + let tail = &stderr[anchor..]; + let bytes = tail.as_bytes(); let mut index = 0usize; while index < bytes.len() { if !bytes[index].is_ascii_digit() { @@ -512,9 +517,10 @@ fn parse_http_status(stderr: &str) -> Option { index += 1; } if index - start == 3 { - let status = stderr[start..index].parse::().ok()?; - if (100..=599).contains(&status) { - return Some(status); + if let Ok(status) = tail[start..index].parse::() { + if (100..=599).contains(&status) { + return Some(status); + } } } } @@ -541,6 +547,17 @@ mod tests { assert!(!failure.retryable()); } + #[test] + fn parse_http_status_anchors_on_curl_error_context() { + // 状态码前出现无关三位数字(如端口 443)时,仍从 "error" 之后取真正的状态码。 + assert_eq!( + parse_http_status("curl: (22) URL https://host:443/a returned error: 503"), + Some(503) + ); + // stderr 中不含 "error" 上下文时不猜测状态码。 + assert_eq!(parse_http_status("connected to host 200 ok"), None); + } + #[test] fn treats_5xx_as_retryable() { let status = std::process::ExitStatus::from_raw(22 << 8);