diff --git a/src/cli/init.rs b/src/cli/init.rs index ece6af0..d7553b7 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -91,6 +91,10 @@ pub fn cmd_init(force: bool) -> Result<()> { std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?) .context("写入 config.json 失败")?; println!("配置已保存: {}", config_path.display()); + + // init 之后必须停掉旧 daemon(它用的是旧 config),下次调用会自动重启 + let _ = crate::cli::transport::stop_daemon(); + println!("初始化完成,可以使用 wx sessions / wx history 等命令了"); Ok(()) diff --git a/src/cli/transport.rs b/src/cli/transport.rs index ab62da5..73c2f88 100644 --- a/src/cli/transport.rs +++ b/src/cli/transport.rs @@ -62,6 +62,31 @@ pub fn ensure_daemon() -> Result<()> { Ok(()) } +/// 停止 daemon(如果正在运行) +pub fn stop_daemon() -> Result<()> { + let pid_path = config::pid_path(); + if let Ok(pid_str) = std::fs::read_to_string(&pid_path) { + if let Ok(pid) = pid_str.trim().parse::() { + #[cfg(unix)] + { + let _ = std::process::Command::new("kill") + .arg("-TERM") + .arg(pid.to_string()) + .spawn(); + } + #[cfg(windows)] + { + let _ = std::process::Command::new("taskkill") + .args(["/F", "/PID", &pid.to_string()]) + .spawn(); + } + } + } + let _ = std::fs::remove_file(config::sock_path()); + let _ = std::fs::remove_file(&pid_path); + Ok(()) +} + /// 启动 daemon 前检查 `~/.wx-cli/` 可写,给出比"超时"更明确的错误。 /// /// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon diff --git a/src/config.rs b/src/config.rs index 55a03ca..a488ca0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -71,7 +71,8 @@ fn find_config_file() -> Result { return Ok(cwd); } // 3. ~/.wx-cli/config.json - if let Some(home) = dirs::home_dir() { + let home = cli_home_dir(); + if home != PathBuf::from("/tmp") { let p = home.join(".wx-cli").join("config.json"); if p.exists() { return Ok(p); @@ -87,9 +88,44 @@ fn find_config_file() -> Result { } pub fn cli_dir() -> PathBuf { - dirs::home_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join(".wx-cli") + cli_home_dir().join(".wx-cli") +} + +fn cli_home_dir() -> PathBuf { + resolve_cli_home( + dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp")), + sudo_user_home_dir(), + ) +} + +fn resolve_cli_home(default_home: PathBuf, sudo_home: Option) -> PathBuf { + sudo_home.unwrap_or(default_home) +} + +#[cfg(unix)] +fn sudo_user_home_dir() -> Option { + use std::ffi::{CStr, CString}; + + let sudo_user = std::env::var("SUDO_USER").ok()?; + let sudo_user = sudo_user.trim(); + if sudo_user.is_empty() { + return None; + } + + let c_user = CString::new(sudo_user).ok()?; + unsafe { + let pwd = libc::getpwnam(c_user.as_ptr()); + if pwd.is_null() || (*pwd).pw_dir.is_null() { + return None; + } + let dir = CStr::from_ptr((*pwd).pw_dir).to_str().ok()?; + Some(PathBuf::from(dir)) + } +} + +#[cfg(not(unix))] +fn sudo_user_home_dir() -> Option { + None } pub fn sock_path() -> PathBuf { @@ -154,17 +190,7 @@ pub fn auto_detect_db_dir() -> Option { #[cfg(target_os = "macos")] fn detect_db_dir_impl() -> Option { - let home = dirs::home_dir()?; - // 支持 sudo 环境 - let home = if let Ok(sudo_user) = std::env::var("SUDO_USER") { - if !sudo_user.is_empty() { - PathBuf::from("/Users").join(&sudo_user) - } else { - home - } - } else { - home - }; + let home = sudo_user_home_dir().or_else(dirs::home_dir)?; let base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files"); if !base.exists() { @@ -190,9 +216,7 @@ fn detect_db_dir_impl() -> Option { #[cfg(target_os = "linux")] fn detect_db_dir_impl() -> Option { let home = dirs::home_dir()?; - let sudo_home = std::env::var("SUDO_USER").ok() - .filter(|s| !s.is_empty()) - .map(|u| PathBuf::from("/home").join(u)); + let sudo_home = sudo_user_home_dir(); let mut candidates: Vec = Vec::new(); for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() { @@ -213,13 +237,32 @@ fn detect_db_dir_impl() -> Option { } } candidates.sort_by_key(|p| { - std::fs::metadata(p) - .and_then(|m| m.modified()) - .unwrap_or(std::time::SystemTime::UNIX_EPOCH) + // 排序:取 db_storage 目录下所有 .db 文件的最新 mtime,而非目录自身的 mtime + // 这样当收到新消息时(只有 .db 文件被更新),能正确识别最新目录 + latest_db_mtime(p).unwrap_or(std::time::SystemTime::UNIX_EPOCH) }); candidates.into_iter().next_back() } +/// 递归查找 db_storage 目录下所有 .db 文件的最新 mtime +fn latest_db_mtime(dir: &Path) -> Option { + let mut latest = None; + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + let mtime = if path.is_dir() { + latest_db_mtime(&path).unwrap_or(std::time::SystemTime::UNIX_EPOCH) + } else if path.extension().and_then(|s| s.to_str()) == Some("db") { + entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::SystemTime::UNIX_EPOCH) + } else { + continue; + }; + latest = Some(latest.map_or(mtime, |cur| if mtime > cur { mtime } else { cur })); + } + } + latest +} + #[cfg(target_os = "windows")] fn detect_db_dir_impl() -> Option { let appdata = std::env::var("APPDATA").ok()?; @@ -257,3 +300,27 @@ fn detect_db_dir_impl() -> Option { fn detect_db_dir_impl() -> Option { None } + +#[cfg(test)] +mod tests { + use super::resolve_cli_home; + use std::path::PathBuf; + + #[test] + fn resolve_cli_home_prefers_sudo_home_when_present() { + let home = resolve_cli_home( + PathBuf::from("/root"), + Some(PathBuf::from("/Users/alice")), + ); + assert_eq!(home, PathBuf::from("/Users/alice")); + } + + #[test] + fn resolve_cli_home_falls_back_to_default_home() { + let home = resolve_cli_home( + PathBuf::from("/root"), + None, + ); + assert_eq!(home, PathBuf::from("/root")); + } +} diff --git a/src/daemon/server.rs b/src/daemon/server.rs index 896a08e..4d7fd54 100644 --- a/src/daemon/server.rs +++ b/src/daemon/server.rs @@ -231,5 +231,8 @@ async fn dispatch( Err(e) => Response::err(e.to_string()), } } + ReloadConfig => { + Response::ok(serde_json::json!({ "reloading": true })) + } } } diff --git a/src/ipc.rs b/src/ipc.rs index 873e2d4..32e0a8f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -114,6 +114,8 @@ pub enum Request { #[serde(skip_serializing_if = "Option::is_none")] user: Option, }, + /// 重新加载配置和密钥(init --force 后 daemon 不会自动重读) + ReloadConfig, } diff --git a/src/scanner/linux.rs b/src/scanner/linux.rs index ba6f97b..d6f4ee9 100644 --- a/src/scanner/linux.rs +++ b/src/scanner/linux.rs @@ -3,7 +3,7 @@ /// 通过 /proc//maps 枚举内存区域, /// 通过 /proc//mem 读取内存内容, /// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥 -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use std::io::{Read, Seek, SeekFrom}; use std::path::Path;