mirror of https://github.com/jackwener/wx-cli.git
Merge 29d716e80f into c284b4ade6
commit
e269ba9f44
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("未知错误"));
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue