//! Daemon core logic. //! //! The daemon is the main runtime that: //! - Manages a persistent Ghidra bridge process //! - Serves commands via local socket IPC //! - Handles graceful shutdown use std::path::PathBuf; use std::sync::Arc; use anyhow::{Context, Result}; use tokio::sync::{broadcast, Mutex}; use tracing::{error, info, warn}; use crate::daemon::process::{write_daemon_info, remove_lock_file, DaemonInfo, get_data_dir}; use crate::ghidra::bridge::GhidraBridge; pub mod cache; pub mod handler; pub mod ipc_server; pub mod process; pub mod queue; pub mod rpc; pub mod state; /// Daemon configuration. pub struct DaemonConfig { /// Path to the project directory pub project_path: PathBuf, /// RPC port (None = auto-select) - kept for backwards compatibility pub port: Option, /// Ghidra installation directory pub ghidra_install_dir: Option, /// Log file path pub log_file: PathBuf, /// Program name to load pub program_name: Option, } /// Run the daemon with the new bridge architecture. pub async fn run(config: DaemonConfig) -> Result<()> { info!("Starting Ghidra daemon"); info!("Project: {}", config.project_path.display()); // Get data directory let data_dir = get_data_dir() .context("Failed to get data directory")?; // Create shutdown channel let (shutdown_tx, _shutdown_rx) = broadcast::channel::<()>(1); // Initialize the Ghidra bridge (starts as None until we have a program) let bridge: Arc>> = Arc::new(Mutex::new(None)); // If we have Ghidra install dir and program name, start the bridge if let (Some(ghidra_dir), Some(program_name)) = (&config.ghidra_install_dir, &config.program_name) { info!("Starting Ghidra bridge for program: {}", program_name); let project_name = config.project_path .file_name() .and_then(|n| n.to_str()) .unwrap_or("project") .to_string(); let mut new_bridge = GhidraBridge::new( ghidra_dir.clone(), config.project_path.clone(), project_name, program_name.clone(), ); if let Err(e) = new_bridge.start() { warn!("Failed to start Ghidra bridge: {}. Will operate without persistent connection.", e); } else { info!("Ghidra bridge started successfully"); let mut bridge_guard = bridge.lock().await; *bridge_guard = Some(new_bridge); } } else { info!("No program specified, bridge will be started on first command"); } // Write lock file with a placeholder port (IPC doesn't use TCP ports) let placeholder_port = config.port.unwrap_or(0); let daemon_info = DaemonInfo::new(&config.project_path, placeholder_port, &config.log_file); write_daemon_info(&data_dir, &config.project_path, &daemon_info) .context("Failed to write lock file")?; // Start IPC server task let ipc_bridge = bridge.clone(); let ipc_shutdown_tx = shutdown_tx.clone(); let ipc_handle = tokio::spawn(async move { if let Err(e) = ipc_server::run_ipc_server(ipc_bridge, ipc_shutdown_tx).await { error!("IPC server error: {}", e); } }); // Also start the legacy RPC server for backwards compatibility let queue = Arc::new(queue::CommandQueue::new(config.project_path.clone(), bridge.clone())); let rpc_port = rpc::run_server(queue.clone(), config.port, shutdown_tx.clone()).await .context("Failed to start RPC server")?; info!("Legacy RPC server listening on port {} (for backwards compatibility)", rpc_port); // Wait for shutdown signal let shutdown_reason = wait_for_shutdown(shutdown_tx.clone()).await; info!("Shutdown initiated: {:?}", shutdown_reason); // Clean up shutdown_tx.send(()).ok(); // Signal all tasks to stop // Stop the bridge { let mut bridge_guard = bridge.lock().await; if let Some(mut b) = bridge_guard.take() { info!("Stopping Ghidra bridge..."); if let Err(e) = b.stop() { error!("Error stopping bridge: {}", e); } } } // Wait for IPC server to stop (with timeout) tokio::select! { _ = ipc_handle => { info!("IPC server stopped"); } _ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => { warn!("IPC server did not stop in time"); } } // Remove lock file remove_lock_file(&data_dir, &config.project_path) .context("Failed to remove lock file")?; info!("Daemon stopped"); Ok(()) } /// The reason for shutdown. #[derive(Debug, Clone)] pub enum ShutdownReason { /// SIGINT (Ctrl+C) Interrupt, /// SIGTERM Terminate, /// RPC shutdown request RpcRequest, } /// Wait for a shutdown signal. async fn wait_for_shutdown(shutdown_tx: broadcast::Sender<()>) -> ShutdownReason { let mut shutdown_rx = shutdown_tx.subscribe(); #[cfg(unix)] { use tokio::signal::unix::{signal, SignalKind}; let mut sigint = signal(SignalKind::interrupt()) .expect("Failed to register SIGINT handler"); let mut sigterm = signal(SignalKind::terminate()) .expect("Failed to register SIGTERM handler"); tokio::select! { _ = sigint.recv() => { info!("Received SIGINT"); ShutdownReason::Interrupt } _ = sigterm.recv() => { info!("Received SIGTERM"); ShutdownReason::Terminate } _ = shutdown_rx.recv() => { info!("Received shutdown request via RPC"); ShutdownReason::RpcRequest } } } #[cfg(windows)] { use tokio::signal; tokio::select! { _ = signal::ctrl_c() => { info!("Received Ctrl+C"); ShutdownReason::Interrupt } _ = shutdown_rx.recv() => { info!("Received shutdown request via RPC"); ShutdownReason::RpcRequest } } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_daemon_config() { let config = DaemonConfig { project_path: PathBuf::from("/test/project"), port: Some(17700), ghidra_install_dir: None, log_file: PathBuf::from("/test/logs/daemon.log"), program_name: None, }; assert_eq!(config.port, Some(17700)); } }