pull/26/merge
shadow 2026-05-14 07:58:48 +00:00 committed by GitHub
commit e269ba9f44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 156 additions and 91 deletions

View File

@ -133,6 +133,8 @@ sudo wx init
wx init
```
**重新扫描密钥**`sudo wx init --force` 或 `wx init --force`)时,会在写入新密钥前**停止正在运行的 wx-daemon**,并**清空 `~/.wx-cli/cache` 解密缓存**(含 `_mtimes.json`),避免仅因 mtime 未变而继续复用按旧密钥解出的缓存文件。
验证安装:
```bash

View File

@ -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(())

View File

@ -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::<serde_json::Value>(&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,21 +110,35 @@ 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 失败")?;
println!("配置已保存: {}", config_path.display());
// init 之后必须停掉旧 daemon它用的是旧 config下次调用会自动重启
let _ = crate::cli::transport::stop_daemon();
let _ = stop_daemon();
println!("初始化完成,可以使用 wx sessions / wx history 等命令了");
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/`。
///
@ -133,7 +172,9 @@ fn drop_privileges_if_sudo() -> Result<()> {
}
// 设置 umask让后续 create 出来的文件/目录默认是 0600 / 0700。
unsafe { libc::umask(0o077); }
unsafe {
libc::umask(0o077);
}
// 必须先 setgid 再 setuid一旦 uid 降下来就没法再改 gid 了。
unsafe {
@ -157,8 +198,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());
}

View File

@ -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<StopDaemonOutcome> {
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() {
@ -62,31 +124,6 @@ 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::<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/` 可写,给出比"超时"更明确的错误。
///
/// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon
@ -138,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)))
@ -149,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 进程")?;
}
@ -161,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)))
@ -214,10 +258,11 @@ pub fn send(req: Request) -> Result<Response> {
fn send_unix(req: Request) -> Result<Response> {
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())?;
@ -226,8 +271,7 @@ fn send_unix(req: Request) -> Result<Response> {
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("未知错误"));
@ -240,10 +284,10 @@ fn send_unix(req: Request) -> Result<Response> {
fn send_windows(req: Request) -> Result<Response> {
use interprocess::local_socket::{prelude::*, GenericNamespaced, Stream};
let name = "wx-cli-daemon".to_ns_name::<GenericNamespaced>()
let name = "wx-cli-daemon"
.to_ns_name::<GenericNamespaced>()
.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);
@ -254,8 +298,7 @@ fn send_windows(req: Request) -> Result<Response> {
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("未知错误"));

View File

@ -245,6 +245,7 @@ fn detect_db_dir_impl() -> Option<PathBuf> {
}
/// 递归查找 db_storage 目录下所有 .db 文件的最新 mtime
#[cfg(target_os = "linux")]
fn latest_db_mtime(dir: &Path) -> Option<std::time::SystemTime> {
let mut latest = None;
if let Ok(entries) = std::fs::read_dir(dir) {