2026-01-12 21:44:02 +00:00
|
|
|
//! Daemon core logic.
|
|
|
|
|
//!
|
|
|
|
|
//! The daemon is the main runtime that:
|
2026-01-20 16:15:02 -08:00
|
|
|
//! - Manages a persistent Ghidra bridge process
|
|
|
|
|
//! - Serves commands via local socket IPC
|
2026-01-12 21:44:02 +00:00
|
|
|
//! - Handles graceful shutdown
|
|
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
2026-01-20 16:15:02 -08:00
|
|
|
use tokio::sync::{broadcast, Mutex};
|
2026-01-12 21:44:02 +00:00
|
|
|
use tracing::{error, info, warn};
|
|
|
|
|
|
|
|
|
|
use crate::daemon::process::{write_daemon_info, remove_lock_file, DaemonInfo, get_data_dir};
|
2026-01-20 16:15:02 -08:00
|
|
|
use crate::ghidra::bridge::GhidraBridge;
|
2026-01-12 21:44:02 +00:00
|
|
|
|
|
|
|
|
pub mod cache;
|
2026-01-20 16:15:02 -08:00
|
|
|
pub mod handler;
|
|
|
|
|
pub mod ipc_server;
|
2026-01-12 21:44:02 +00:00
|
|
|
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,
|
2026-01-20 16:15:02 -08:00
|
|
|
/// RPC port (None = auto-select) - kept for backwards compatibility
|
2026-01-12 21:44:02 +00:00
|
|
|
pub port: Option<u16>,
|
|
|
|
|
/// Ghidra installation directory
|
|
|
|
|
pub ghidra_install_dir: Option<PathBuf>,
|
|
|
|
|
/// Log file path
|
|
|
|
|
pub log_file: PathBuf,
|
2026-01-20 16:15:02 -08:00
|
|
|
/// Program name to load
|
|
|
|
|
pub program_name: Option<String>,
|
2026-01-12 21:44:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-20 16:15:02 -08:00
|
|
|
/// Run the daemon with the new bridge architecture.
|
2026-01-12 21:44:02 +00:00
|
|
|
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);
|
|
|
|
|
|
2026-01-20 16:15:02 -08:00
|
|
|
// Initialize the Ghidra bridge (starts as None until we have a program)
|
|
|
|
|
let bridge: Arc<Mutex<Option<GhidraBridge>>> = Arc::new(Mutex::new(None));
|
2026-01-12 21:44:02 +00:00
|
|
|
|
2026-01-20 16:15:02 -08:00
|
|
|
// 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");
|
|
|
|
|
}
|
2026-01-12 21:44:02 +00:00
|
|
|
|
2026-01-20 16:15:02 -08:00
|
|
|
// 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);
|
2026-01-12 21:44:02 +00:00
|
|
|
write_daemon_info(&data_dir, &config.project_path, &daemon_info)
|
|
|
|
|
.context("Failed to write lock file")?;
|
|
|
|
|
|
2026-01-20 16:15:02 -08:00
|
|
|
// 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
|
2026-01-25 03:06:08 -08:00
|
|
|
let queue = Arc::new(queue::CommandQueue::new(config.project_path.clone(), bridge.clone()));
|
2026-01-20 16:15:02 -08:00
|
|
|
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);
|
2026-01-12 21:44:02 +00:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
2026-01-20 16:15:02 -08:00
|
|
|
// 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)
|
2026-01-12 21:44:02 +00:00
|
|
|
tokio::select! {
|
2026-01-20 16:15:02 -08:00
|
|
|
_ = ipc_handle => {
|
|
|
|
|
info!("IPC server stopped");
|
2026-01-12 21:44:02 +00:00
|
|
|
}
|
|
|
|
|
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
|
2026-01-20 16:15:02 -08:00
|
|
|
warn!("IPC server did not stop in time");
|
2026-01-12 21:44:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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"),
|
2026-01-20 16:15:02 -08:00
|
|
|
program_name: None,
|
2026-01-12 21:44:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert_eq!(config.port, Some(17700));
|
|
|
|
|
}
|
|
|
|
|
}
|