diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e89dd2f..61dca8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: restore-keys: ${{ runner.os }}-cargo- - name: Build release - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release --locked --target ${{ matrix.target }} - name: Rename binary (Unix) if: matrix.os != 'windows-latest' diff --git a/src/cli/transport.rs b/src/cli/transport.rs index 7f54e63..9d94627 100644 --- a/src/cli/transport.rs +++ b/src/cli/transport.rs @@ -66,13 +66,15 @@ fn start_daemon() -> Result<()> { #[cfg(unix)] { - let _ = std::process::Command::new(&exe) - .env("WX_DAEMON_MODE", "1") + use std::os::unix::process::CommandExt; + let mut cmd = std::process::Command::new(&exe); + cmd.env("WX_DAEMON_MODE", "1") .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - .context("无法启动 daemon 进程")?; + .stderr(std::process::Stdio::null()); + // SAFETY: setsid() 在 fork 后的子进程中调用,使 daemon 脱离控制终端 + unsafe { cmd.pre_exec(|| { libc::setsid(); Ok(()) }); } + let _ = cmd.spawn().context("无法启动 daemon 进程")?; } #[cfg(windows)] diff --git a/src/cli/watch.rs b/src/cli/watch.rs index cc8468d..ed54fa1 100644 --- a/src/cli/watch.rs +++ b/src/cli/watch.rs @@ -28,6 +28,11 @@ pub fn cmd_watch(chat: Option, json: bool) -> Result<()> { eprintln!("监听中(Ctrl+C 退出)...\n"); } + #[cfg(windows)] + { + anyhow::bail!("watch 命令在 Windows 上暂不支持,请使用 Unix 系统"); + } + #[cfg(unix)] { let reader = std::io::BufReader::new(stream.try_clone()?); diff --git a/src/daemon/cache.rs b/src/daemon/cache.rs index 6e7287c..4b742f4 100644 --- a/src/daemon/cache.rs +++ b/src/daemon/cache.rs @@ -11,15 +11,15 @@ use crate::crypto::wal; #[derive(Debug, Clone, Serialize, Deserialize)] struct MtimeEntry { - db_mt: f64, - wal_mt: f64, + db_mt: u64, + wal_mt: u64, path: String, } #[derive(Debug, Clone)] struct CacheEntry { - db_mtime: f64, - wal_mtime: f64, + db_mtime: u64, + wal_mtime: u64, decrypted_path: PathBuf, } @@ -83,10 +83,10 @@ impl DbCache { let wal_path_str = format!("{}-wal", db_path.display()); let wal_path = Path::new(&wal_path_str); - let db_mt = mtime_f64(&db_path); - let wal_mt = if wal_path.exists() { mtime_f64(wal_path) } else { 0.0 }; + let db_mt = mtime_nanos(&db_path); + let wal_mt = if wal_path.exists() { mtime_nanos(wal_path) } else { 0 }; - if (db_mt - entry.db_mt).abs() < 0.001 && (wal_mt - entry.wal_mt).abs() < 0.001 { + if db_mt == entry.db_mt && wal_mt == entry.wal_mt { inner.insert(rel_key.clone(), CacheEntry { db_mtime: db_mt, wal_mtime: wal_mt, @@ -138,15 +138,15 @@ impl DbCache { let wal_path_str = format!("{}-wal", db_path.display()); let wal_path = Path::new(&wal_path_str).to_path_buf(); - let db_mt = mtime_f64(&db_path); - let wal_mt = if wal_path.exists() { mtime_f64(&wal_path) } else { 0.0 }; + let db_mt = mtime_nanos(&db_path); + let wal_mt = if wal_path.exists() { mtime_nanos(&wal_path) } else { 0 }; // 检查缓存 { let inner = self.inner.lock().await; if let Some(entry) = inner.get(rel_key) { - if (entry.db_mtime - db_mt).abs() < 0.001 - && (entry.wal_mtime - wal_mt).abs() < 0.001 + if entry.db_mtime == db_mt + && entry.wal_mtime == wal_mt && entry.decrypted_path.exists() { return Ok(Some(entry.decrypted_path.clone())); @@ -195,11 +195,11 @@ impl DbCache { } } -fn mtime_f64(path: &Path) -> f64 { +fn mtime_nanos(path: &Path) -> u64 { std::fs::metadata(path) .and_then(|m| m.modified()) - .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs_f64()) - .unwrap_or(0.0) + .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos() as u64) + .unwrap_or(0) } fn hex_to_32bytes(s: &str) -> Result<[u8; 32]> { diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 72243ad..3058a99 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -1,6 +1,5 @@ pub mod cache; pub mod query; -pub mod watcher; pub mod server; use anyhow::Result; @@ -108,7 +107,7 @@ async fn run_watcher( use std::time::Duration; use crate::ipc::WatchEvent; - let mut last_mtime = 0.0f64; + let mut last_mtime = 0u64; let mut last_ts: HashMap = HashMap::new(); let mut initialized = false; @@ -119,11 +118,11 @@ async fn run_watcher( continue; } - let wal_mtime = match mtime_f64(&session_wal) { - Some(m) => m, - None => continue, + let wal_mtime = match mtime_nanos(&session_wal) { + 0 => continue, + m => m, }; - if (wal_mtime - last_mtime).abs() < 0.001 { + if wal_mtime == last_mtime { continue; } last_mtime = wal_mtime; @@ -206,10 +205,11 @@ async fn run_watcher( } } -fn mtime_f64(path: &std::path::Path) -> Option { - std::fs::metadata(path).ok()? - .modified().ok() - .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs_f64()) +fn mtime_nanos(path: &std::path::Path) -> u64 { + std::fs::metadata(path) + .and_then(|m| m.modified()) + .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos() as u64) + .unwrap_or(0) } fn decompress_or_str(data: &[u8]) -> String { diff --git a/src/daemon/query.rs b/src/daemon/query.rs index 29c6406..e8706f6 100644 --- a/src/daemon/query.rs +++ b/src/daemon/query.rs @@ -349,7 +349,7 @@ async fn find_msg_tables( return Ok(Vec::new()); } - let mut results = Vec::new(); + let mut results: Vec<(i64, std::path::PathBuf, String)> = Vec::new(); for rel_key in &names.msg_db_keys { let path = match db.get(rel_key).await? { Some(p) => p, @@ -357,9 +357,8 @@ async fn find_msg_tables( }; let tname = table_name.clone(); let path2 = path.clone(); - let exists: Option = tokio::task::spawn_blocking(move || { + let max_ts: Option = tokio::task::spawn_blocking(move || { let conn = Connection::open(&path2)?; - // 检查表是否存在 let table_exists: Option = conn.query_row( "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", [&tname], @@ -368,21 +367,22 @@ async fn find_msg_tables( if table_exists.is_none() { return Ok::<_, anyhow::Error>(None); } - let max_ts: Option = conn.query_row( + let ts: Option = conn.query_row( &format!("SELECT MAX(create_time) FROM [{}]", tname), [], |row| row.get(0), ).ok().flatten(); - Ok(max_ts) + Ok(ts) }).await??; - if exists.is_some() { - results.push((path.clone(), table_name.clone())); + if let Some(ts) = max_ts { + results.push((ts, path.clone(), table_name.clone())); } } - // 按最大时间戳排序(最新的优先) - Ok(results) + // 按最大时间戳降序排列(最新的优先) + results.sort_by_key(|(ts, _, _)| std::cmp::Reverse(*ts)); + Ok(results.into_iter().map(|(_, p, t)| (p, t)).collect()) } fn query_messages( diff --git a/src/daemon/watcher.rs b/src/daemon/watcher.rs deleted file mode 100644 index 9b44857..0000000 --- a/src/daemon/watcher.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; -use std::time::Duration; -use tokio::sync::broadcast; - -use super::cache::DbCache; -use super::query::{fmt_type, Names}; -use crate::ipc::WatchEvent; - -/// 启动 WAL 变化监听 task -/// -/// 每 500ms 检测 session.db-wal 的 mtime,有变化时重新读 session.db, -/// 找到 timestamp 更新的行,broadcast 到所有 watch 客户端 -#[allow(dead_code)] -pub async fn start_watcher( - db: &'static DbCache, - names_ref: &'static std::sync::RwLock, - tx: broadcast::Sender, - session_wal_path: PathBuf, -) { - tokio::spawn(async move { - let mut last_mtime = 0.0f64; - let mut last_ts: HashMap = HashMap::new(); - let mut initialized = false; - - loop { - tokio::time::sleep(Duration::from_millis(500)).await; - - // 如果没有订阅者,跳过 - if tx.receiver_count() == 0 { - continue; - } - - let wal_mtime = match mtime_f64(&session_wal_path) { - Some(m) => m, - None => continue, - }; - - if (wal_mtime - last_mtime).abs() < 0.001 { - continue; - } - last_mtime = wal_mtime; - - // 重新解密 session.db - let path = match db.get("session/session.db").await { - Ok(Some(p)) => p, - _ => continue, - }; - - let path2 = path.clone(); - let rows: Vec<(String, Vec, i64, i64, String)> = match tokio::task::spawn_blocking(move || { - let conn = rusqlite::Connection::open(&path2)?; - let mut stmt = conn.prepare( - "SELECT username, summary, last_timestamp, last_msg_type, last_msg_sender - FROM SessionTable WHERE last_timestamp > 0 - ORDER BY last_timestamp DESC LIMIT 50" - )?; - let rows = stmt.query_map([], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, Vec>(1).unwrap_or_default(), - row.get::<_, i64>(2)?, - row.get::<_, i64>(3).unwrap_or(0), - row.get::<_, String>(4).unwrap_or_default(), - )) - })? - .collect::>>()?; - Ok::<_, anyhow::Error>(rows) - }).await { - Ok(Ok(r)) => r, - _ => continue, - }; - - let names_guard = names_ref.read().expect("names lock poisoned"); - - for (username, summary_bytes, ts, msg_type, sender) in &rows { - if !initialized { - last_ts.insert(username.clone(), *ts); - continue; - } - - let prev_ts = last_ts.get(username).copied().unwrap_or(0); - if *ts <= prev_ts { - continue; - } - last_ts.insert(username.clone(), *ts); - - let display = names_guard.display(username); - let is_group = username.contains("@chatroom"); - - let summary = decompress_or_str(summary_bytes); - let summary = if summary.contains(":\n") { - summary.splitn(2, ":\n").nth(1).unwrap_or(&summary).to_string() - } else { - summary - }; - - let sender_display = if !sender.is_empty() { - names_guard.map.get(sender).cloned().unwrap_or_else(|| sender.clone()) - } else { - String::new() - }; - - let event = WatchEvent { - event: "message".into(), - time: Some(fmt_time_hhmm(*ts)), - chat: Some(display), - username: Some(username.clone()), - is_group: Some(is_group), - sender: Some(sender_display), - content: Some(summary), - msg_type: Some(fmt_type(*msg_type)), - timestamp: Some(*ts), - }; - - let _ = tx.send(event); - } - - if !initialized { - initialized = true; - } - } - }); -} - -fn mtime_f64(path: &std::path::Path) -> Option { - std::fs::metadata(path) - .and_then(|m| m.modified()) - .ok() - .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs_f64()) -} - -fn decompress_or_str(data: &[u8]) -> String { - if data.is_empty() { - return String::new(); - } - if let Ok(dec) = zstd::decode_all(data) { - if let Ok(s) = String::from_utf8(dec) { - return s; - } - } - String::from_utf8_lossy(data).into_owned() -} - -fn fmt_time_hhmm(ts: i64) -> String { - use chrono::{Local, TimeZone}; - Local.timestamp_opt(ts, 0) - .single() - .map(|dt| dt.format("%H:%M").to_string()) - .unwrap_or_else(|| ts.to_string()) -} diff --git a/src/scanner/linux.rs b/src/scanner/linux.rs index 7b70727..ba6f97b 100644 --- a/src/scanner/linux.rs +++ b/src/scanner/linux.rs @@ -87,8 +87,6 @@ pub fn scan_keys(db_dir: &Path) -> Result> { for (start, end) in ®ions { scan_region(&mut mem_file, *start, *end, &mut raw_keys); } - // 去重 - raw_keys.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1); eprintln!("找到 {} 个候选密钥", raw_keys.len()); let mut entries = Vec::new(); diff --git a/src/scanner/macos.rs b/src/scanner/macos.rs index f0734e7..ec7e988 100644 --- a/src/scanner/macos.rs +++ b/src/scanner/macos.rs @@ -193,8 +193,6 @@ fn scan_memory(task: mach_port_t) -> Result> { addr = addr.saturating_add(size); } - // 去重 - results.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1); Ok(results) } diff --git a/src/scanner/windows.rs b/src/scanner/windows.rs index 35e415f..a6660cb 100644 --- a/src/scanner/windows.rs +++ b/src/scanner/windows.rs @@ -130,7 +130,6 @@ fn scan_memory(process: HANDLE) -> Result> { } } - results.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1); Ok(results) }