From a179b5022771f25596a5af09477fbfdc0a7cf00f Mon Sep 17 00:00:00 2001 From: shadow Date: Sun, 3 May 2026 11:22:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(init):=20--force=20=E6=97=B6=E5=81=9C?= =?UTF-8?q?=E6=AD=A2=20daemon=20=E5=B9=B6=E6=B8=85=E7=A9=BA=E8=A7=A3?= =?UTF-8?q?=E5=AF=86=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在成功扫描密钥、drop 到用户身份之后,若使用 --force,先调用与 `wx daemon stop` 相同的 stop_daemon 逻辑,再删除并重建 ~/.wx-cli/cache,避免 mtime 未变时仍复用旧密钥解出的缓存。 将停止 daemon 的逻辑提取到 transport::stop_daemon,供 init 与 daemon stop 子命令复用。 Co-authored-by: Cursor --- README.md | 2 + src/cli/daemon_cmd.rs | 57 ++++++++------------------ src/cli/init.rs | 66 ++++++++++++++++++++++++------ src/cli/transport.rs | 94 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 154 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index e0f06da..69bde76 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,8 @@ sudo wx init wx init ``` +**重新扫描密钥**(`sudo wx init --force` 或 `wx init --force`)时,会在写入新密钥前**停止正在运行的 wx-daemon**,并**清空 `~/.wx-cli/cache` 解密缓存**(含 `_mtimes.json`),避免仅因 mtime 未变而继续复用按旧密钥解出的缓存文件。 + 验证安装: ```bash diff --git a/src/cli/daemon_cmd.rs b/src/cli/daemon_cmd.rs index 31b0792..a900a1d 100644 --- a/src/cli/daemon_cmd.rs +++ b/src/cli/daemon_cmd.rs @@ -1,7 +1,7 @@ -use anyhow::Result; -use crate::config; -use crate::cli::DaemonCommands; use crate::cli::transport; +use crate::cli::DaemonCommands; +use crate::config; +use anyhow::Result; pub fn cmd_daemon(cmd: DaemonCommands) -> Result<()> { match cmd { @@ -25,42 +25,13 @@ fn cmd_status() -> Result<()> { } fn cmd_stop() -> Result<()> { - let pid_path = config::pid_path(); - if !pid_path.exists() { - println!("daemon 未运行"); - return Ok(()); - } - - let pid_str = std::fs::read_to_string(&pid_path)?; - let pid: u32 = pid_str.trim().parse() - .map_err(|_| anyhow::anyhow!("PID 文件格式错误"))?; - - #[cfg(unix)] - { - let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) }; - if ret != 0 { - let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); - if errno == libc::ESRCH { - println!("wx-daemon (PID {}) 已不在运行,清理残留文件", pid); - } else { - anyhow::bail!("发送 SIGTERM 失败 (errno {})", errno); - } - } else { - println!("已停止 wx-daemon (PID {})", pid); + match transport::stop_daemon()? { + transport::StopDaemonOutcome::NoPidFile => println!("daemon 未运行"), + transport::StopDaemonOutcome::Stopped(pid) => println!("已停止 wx-daemon (PID {})", pid), + transport::StopDaemonOutcome::StalePid(pid) => { + println!("wx-daemon (PID {}) 已不在运行,清理残留文件", pid); } } - - #[cfg(windows)] - { - std::process::Command::new("taskkill") - .args(["/PID", &pid.to_string(), "/F"]) - .output()?; - println!("已停止 wx-daemon (PID {})", pid); - } - - let _ = std::fs::remove_file(config::sock_path()); - let _ = std::fs::remove_file(&pid_path); - Ok(()) } @@ -89,19 +60,25 @@ fn cmd_logs(follow: bool, lines: usize) -> Result<()> { file.read_to_string(&mut content)?; let all_lines: Vec<&str> = content.lines().collect(); let show = &all_lines[all_lines.len().saturating_sub(lines)..]; - for line in show { println!("{}", line); } + for line in show { + println!("{}", line); + } loop { std::thread::sleep(std::time::Duration::from_millis(500)); let mut buf = String::new(); file.read_to_string(&mut buf)?; - if !buf.is_empty() { print!("{}", buf); } + if !buf.is_empty() { + print!("{}", buf); + } } } } else { let content = std::fs::read_to_string(&log_path)?; let all_lines: Vec<&str> = content.lines().collect(); let show = &all_lines[all_lines.len().saturating_sub(lines)..]; - for line in show { println!("{}", line); } + for line in show { + println!("{}", line); + } } Ok(()) diff --git a/src/cli/init.rs b/src/cli/init.rs index ece6af0..54acae4 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use serde_json::json; use std::collections::HashMap; +use crate::cli::transport::{stop_daemon, StopDaemonOutcome}; use crate::config; use crate::scanner; @@ -14,14 +15,20 @@ pub fn cmd_init(force: bool) -> Result<()> { if let Ok(content) = std::fs::read_to_string(&config_path) { if let Ok(cfg) = serde_json::from_str::(&content) { let db_dir = cfg.get("db_dir").and_then(|v| v.as_str()).unwrap_or(""); - let keys_file = cfg.get("keys_file").and_then(|v| v.as_str()).unwrap_or("all_keys.json"); + let keys_file = cfg + .get("keys_file") + .and_then(|v| v.as_str()) + .unwrap_or("all_keys.json"); let keys_path = if std::path::Path::new(keys_file).is_absolute() { std::path::PathBuf::from(keys_file) } else { - config_path.parent().unwrap_or(std::path::Path::new(".")) + config_path + .parent() + .unwrap_or(std::path::Path::new(".")) .join(keys_file) }; - if !db_dir.is_empty() && !db_dir.contains("your_wxid") + if !db_dir.is_empty() + && !db_dir.contains("your_wxid") && std::path::Path::new(db_dir).exists() && keys_path.exists() { @@ -50,6 +57,20 @@ pub fn cmd_init(force: bool) -> Result<()> { #[cfg(unix)] drop_privileges_if_sudo()?; + // --force:先停 daemon 并清空解密缓存,避免旧缓存与新密钥 mtime 一致仍被复用 + if force { + println!("(--force) 停止 wx-daemon 并清空解密缓存…"); + match stop_daemon()? { + StopDaemonOutcome::NoPidFile => {} + StopDaemonOutcome::Stopped(pid) => println!("已停止 wx-daemon (PID {})", pid), + StopDaemonOutcome::StalePid(pid) => { + println!("wx-daemon (PID {}) 已不在运行,已清理残留文件", pid); + } + } + clear_decrypt_cache()?; + println!("已清空 {}", config::cache_dir().display()); + } + // 确保父目录存在(如 ~/.wx-cli/),必须在任何写入之前 if let Some(parent) = config_path.parent() { std::fs::create_dir_all(parent) @@ -57,15 +78,19 @@ pub fn cmd_init(force: bool) -> Result<()> { } // Step 3: 保存 all_keys.json - let keys_file_path = config_path.parent() + let keys_file_path = config_path + .parent() .unwrap_or(std::path::Path::new(".")) .join("all_keys.json"); let mut keys_json = serde_json::Map::new(); for entry in &entries { - keys_json.insert(entry.db_name.clone(), json!({ - "enc_key": entry.enc_key, - })); + keys_json.insert( + entry.db_name.clone(), + json!({ + "enc_key": entry.enc_key, + }), + ); } std::fs::write(&keys_file_path, serde_json::to_string_pretty(&keys_json)?) .context("写入 all_keys.json 失败")?; @@ -85,8 +110,10 @@ pub fn cmd_init(force: bool) -> Result<()> { } } cfg.insert("db_dir".into(), json!(db_dir.to_string_lossy())); - cfg.entry("keys_file".into()).or_insert_with(|| json!("all_keys.json")); - cfg.entry("decrypted_dir".into()).or_insert_with(|| json!("decrypted")); + cfg.entry("keys_file".into()) + .or_insert_with(|| json!("all_keys.json")); + cfg.entry("decrypted_dir".into()) + .or_insert_with(|| json!("decrypted")); std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?) .context("写入 config.json 失败")?; @@ -96,6 +123,18 @@ pub fn cmd_init(force: bool) -> Result<()> { Ok(()) } +/// 删除 `~/.wx-cli/cache`(含 `_mtimes.json` 与已解密 DB),并重建空目录。 +fn clear_decrypt_cache() -> Result<()> { + let dir = config::cache_dir(); + if dir.exists() { + std::fs::remove_dir_all(&dir) + .with_context(|| format!("删除解密缓存目录失败: {}", dir.display()))?; + } + std::fs::create_dir_all(&dir) + .with_context(|| format!("创建解密缓存目录失败: {}", dir.display()))?; + Ok(()) +} + /// 如果当前以 root 身份运行且是通过 sudo 启动的,drop 到调用用户身份, /// 并迁移旧版本遗留的 root 属主 `~/.wx-cli/`。 /// @@ -129,7 +168,9 @@ fn drop_privileges_if_sudo() -> Result<()> { } // 设置 umask,让后续 create 出来的文件/目录默认是 0600 / 0700。 - unsafe { libc::umask(0o077); } + unsafe { + libc::umask(0o077); + } // 必须先 setgid 再 setuid:一旦 uid 降下来就没法再改 gid 了。 unsafe { @@ -153,8 +194,9 @@ fn drop_privileges_if_sudo() -> Result<()> { Ok(()) } fn chown_one(path: &Path, uid: u32, gid: u32) -> std::io::Result<()> { - let c = CString::new(path.as_os_str().as_bytes()) - .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL"))?; + let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL") + })?; if unsafe { libc::chown(c.as_ptr(), uid, gid) } != 0 { return Err(std::io::Error::last_os_error()); } diff --git a/src/cli/transport.rs b/src/cli/transport.rs index ab62da5..2a4624a 100644 --- a/src/cli/transport.rs +++ b/src/cli/transport.rs @@ -52,6 +52,68 @@ pub fn is_alive() -> bool { } } +/// [`stop_daemon`] 的返回值,供 CLI 提示或 `wx init --force` 选用。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StopDaemonOutcome { + /// 无 `daemon.pid` + NoPidFile, + /// 已向进程发送停止信号并清理 pid/socket + Stopped(u32), + /// 进程已不存在,仅清理 pid/socket + StalePid(u32), +} + +/// 停止 wx-daemon(与 `wx daemon stop` 同一套逻辑)。 +pub fn stop_daemon() -> Result { + let pid_path = config::pid_path(); + if !pid_path.exists() { + return Ok(StopDaemonOutcome::NoPidFile); + } + + let pid_str = std::fs::read_to_string(&pid_path)?; + let pid: u32 = pid_str + .trim() + .parse() + .map_err(|_| anyhow::anyhow!("PID 文件格式错误"))?; + + #[cfg(unix)] + { + let ret = unsafe { libc::kill(pid as libc::pid_t, libc::SIGTERM) }; + if ret != 0 { + let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0); + if errno == libc::ESRCH { + let _ = std::fs::remove_file(config::sock_path()); + let _ = std::fs::remove_file(&pid_path); + return Ok(StopDaemonOutcome::StalePid(pid)); + } + anyhow::bail!("发送 SIGTERM 失败 (errno {})", errno); + } + let _ = std::fs::remove_file(config::sock_path()); + let _ = std::fs::remove_file(&pid_path); + return Ok(StopDaemonOutcome::Stopped(pid)); + } + + #[cfg(windows)] + { + let out = std::process::Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .output(); + let ok = matches!(&out, Ok(o) if o.status.success()); + let _ = std::fs::remove_file(config::sock_path()); + let _ = std::fs::remove_file(&pid_path); + return Ok(if ok { + StopDaemonOutcome::Stopped(pid) + } else { + StopDaemonOutcome::StalePid(pid) + }); + } + + #[cfg(not(any(unix, windows)))] + { + anyhow::bail!("当前平台不支持 stop_daemon"); + } +} + /// 确保 daemon 运行,必要时自动启动 pub fn ensure_daemon() -> Result<()> { if is_alive() { @@ -113,7 +175,8 @@ fn start_daemon() -> Result<()> { let _ = std::fs::create_dir_all(parent); } let (stdout_stdio, stderr_stdio) = std::fs::OpenOptions::new() - .create(true).append(true) + .create(true) + .append(true) .open(&log_path) .and_then(|f| f.try_clone().map(|g| (f, g))) .map(|(f, g)| (std::process::Stdio::from(f), std::process::Stdio::from(g))) @@ -124,7 +187,12 @@ fn start_daemon() -> Result<()> { .stdout(stdout_stdio) .stderr(stderr_stdio); // SAFETY: setsid() 在 fork 后的子进程中调用,使 daemon 脱离控制终端 - unsafe { cmd.pre_exec(|| { libc::setsid(); Ok(()) }); } + unsafe { + cmd.pre_exec(|| { + libc::setsid(); + Ok(()) + }); + } let _ = cmd.spawn().context("无法启动 daemon 进程")?; } @@ -136,7 +204,8 @@ fn start_daemon() -> Result<()> { let _ = std::fs::create_dir_all(parent); } let (stdout_stdio, stderr_stdio) = std::fs::OpenOptions::new() - .create(true).append(true) + .create(true) + .append(true) .open(&log_path) .and_then(|f| f.try_clone().map(|g| (f, g))) .map(|(f, g)| (std::process::Stdio::from(f), std::process::Stdio::from(g))) @@ -189,10 +258,11 @@ pub fn send(req: Request) -> Result { fn send_unix(req: Request) -> Result { use std::os::unix::net::UnixStream; let sock_path = config::sock_path(); - let mut stream = UnixStream::connect(&sock_path) - .context("连接 daemon socket 失败")?; + let mut stream = UnixStream::connect(&sock_path).context("连接 daemon socket 失败")?; stream.set_read_timeout(Some(Duration::from_secs(120))).ok(); - stream.set_write_timeout(Some(Duration::from_secs(120))).ok(); + stream + .set_write_timeout(Some(Duration::from_secs(120))) + .ok(); let req_str = serde_json::to_string(&req)? + "\n"; stream.write_all(req_str.as_bytes())?; @@ -201,8 +271,7 @@ fn send_unix(req: Request) -> Result { let mut reader = BufReader::new(&stream); reader.read_line(&mut line)?; - let resp: Response = serde_json::from_str(&line) - .context("解析 daemon 响应失败")?; + let resp: Response = serde_json::from_str(&line).context("解析 daemon 响应失败")?; if !resp.ok { bail!("{}", resp.error.as_deref().unwrap_or("未知错误")); @@ -215,10 +284,10 @@ fn send_unix(req: Request) -> Result { fn send_windows(req: Request) -> Result { use interprocess::local_socket::{prelude::*, GenericNamespaced, Stream}; - let name = "wx-cli-daemon".to_ns_name::() + let name = "wx-cli-daemon" + .to_ns_name::() .context("构造 pipe name 失败")?; - let stream = Stream::connect(name) - .context("连接 daemon named pipe 失败")?; + let stream = Stream::connect(name).context("连接 daemon named pipe 失败")?; // interprocess::Stream 同时实现 Read + Write,但需要拆分读写端 let mut reader = BufReader::new(stream); @@ -229,8 +298,7 @@ fn send_windows(req: Request) -> Result { let mut line = String::new(); reader.read_line(&mut line)?; - let resp: Response = serde_json::from_str(&line) - .context("解析 daemon 响应失败")?; + let resp: Response = serde_json::from_str(&line).context("解析 daemon 响应失败")?; if !resp.ok { bail!("{}", resp.error.as_deref().unwrap_or("未知错误"));