mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:06:44 +08:00
refactor(official-sync): CLI 工程卫生清理
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,7 @@ fn main() {
|
|||||||
let payload = ErrorReport {
|
let payload = ErrorReport {
|
||||||
status: "error",
|
status: "error",
|
||||||
exit_code,
|
exit_code,
|
||||||
error: localize_error_message(&error_message),
|
error: error_message,
|
||||||
next_retry_seconds: None,
|
next_retry_seconds: None,
|
||||||
};
|
};
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -396,7 +396,7 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> {
|
|||||||
DaemonStatusUpdate {
|
DaemonStatusUpdate {
|
||||||
state: "error",
|
state: "error",
|
||||||
last_update_status: None,
|
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()),
|
next_retry_seconds: Some(sleep_for.as_secs()),
|
||||||
last_success_unix_seconds: None,
|
last_success_unix_seconds: None,
|
||||||
next_check_unix_seconds: Some(unix_seconds_after(sleep_for)),
|
next_check_unix_seconds: Some(unix_seconds_after(sleep_for)),
|
||||||
@@ -409,13 +409,13 @@ fn run_watch(options: CliOptions) -> anyhow::Result<()> {
|
|||||||
format!(
|
format!(
|
||||||
"本轮失败;将在 {} 后重试:{}",
|
"本轮失败;将在 {} 后重试:{}",
|
||||||
format_duration(sleep_for),
|
format_duration(sleep_for),
|
||||||
localize_error_message(&error.to_string())
|
error
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let payload = ErrorReport {
|
let payload = ErrorReport {
|
||||||
status: "error",
|
status: "error",
|
||||||
exit_code: EXIT_ERROR,
|
exit_code: EXIT_ERROR,
|
||||||
error: localize_error_message(&error.to_string()),
|
error: error.to_string(),
|
||||||
next_retry_seconds: Some(sleep_for.as_secs()),
|
next_retry_seconds: Some(sleep_for.as_secs()),
|
||||||
};
|
};
|
||||||
eprintln!("{}", serde_json::to_string_pretty(&payload)?);
|
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);
|
// 非 RPC 路径:stop_daemon_inner 内部会通过 classify_pid_lock_file 校验并分类
|
||||||
let Some(_pid) = read_pid_file(&pid_path)? else {
|
// PID 文件(含 symlink 拒绝),无需在此重复读取。
|
||||||
let report = stop_daemon_inner(state_dir)?;
|
|
||||||
print_report(output_format, &report)?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let report = stop_daemon_inner(state_dir)?;
|
let report = stop_daemon_inner(state_dir)?;
|
||||||
print_report(output_format, &report)?;
|
print_report(output_format, &report)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2810,16 +2805,15 @@ fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let status_path = daemon_status_path(&options.state_dir);
|
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 {
|
checks.push(DoctorCheck {
|
||||||
name: "daemon_status",
|
name: "daemon_status",
|
||||||
ok: match read_daemon_status_file(&status_path) {
|
ok: daemon_status_result.is_ok(),
|
||||||
Ok(Some(_)) | Ok(None) => true,
|
message: match &daemon_status_result {
|
||||||
Err(_) => false,
|
Ok(Some(_)) => format!("后台状态文件可解析:{}", status_path.display()),
|
||||||
},
|
Ok(None) => "后台状态文件尚未创建".to_string(),
|
||||||
message: if status_path.exists() {
|
Err(error) => format!("后台状态文件无法解析:{};{error}", status_path.display()),
|
||||||
format!("后台状态文件可解析:{}", status_path.display())
|
|
||||||
} else {
|
|
||||||
"后台状态文件尚未创建".to_string()
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3154,20 +3148,20 @@ fn read_pid_file(path: &Path) -> anyhow::Result<Option<u32>> {
|
|||||||
Ok(contents.trim().parse::<u32>().ok().filter(|pid| *pid > 0))
|
Ok(contents.trim().parse::<u32>().ok().filter(|pid| *pid > 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_pid_path(output_root: &Path) -> PathBuf {
|
fn daemon_pid_path(state_dir: &Path) -> PathBuf {
|
||||||
output_root.join(DAEMON_PID_FILE)
|
state_dir.join(DAEMON_PID_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_status_path(output_root: &Path) -> PathBuf {
|
fn daemon_status_path(state_dir: &Path) -> PathBuf {
|
||||||
output_root.join(DAEMON_STATUS_FILE)
|
state_dir.join(DAEMON_STATUS_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_log_path(output_root: &Path) -> PathBuf {
|
fn daemon_log_path(state_dir: &Path) -> PathBuf {
|
||||||
output_root.join(DAEMON_LOG_FILE)
|
state_dir.join(DAEMON_LOG_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_structured_log_path(output_root: &Path) -> PathBuf {
|
fn daemon_structured_log_path(state_dir: &Path) -> PathBuf {
|
||||||
output_root.join(DAEMON_STRUCTURED_LOG_FILE)
|
state_dir.join(DAEMON_STRUCTURED_LOG_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rotated_structured_log_path(path: &Path, index: usize) -> PathBuf {
|
fn rotated_structured_log_path(path: &Path, index: usize) -> PathBuf {
|
||||||
@@ -3185,12 +3179,12 @@ fn rotated_structured_log_paths(path: &Path) -> Vec<PathBuf> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_socket_path(output_root: &Path) -> PathBuf {
|
fn daemon_socket_path(state_dir: &Path) -> PathBuf {
|
||||||
output_root.join(DAEMON_SOCKET_FILE)
|
state_dir.join(DAEMON_SOCKET_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_control_lock_path(output_root: &Path) -> PathBuf {
|
fn daemon_control_lock_path(state_dir: &Path) -> PathBuf {
|
||||||
output_root.join(DAEMON_CONTROL_LOCK_FILE)
|
state_dir.join(DAEMON_CONTROL_LOCK_FILE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn daemon_proxy_secret_path(state_dir: &Path) -> PathBuf {
|
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))]
|
#[cfg(not(unix))]
|
||||||
fn process_exists(_pid: u32) -> bool {
|
fn process_exists(_pid: u32) -> bool {
|
||||||
true
|
true
|
||||||
@@ -3762,10 +3759,6 @@ fn is_locked_error(message: &str) -> bool {
|
|||||||
message.contains("locked") || message.contains("锁定")
|
message.contains("locked") || message.contains("锁定")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn localize_error_message(message: &str) -> String {
|
|
||||||
message.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_startup_banner() {
|
fn print_startup_banner() {
|
||||||
eprintln!("{STARTUP_BANNER}");
|
eprintln!("{STARTUP_BANNER}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -500,7 +500,12 @@ fn classify_failure(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_http_status(stderr: &str) -> Option<u16> {
|
fn parse_http_status(stderr: &str) -> Option<u16> {
|
||||||
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;
|
let mut index = 0usize;
|
||||||
while index < bytes.len() {
|
while index < bytes.len() {
|
||||||
if !bytes[index].is_ascii_digit() {
|
if !bytes[index].is_ascii_digit() {
|
||||||
@@ -512,9 +517,10 @@ fn parse_http_status(stderr: &str) -> Option<u16> {
|
|||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
if index - start == 3 {
|
if index - start == 3 {
|
||||||
let status = stderr[start..index].parse::<u16>().ok()?;
|
if let Ok(status) = tail[start..index].parse::<u16>() {
|
||||||
if (100..=599).contains(&status) {
|
if (100..=599).contains(&status) {
|
||||||
return Some(status);
|
return Some(status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,6 +547,17 @@ mod tests {
|
|||||||
assert!(!failure.retryable());
|
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]
|
#[test]
|
||||||
fn treats_5xx_as_retryable() {
|
fn treats_5xx_as_retryable() {
|
||||||
let status = std::process::ExitStatus::from_raw(22 << 8);
|
let status = std::process::ExitStatus::from_raw(22 << 8);
|
||||||
|
|||||||
Reference in New Issue
Block a user