use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use anyhow::Result;
use colored::*;
use serde::{Deserialize, Serialize};
use xore_config::XorePaths;
use super::watch::{is_process_running, read_pid};
pub enum AbyssAction {
Start {
force: bool,
exclude: Option<String>,
include: Option<String>,
},
Status,
Logs { lines: usize },
Stop,
Config { exclude: Option<String>, include: Option<String> },
}
pub struct AbyssArgs {
pub action: AbyssAction,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AbyssMeta {
pub pid: u32,
pub home_path: String,
pub started_at: String,
pub exclude_dirs: Vec<String>,
pub include_extensions: Vec<String>,
pub version: String,
}
#[derive(Debug)]
pub struct AbyssStatus {
pub running: bool,
pub pid: Option<u32>,
pub home_path: String,
pub started_at: Option<String>,
pub exclude_dirs: Vec<String>,
pub include_extensions: Vec<String>,
pub log_file: String,
}
fn get_abyss_runtime_dir() -> Result<PathBuf> {
let xore_paths = XorePaths::new().map_err(|e| anyhow::anyhow!("无法获取 XORE 路径: {}", e))?;
let dir = xore_paths.cache_dir().join("abyss");
fs::create_dir_all(&dir)?;
Ok(dir)
}
fn get_abyss_pid_file() -> Result<PathBuf> {
Ok(get_abyss_runtime_dir()?.join("abyss.pid"))
}
fn get_abyss_meta_file() -> Result<PathBuf> {
Ok(get_abyss_runtime_dir()?.join("abyss.meta.json"))
}
fn get_abyss_log_file() -> Result<PathBuf> {
Ok(get_abyss_runtime_dir()?.join("abyss.log"))
}
pub fn execute(args: AbyssArgs) -> Result<()> {
match args.action {
AbyssAction::Start { force, exclude, include } => start_abyss(force, exclude, include),
AbyssAction::Status => show_abyss_status(),
AbyssAction::Logs { lines } => show_abyss_logs(lines),
AbyssAction::Stop => stop_abyss(),
AbyssAction::Config { exclude, include } => show_or_update_config(exclude, include),
}
}
fn start_abyss(force: bool, exclude: Option<String>, include: Option<String>) -> Result<()> {
if let Some(existing_pid) = get_running_pid()? {
return Err(anyhow::anyhow!(
"Abyss 全局监控已在运行 (PID: {})\n提示:使用 'xore agent abyss --stop' 先停止",
existing_pid
));
}
check_permissions()?;
if !force {
let confirmed = show_warning_and_confirm()?;
if !confirmed {
println!("{} 已取消操作", "🚫".yellow());
return Ok(());
}
}
let exclude_dirs: Vec<String> = exclude
.as_deref()
.unwrap_or("Downloads,Desktop,Library,Movies,Music,Pictures")
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().to_string())
.collect();
let include_extensions: Vec<String> = include
.as_deref()
.unwrap_or("")
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().trim_start_matches("*.").trim_start_matches('.').to_lowercase())
.collect();
let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("无法获取用户主目录"))?;
let pid = spawn_abyss_daemon(&home_dir, &exclude_dirs, &include_extensions)?;
save_abyss_meta(pid, &home_dir, &exclude_dirs, &include_extensions)?;
println!("{} Abyss 全局监控已启动", "✅".green());
println!(" PID: {}", pid.to_string().yellow());
println!(" 路径: {}", home_dir.display().to_string().cyan());
println!(" 排除: {}", exclude_dirs.join(", ").dimmed());
if !include_extensions.is_empty() {
println!(" 仅监控: *.{}", include_extensions.join(", *.").cyan());
}
println!();
println!(
"提示:使用 {} 查看状态,{} 停止监控",
"xore agent abyss --status".cyan(),
"xore agent abyss --stop".yellow()
);
Ok(())
}
fn show_warning_and_confirm() -> Result<bool> {
println!("{}", "⚠️ 警告:全局文件监控".yellow().bold());
println!();
println!("此操作将监控您主目录下的所有文件变化。");
println!();
println!("{}", "影响:".red());
println!(" • 可能会消耗额外的 CPU 和内存资源");
println!(" • 监控范围包括您的所有文件(已排除常见无关目录)");
println!(" • 文件变化记录会保存在本地日志中");
println!();
println!("{}", "建议:".cyan());
println!(" 1. 使用 --exclude 排除不需要监控的目录");
println!(" 例如:--exclude \"Downloads,Desktop,Videos\"");
println!(" 2. 监控完成后及时使用 --stop 停止");
println!(" 3. 仅在需要时启动,避免长期运行");
println!();
print!("是否继续启动全局监控? [y/N]: ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_lowercase() == "y")
}
fn check_permissions() -> Result<()> {
#[cfg(target_os = "linux")]
{
let max_watches = fs::read_to_string("/proc/sys/fs/inotify/max_user_watches")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or(8192);
if max_watches < 10_000 {
eprintln!("{} inotify 监控数量限制较低 ({})", "⚠️".yellow(), max_watches);
eprintln!(" 建议提升限制:");
eprintln!(" echo 100000 | sudo tee /proc/sys/fs/inotify/max_user_watches");
eprintln!();
}
}
#[cfg(target_os = "macos")]
{
eprintln!(
"{} macOS 提示:如果监控不生效,请在「系统偏好设置」>「安全性与隐私」>「完全磁盘访问权限」中授权 XORE",
"ℹ️".cyan()
);
}
Ok(())
}
fn spawn_abyss_daemon(
home_dir: &Path,
exclude_dirs: &[String],
include_extensions: &[String],
) -> Result<u32> {
let current_exe =
std::env::current_exe().map_err(|e| anyhow::anyhow!("无法获取可执行文件路径: {}", e))?;
let log_file_path = get_abyss_log_file()?;
let cmd_args = vec![
"f".to_string(),
"--watch".to_string(),
"--index".to_string(),
"--path".to_string(),
home_dir.display().to_string(),
];
let exclude_env = exclude_dirs.join(",");
let include_env = include_extensions.join(",");
let log_file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
.map_err(|e| anyhow::anyhow!("无法创建日志文件 {:?}: {}", log_file_path, e))?;
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let log_clone = log_file.try_clone()?;
let mut log_writer = std::io::BufWriter::new(log_clone);
writeln!(
log_writer,
"\n=== Abyss daemon started at {} ===\n主目录: {}\n排除: {}\n",
now,
home_dir.display(),
exclude_env
)?;
drop(log_writer);
let mut cmd = std::process::Command::new(¤t_exe);
cmd.args(&cmd_args)
.env("XORE_ABYSS_EXCLUDE", &exclude_env)
.env("XORE_ABYSS_INCLUDE", &include_env)
.stdin(std::process::Stdio::null())
.stdout(log_file.try_clone()?)
.stderr(log_file);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
cmd.pre_exec(|| {
if libc::setsid() < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
let child = cmd.spawn().map_err(|e| anyhow::anyhow!("启动 Abyss 守护进程失败: {}", e))?;
let pid = child.id();
let pid_file = get_abyss_pid_file()?;
fs::write(&pid_file, pid.to_string())?;
drop(child);
Ok(pid)
}
fn save_abyss_meta(
pid: u32,
home_dir: &Path,
exclude_dirs: &[String],
include_extensions: &[String],
) -> Result<()> {
let meta_file = get_abyss_meta_file()?;
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let meta = AbyssMeta {
pid,
home_path: home_dir.display().to_string(),
started_at: now,
exclude_dirs: exclude_dirs.to_vec(),
include_extensions: include_extensions.to_vec(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
let json = serde_json::to_string_pretty(&meta)?;
fs::write(meta_file, json)?;
Ok(())
}
fn get_running_pid() -> Result<Option<u32>> {
let pid_file = get_abyss_pid_file()?;
let pid = match read_pid(&pid_file)? {
Some(p) => p,
None => return Ok(None),
};
if is_process_running(pid) {
Ok(Some(pid))
} else {
let _ = fs::remove_file(&pid_file);
Ok(None)
}
}
fn get_abyss_status() -> Result<AbyssStatus> {
let pid_file = get_abyss_pid_file()?;
let log_file = get_abyss_log_file()?;
let pid = read_pid(&pid_file)?;
let running = pid.map(is_process_running).unwrap_or(false);
let (home_path, started_at, exclude_dirs, include_extensions) =
if let Ok(meta_file) = get_abyss_meta_file() {
if meta_file.exists() {
let content = fs::read_to_string(&meta_file).unwrap_or_default();
let meta: Option<AbyssMeta> = serde_json::from_str(&content).ok();
if let Some(m) = meta {
(m.home_path, Some(m.started_at), m.exclude_dirs, m.include_extensions)
} else {
(
dirs::home_dir().map(|h| h.display().to_string()).unwrap_or_default(),
None,
vec![],
vec![],
)
}
} else {
(
dirs::home_dir().map(|h| h.display().to_string()).unwrap_or_default(),
None,
vec![],
vec![],
)
}
} else {
(
dirs::home_dir().map(|h| h.display().to_string()).unwrap_or_default(),
None,
vec![],
vec![],
)
};
Ok(AbyssStatus {
running,
pid,
home_path,
started_at,
exclude_dirs,
include_extensions,
log_file: log_file.display().to_string(),
})
}
fn show_abyss_status() -> Result<()> {
let status = get_abyss_status()?;
println!("{} Abyss 全局监控状态", "🌊".cyan());
println!();
if status.running {
println!(" 状态: {}", "运行中 🟢".green().bold());
if let Some(pid) = status.pid {
println!(" PID: {}", pid.to_string().yellow());
}
if let Some(ref started) = status.started_at {
println!(" 启动: {}", started.dimmed());
}
println!(" 路径: {}", status.home_path.cyan());
if !status.exclude_dirs.is_empty() {
println!(" 排除: {}", status.exclude_dirs.join(", ").dimmed());
}
if !status.include_extensions.is_empty() {
println!(" 仅监控: *.{}", status.include_extensions.join(", *.").cyan());
}
println!(" 日志: {}", status.log_file.dimmed());
} else {
println!(" 状态: {}", "未运行 🔴".red());
println!();
println!(" 提示:使用 {} 启动全局监控", "xore agent abyss --start".cyan());
}
Ok(())
}
fn show_abyss_logs(lines: usize) -> Result<()> {
let log_file_path = get_abyss_log_file()?;
if !log_file_path.exists() {
println!("{}", "没有找到 Abyss 日志文件".yellow());
println!("提示:先启动监控:{}", "xore agent abyss --start".cyan());
return Ok(());
}
println!("{} Abyss 监控日志(最后 {} 行)", "📋".cyan(), lines);
println!(" 文件: {}", log_file_path.display().to_string().dimmed());
println!();
let file = fs::File::open(&log_file_path)?;
let reader = BufReader::new(file);
let all_lines: Vec<String> = reader.lines().map_while(|l| l.ok()).collect();
let start = all_lines.len().saturating_sub(lines);
let display_lines = &all_lines[start..];
if display_lines.is_empty() {
println!("{}", "(日志为空)".dimmed());
} else {
for line in display_lines {
if line.contains("===") {
println!(" {}", line.cyan());
} else if line.to_lowercase().contains("error") || line.contains("ERROR") {
println!(" {}", line.red());
} else if line.to_lowercase().contains("warn") || line.contains("WARN") {
println!(" {}", line.yellow());
} else {
println!(" {}", line.dimmed());
}
}
}
let total_lines = all_lines.len();
if total_lines > lines {
println!();
println!(" {} 仅显示最后 {} 行(共 {} 行)", "ℹ️".cyan(), lines, total_lines);
}
Ok(())
}
fn stop_abyss() -> Result<()> {
let pid_file = get_abyss_pid_file()?;
let pid = match read_pid(&pid_file)? {
Some(p) => p,
None => {
println!("{}", "Abyss 全局监控未在运行".yellow());
return Ok(());
}
};
if !is_process_running(pid) {
cleanup_abyss_files()?;
println!("{} Abyss 守护进程已经停止(PID: {}),已清理文件", "✓".green(), pid);
return Ok(());
}
println!("{} 正在停止 Abyss 守护进程 (PID: {})...", "⏹️".yellow(), pid);
#[cfg(unix)]
unsafe {
use std::time::Duration;
libc::kill(pid as libc::pid_t, libc::SIGTERM);
for _ in 0..30 {
std::thread::sleep(Duration::from_millis(100));
if !is_process_running(pid) {
break;
}
}
if is_process_running(pid) {
eprintln!("{} SIGTERM 无效,强制终止...", "⚠️".yellow());
libc::kill(pid as libc::pid_t, libc::SIGKILL);
}
}
#[cfg(windows)]
{
let _ =
std::process::Command::new("taskkill").args(["/PID", &pid.to_string(), "/F"]).output();
}
cleanup_abyss_files()?;
println!("{} Abyss 全局监控已停止 (PID: {})", "✓".green(), pid);
Ok(())
}
fn cleanup_abyss_files() -> Result<()> {
if let Ok(pid_file) = get_abyss_pid_file() {
let _ = fs::remove_file(pid_file);
}
if let Ok(meta_file) = get_abyss_meta_file() {
let _ = fs::remove_file(meta_file);
}
Ok(())
}
fn show_or_update_config(exclude: Option<String>, include: Option<String>) -> Result<()> {
if exclude.is_none() && include.is_none() {
let status = get_abyss_status()?;
println!("{} Abyss 当前配置", "⚙️".cyan());
println!();
println!(" 默认排除目录:");
for dir in &status.exclude_dirs {
println!(" - {}", dir.dimmed());
}
println!();
if status.include_extensions.is_empty() {
println!(" 监控范围: 所有文件类型");
} else {
println!(" 仅监控扩展名: {}", status.include_extensions.join(", "));
}
println!();
println!("提示:重新启动时通过 --exclude 和 --include 参数重新配置");
} else {
println!("{} 配置已更新(将在下次启动时生效)", "ℹ️".cyan());
if let Some(ref exc) = exclude {
println!(" 排除: {}", exc);
}
if let Some(ref inc) = include {
println!(" 包含: {}", inc);
}
}
Ok(())
}
#[allow(dead_code)]
pub fn read_abyss_env_config() -> (Vec<String>, Vec<String>) {
let exclude_dirs: Vec<String> = std::env::var("XORE_ABYSS_EXCLUDE")
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
let include_extensions: Vec<String> = std::env::var("XORE_ABYSS_INCLUDE")
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
(exclude_dirs, include_extensions)
}
#[allow(dead_code)]
pub fn should_exclude_from_abyss(path: &Path, exclude_dirs: &[String]) -> bool {
let path_str = path.to_string_lossy();
for exclude in exclude_dirs {
if path_str.contains(exclude.as_str()) {
return true;
}
}
false
}
#[allow(dead_code)]
pub fn should_include_in_abyss(path: &Path, include_extensions: &[String]) -> bool {
if include_extensions.is_empty() {
return true; }
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
include_extensions.iter().any(|inc| inc == &ext)
}
#[allow(dead_code)]
pub fn get_abyss_stats() -> serde_json::Value {
match get_abyss_status() {
Ok(status) => serde_json::json!({
"running": status.running,
"pid": status.pid,
"home_path": status.home_path,
"started_at": status.started_at,
"exclude_dirs": status.exclude_dirs,
"include_extensions": status.include_extensions,
"log_file": status.log_file,
}),
Err(e) => serde_json::json!({
"error": e.to_string(),
"running": false,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_should_exclude_from_abyss() {
let exclude_dirs = vec!["Downloads".to_string(), "Library".to_string()];
assert!(
should_exclude_from_abyss(Path::new("/home/user/Downloads/file.txt"), &exclude_dirs),
"Downloads 应被排除"
);
assert!(
should_exclude_from_abyss(Path::new("/home/user/Library/prefs.plist"), &exclude_dirs),
"Library 应被排除"
);
assert!(
!should_exclude_from_abyss(Path::new("/home/user/Documents/file.txt"), &exclude_dirs),
"Documents 不应被排除"
);
}
#[test]
fn test_should_include_in_abyss_empty_list() {
assert!(should_include_in_abyss(Path::new("any.txt"), &[]));
assert!(should_include_in_abyss(Path::new("any.bin"), &[]));
}
#[test]
fn test_should_include_in_abyss_filter() {
let include_exts = vec!["rs".to_string(), "toml".to_string()];
assert!(should_include_in_abyss(Path::new("main.rs"), &include_exts), ".rs 应被包含");
assert!(should_include_in_abyss(Path::new("Cargo.toml"), &include_exts), ".toml 应被包含");
assert!(!should_include_in_abyss(Path::new("image.png"), &include_exts), ".png 不应被包含");
assert!(!should_include_in_abyss(Path::new("noext"), &include_exts), "无扩展名不应被包含");
}
#[test]
fn test_abyss_meta_serialization() {
let meta = AbyssMeta {
pid: 9999,
home_path: "/home/user".to_string(),
started_at: "2026-01-01T00:00:00Z".to_string(),
exclude_dirs: vec!["Downloads".to_string()],
include_extensions: vec!["rs".to_string()],
version: "1.2.0".to_string(),
};
let json = serde_json::to_string_pretty(&meta).unwrap();
let parsed: AbyssMeta = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.pid, 9999);
assert_eq!(parsed.home_path, "/home/user");
assert_eq!(parsed.exclude_dirs, vec!["Downloads"]);
assert_eq!(parsed.include_extensions, vec!["rs"]);
}
#[test]
fn test_read_abyss_env_config() {
let _guard = ENV_MUTEX.lock().unwrap_or_else(|p| p.into_inner());
std::env::set_var("XORE_ABYSS_EXCLUDE", "Downloads,Library");
std::env::set_var("XORE_ABYSS_INCLUDE", "rs,toml");
let (exclude, include) = read_abyss_env_config();
assert_eq!(exclude, vec!["Downloads", "Library"]);
assert_eq!(include, vec!["rs", "toml"]);
std::env::remove_var("XORE_ABYSS_EXCLUDE");
std::env::remove_var("XORE_ABYSS_INCLUDE");
}
#[test]
fn test_read_abyss_env_config_empty() {
let _guard = ENV_MUTEX.lock().unwrap_or_else(|p| p.into_inner());
std::env::remove_var("XORE_ABYSS_EXCLUDE");
std::env::remove_var("XORE_ABYSS_INCLUDE");
let (exclude, include) = read_abyss_env_config();
assert!(exclude.is_empty(), "未设置时应为空");
assert!(include.is_empty(), "未设置时应为空");
}
#[test]
fn test_abyss_status_command() {
let args = AbyssArgs { action: AbyssAction::Status };
let result = execute(args);
assert!(result.is_ok(), "status 命令应成功: {:?}", result);
}
#[test]
fn test_get_abyss_stats_not_running() {
let stats = get_abyss_stats();
assert!(stats.is_object());
assert!(stats.get("running").is_some());
}
#[test]
fn test_check_permissions_no_panic() {
let result = check_permissions();
assert!(result.is_ok(), "权限检查应成功: {:?}", result);
}
}