From 847d53f46207a8475ee0df4c526faf6c70b35da4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 22:00:03 +0000 Subject: [PATCH] Implement proper daemon background mode for Unix and Windows Add true background daemonization that detaches from terminal and allows the process to continue after terminal closes. Unix implementation: - Use daemonize crate for proper fork/detach/setsid - Redirect stdout/stderr to log file - Create PID file for process tracking - Change working directory to root Windows implementation: - Spawn detached child process with CREATE_NO_WINDOW flag - Pass --foreground flag to child to run in foreground internally - Parent process exits immediately after spawn Resolves the TODO at main.rs:180 for proper daemonization. --- Cargo.lock | 10 +++++ Cargo.toml | 4 ++ src/main.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2a23b9..be05541 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "daemonize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +dependencies = [ + "libc", +] + [[package]] name = "difflib" version = "0.4.0" @@ -572,6 +581,7 @@ dependencies = [ "clap", "comfy-table", "csv", + "daemonize", "dirs", "dunce", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 21d6e69..dbe7480 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,10 @@ strsim = "0.11" dunce = "1.0" # Windows path handling atty = "0.2" # TTY detection +# Daemonization (Unix only) +[target.'cfg(unix)'.dependencies] +daemonize = "0.5" + [dev-dependencies] assert_cmd = "2.0" predicates = "3.0" diff --git a/src/main.rs b/src/main.rs index 0fba6ca..6ee3341 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,9 @@ use query::{Query, DataType, FieldSelector, SortKey}; use std::path::PathBuf; use tracing::{info, error}; +#[cfg(unix)] +use daemonize::Daemonize; + #[tokio::main] async fn main() { // Initialize logging @@ -177,11 +180,22 @@ async fn handle_daemon_start(project: Option, port: Option, foregro println!("Starting daemon in foreground mode..."); run_daemon(daemon_config).await?; } else { - // TODO: Daemonize properly (fork, detach, etc.) - // For now, just run in foreground + // Run in background - platform-specific daemonization println!("Starting daemon for project: {}", project_path.display()); - println!("Note: Background mode not yet implemented, running in foreground"); - run_daemon(daemon_config).await?; + + #[cfg(unix)] + { + daemonize_unix(daemon_config)?; + } + + #[cfg(windows)] + { + daemonize_windows(daemon_config, port)?; + } + + println!("Daemon started successfully"); + println!(" Log file: {}", data_dir.join("daemon.log").display()); + println!(" Use 'ghidra daemon status' to check daemon status"); } Ok(()) @@ -313,6 +327,87 @@ async fn handle_daemon_clear_cache(project: Option) -> anyhow::Result<() Ok(()) } +/// Daemonize on Unix systems using fork and detach. +#[cfg(unix)] +fn daemonize_unix(daemon_config: DaemonConfig) -> anyhow::Result<()> { + use std::fs::OpenOptions; + + let log_file_path = daemon_config.log_file.clone(); + let project_path = daemon_config.project_path.clone(); + + // Open log file for stdout/stderr + let log_file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_file_path)?; + + let stdout = log_file.try_clone()?; + let stderr = log_file; + + // Get PID file path - use hash of project path like lock file + let data_dir = get_data_dir()?; + let project_hash = format!("{:x}", md5::compute(project_path.to_string_lossy().as_bytes())); + let pid_file = data_dir.join(format!("daemon-{}.pid", project_hash)); + + // Configure daemonization + let daemonize = Daemonize::new() + .pid_file(pid_file) + .working_directory("/") + .stdout(stdout) + .stderr(stderr); + + // Fork and daemonize + daemonize.start() + .map_err(|e| anyhow::anyhow!("Failed to daemonize: {}", e))?; + + // We're now in the daemon process - initialize tokio runtime and run + let runtime = tokio::runtime::Runtime::new()?; + runtime.block_on(async { + run_daemon(daemon_config).await + })?; + + Ok(()) +} + +/// Daemonize on Windows by spawning a detached process. +#[cfg(windows)] +fn daemonize_windows(daemon_config: DaemonConfig, port: Option) -> anyhow::Result<()> { + use std::process::Command; + + // Get the current executable path + let exe_path = std::env::current_exe()?; + + // Build the command to spawn ourselves with --foreground flag + let mut cmd = Command::new(exe_path); + cmd.arg("daemon") + .arg("start") + .arg("--foreground"); + + // Add project path + cmd.arg("--project") + .arg(daemon_config.project_path.to_string_lossy().to_string()); + + // Add port if specified + if let Some(p) = port { + cmd.arg("--port").arg(p.to_string()); + } + + // Windows-specific: CREATE_NO_WINDOW flag + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + const DETACHED_PROCESS: u32 = 0x00000008; + cmd.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS); + } + + // Spawn the detached process + cmd.spawn() + .map_err(|e| anyhow::anyhow!("Failed to spawn daemon process: {}", e))?; + + Ok(()) +} + fn handle_query(args: QueryArgs) -> anyhow::Result<()> { let config = Config::load()?; let client = GhidraClient::new(config.clone())?;