Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 17 additions & 40 deletions src/cli/daemon_cmd.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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(())
}

Expand Down Expand Up @@ -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(())
Expand Down
66 changes: 54 additions & 12 deletions src/cli/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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()
{
Expand Down Expand Up @@ -50,22 +57,40 @@ 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)
.with_context(|| format!("创建目录失败: {}", parent.display()))?;
}

// 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 失败")?;
Expand All @@ -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 失败")?;
Expand All @@ -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/`。
///
Expand Down Expand Up @@ -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 {
Expand All @@ -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());
}
Expand Down
94 changes: 81 additions & 13 deletions src/cli/transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)))
Expand All @@ -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 进程")?;
}

Expand All @@ -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)))
Expand Down Expand Up @@ -189,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())?;
Expand All @@ -201,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("未知错误"));
Expand All @@ -215,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);
Expand All @@ -229,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("未知错误"));
Expand Down