mirror of https://github.com/jackwener/wx-cli.git
fix(cli,config): 修复 sudo 下初始化失败 + daemon 不重载问题 (#37)
* fix(cli,config): 修复 sudo 下初始化失败 + daemon 不重载问题 - cli/transport: 新增 stop_daemon(),init 后自动停止旧 daemon - config: cli_dir() 优先读 SUDO_USER 环境变量,避免写到 /root/.wx-cli - config: auto_detect_db_dir() 按 .db 文件最新 mtime 排序,正确选最新目录 - daemon/server: dispatch 新增 ReloadConfig 命令(预留) - ipc: Request 新增 ReloadConfig 变体 - scanner/linux: 移除调试日志,清理 unused bail import * fix(config): resolve sudo home via passwd lookup --------- Co-authored-by: cjliu <cjliu@upointech.com> Co-authored-by: jackwener <jakevingoo@gmail.com>pull/21/merge
parent
6659f48984
commit
d750ef6e9f
|
|
@ -91,6 +91,10 @@ pub fn cmd_init(force: bool) -> Result<()> {
|
||||||
std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?)
|
std::fs::write(&config_path, serde_json::to_string_pretty(&cfg)?)
|
||||||
.context("写入 config.json 失败")?;
|
.context("写入 config.json 失败")?;
|
||||||
println!("配置已保存: {}", config_path.display());
|
println!("配置已保存: {}", config_path.display());
|
||||||
|
|
||||||
|
// init 之后必须停掉旧 daemon(它用的是旧 config),下次调用会自动重启
|
||||||
|
let _ = crate::cli::transport::stop_daemon();
|
||||||
|
|
||||||
println!("初始化完成,可以使用 wx sessions / wx history 等命令了");
|
println!("初始化完成,可以使用 wx sessions / wx history 等命令了");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,31 @@ pub fn ensure_daemon() -> Result<()> {
|
||||||
Ok(())
|
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::<u32>() {
|
||||||
|
#[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/` 可写,给出比"超时"更明确的错误。
|
/// 启动 daemon 前检查 `~/.wx-cli/` 可写,给出比"超时"更明确的错误。
|
||||||
///
|
///
|
||||||
/// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon
|
/// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon
|
||||||
|
|
|
||||||
109
src/config.rs
109
src/config.rs
|
|
@ -71,7 +71,8 @@ fn find_config_file() -> Result<PathBuf> {
|
||||||
return Ok(cwd);
|
return Ok(cwd);
|
||||||
}
|
}
|
||||||
// 3. ~/.wx-cli/config.json
|
// 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");
|
let p = home.join(".wx-cli").join("config.json");
|
||||||
if p.exists() {
|
if p.exists() {
|
||||||
return Ok(p);
|
return Ok(p);
|
||||||
|
|
@ -87,9 +88,44 @@ fn find_config_file() -> Result<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli_dir() -> PathBuf {
|
pub fn cli_dir() -> PathBuf {
|
||||||
dirs::home_dir()
|
cli_home_dir().join(".wx-cli")
|
||||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
}
|
||||||
.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>) -> PathBuf {
|
||||||
|
sudo_home.unwrap_or(default_home)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn sudo_user_home_dir() -> Option<PathBuf> {
|
||||||
|
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<PathBuf> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sock_path() -> PathBuf {
|
pub fn sock_path() -> PathBuf {
|
||||||
|
|
@ -154,17 +190,7 @@ pub fn auto_detect_db_dir() -> Option<PathBuf> {
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let home = dirs::home_dir()?;
|
let home = sudo_user_home_dir().or_else(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 base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files");
|
let base = home.join("Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files");
|
||||||
if !base.exists() {
|
if !base.exists() {
|
||||||
|
|
@ -190,9 +216,7 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let home = dirs::home_dir()?;
|
let home = dirs::home_dir()?;
|
||||||
let sudo_home = std::env::var("SUDO_USER").ok()
|
let sudo_home = sudo_user_home_dir();
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.map(|u| PathBuf::from("/home").join(u));
|
|
||||||
|
|
||||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||||
for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() {
|
for base_home in [Some(home.clone()), sudo_home].into_iter().flatten() {
|
||||||
|
|
@ -213,13 +237,32 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
candidates.sort_by_key(|p| {
|
candidates.sort_by_key(|p| {
|
||||||
std::fs::metadata(p)
|
// 排序:取 db_storage 目录下所有 .db 文件的最新 mtime,而非目录自身的 mtime
|
||||||
.and_then(|m| m.modified())
|
// 这样当收到新消息时(只有 .db 文件被更新),能正确识别最新目录
|
||||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
latest_db_mtime(p).unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
});
|
});
|
||||||
candidates.into_iter().next_back()
|
candidates.into_iter().next_back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 递归查找 db_storage 目录下所有 .db 文件的最新 mtime
|
||||||
|
fn latest_db_mtime(dir: &Path) -> Option<std::time::SystemTime> {
|
||||||
|
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")]
|
#[cfg(target_os = "windows")]
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
let appdata = std::env::var("APPDATA").ok()?;
|
let appdata = std::env::var("APPDATA").ok()?;
|
||||||
|
|
@ -257,3 +300,27 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
fn detect_db_dir_impl() -> Option<PathBuf> {
|
fn detect_db_dir_impl() -> Option<PathBuf> {
|
||||||
None
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -231,5 +231,8 @@ async fn dispatch(
|
||||||
Err(e) => Response::err(e.to_string()),
|
Err(e) => Response::err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ReloadConfig => {
|
||||||
|
Response::ok(serde_json::json!({ "reloading": true }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ pub enum Request {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
},
|
},
|
||||||
|
/// 重新加载配置和密钥(init --force 后 daemon 不会自动重读)
|
||||||
|
ReloadConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
/// 通过 /proc/<pid>/maps 枚举内存区域,
|
/// 通过 /proc/<pid>/maps 枚举内存区域,
|
||||||
/// 通过 /proc/<pid>/mem 读取内存内容,
|
/// 通过 /proc/<pid>/mem 读取内存内容,
|
||||||
/// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
|
/// 搜索 x'<64hex><32hex>' 格式的 SQLCipher 密钥
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue