This commit is contained in:
Alexander Kiselev
2026-02-04 14:04:32 -08:00
parent 005581c960
commit ac1fb7b931
43 changed files with 3988 additions and 4650 deletions
Generated
+1 -118
View File
@@ -498,12 +498,6 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "doctest-file"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562"
[[package]]
name = "document-features"
version = "0.2.12"
@@ -640,30 +634,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fslock"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -671,7 +641,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -726,7 +695,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -785,12 +753,11 @@ dependencies = [
"dirs",
"dunce",
"env_logger",
"fslock",
"futures-util",
"indicatif",
"insta",
"interprocess",
"lazy_static",
"libc",
"log",
"md5",
"once_cell",
@@ -799,7 +766,6 @@ dependencies = [
"predicates",
"proptest",
"regex",
"remoc",
"reqwest",
"serde",
"serde_json",
@@ -1140,21 +1106,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "interprocess"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d"
dependencies = [
"doctest-file",
"futures-core",
"libc",
"recvmsg",
"tokio",
"widestring",
"windows-sys 0.52.0",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -1751,12 +1702,6 @@ dependencies = [
"rand_core 0.9.4",
]
[[package]]
name = "recvmsg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1806,37 +1751,6 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "remoc"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b862dbacf3e0cad8d9031e799ba77db2f6f1f65a2480c3d3310fab8b3b1f499b"
dependencies = [
"byteorder",
"bytes",
"futures",
"rand",
"remoc_macro",
"serde",
"serde_json",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "remoc_macro"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b929c6b6255d8d80205d79d80f41093adf0d14089e51c74ff5b850f2160b0677"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "reqwest"
version = "0.11.27"
@@ -2181,16 +2095,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.8"
@@ -2423,25 +2327,11 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.6.1",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
@@ -2634,7 +2524,6 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
dependencies = [
"getrandom 0.3.4",
"js-sys",
"serde_core",
"wasm-bindgen",
]
@@ -2808,12 +2697,6 @@ dependencies = [
"winsafe",
]
[[package]]
name = "widestring"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"
+3 -11
View File
@@ -34,21 +34,12 @@ log = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Async runtime
tokio = { version = "1.35", features = ["full"] }
# RPC
remoc = { version = "0.16", features = ["full"] }
# IPC (local sockets)
interprocess = { version = "2.2", features = ["tokio"] }
# Async runtime (only needed for setup command's HTTP downloads)
tokio = { version = "1.35", features = ["rt", "io-util", "time", "net"] }
# Time
chrono = { version = "0.4", features = ["serde"] }
# File locking
fslock = "0.2"
# Hashing
md5 = "0.7"
@@ -59,6 +50,7 @@ walkdir = "2.4"
# Process management
which = "6.0"
libc = "0.2"
# Regex
regex = "1.10"
-155
View File
@@ -1,155 +0,0 @@
//! Caching layer for common requests.
//!
//! Caches results of expensive Ghidra operations to speed up repeated queries.
#![allow(dead_code)]
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::debug;
use crate::cli::Commands;
/// A cached entry with timestamp.
struct CacheEntry {
value: String,
inserted_at: Instant,
}
impl CacheEntry {
fn new(value: String) -> Self {
Self {
value,
inserted_at: Instant::now(),
}
}
fn is_expired(&self, ttl: Duration) -> bool {
self.inserted_at.elapsed() > ttl
}
}
/// Cache for command results.
pub struct Cache {
/// Cache storage
entries: Arc<RwLock<HashMap<String, CacheEntry>>>,
/// Time-to-live for cache entries
ttl: Duration,
}
impl Cache {
/// Create a new cache with default TTL (5 minutes).
pub fn new() -> Self {
Self::with_ttl(Duration::from_secs(300))
}
/// Create a new cache with custom TTL.
pub fn with_ttl(ttl: Duration) -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
/// Get a cached value if it exists and hasn't expired.
pub async fn get(&self, command: &Commands) -> Option<String> {
let key = self.cache_key(command)?;
let entries = self.entries.read().await;
if let Some(entry) = entries.get(&key) {
if !entry.is_expired(self.ttl) {
debug!("Cache hit for key: {}", key);
return Some(entry.value.clone());
} else {
debug!("Cache entry expired for key: {}", key);
}
}
None
}
/// Set a cached value.
pub async fn set(&self, command: &Commands, value: String) {
if let Some(key) = self.cache_key(command) {
let mut entries = self.entries.write().await;
entries.insert(key.clone(), CacheEntry::new(value));
debug!("Cached result for key: {}", key);
}
}
/// Clear all cached entries.
pub async fn clear(&self) {
let mut entries = self.entries.write().await;
entries.clear();
debug!("Cache cleared");
}
/// Remove expired entries.
pub async fn cleanup(&self) {
let mut entries = self.entries.write().await;
let ttl = self.ttl;
entries.retain(|_, entry| !entry.is_expired(ttl));
debug!("Cache cleanup completed");
}
/// Generate a cache key for a command.
/// Only cacheable commands return Some.
fn cache_key(&self, command: &Commands) -> Option<String> {
// For now, generate a simple cache key based on debug representation
// TODO: Implement proper cache key generation for specific command types
Some(format!("{:?}", command))
}
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cache_operations() {
let cache = Cache::new();
// Create a test command (using Version since it's simple)
let command = Commands::Version;
// Should be empty initially
assert!(cache.get(&command).await.is_none());
// Set a value
cache.set(&command, "test result".to_string()).await;
// Should return the value
assert_eq!(cache.get(&command).await, Some("test result".to_string()));
// Clear cache
cache.clear().await;
// Should be empty again
assert!(cache.get(&command).await.is_none());
}
#[tokio::test]
async fn test_cache_expiration() {
let cache = Cache::with_ttl(Duration::from_millis(100));
let command = Commands::Version;
cache.set(&command, "test".to_string()).await;
assert!(cache.get(&command).await.is_some());
// Wait for expiration
tokio::time::sleep(Duration::from_millis(150)).await;
// Should be expired
assert!(cache.get(&command).await.is_none());
}
}
-427
View File
@@ -1,427 +0,0 @@
//! Command handler for processing IPC requests.
//!
//! Translates IPC commands into Ghidra bridge operations.
//! Handles lazy bridge startup on Import/Analyze commands.
use std::sync::Arc;
use serde_json::json;
use tokio::sync::Mutex;
use tracing::{debug, info};
use crate::ghidra::bridge::{BridgeStartMode, GhidraBridge};
use crate::ipc::protocol::{Command, Response};
use super::DaemonState;
/// Handle an IPC command.
pub async fn handle_command(
state: &Arc<DaemonState>,
id: u64,
command: Command,
) -> Response {
match handle_command_inner(state, command).await {
Ok(result) => Response::success(id, result),
Err(e) => Response::error(id, e.to_string()),
}
}
/// Ensure the bridge is running, returning an error if not.
async fn require_bridge(
bridge: &Arc<Mutex<Option<GhidraBridge>>>,
) -> anyhow::Result<()> {
let bridge_guard = bridge.lock().await;
let b = bridge_guard
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No program loaded. Run 'ghidra import <binary>' first."))?;
if !b.is_running() {
anyhow::bail!("Bridge is not running. Try restarting the daemon.");
}
Ok(())
}
/// Start the bridge in import mode (lazy start).
async fn start_bridge_for_import(
state: &Arc<DaemonState>,
binary_path: &str,
project: &str,
) -> anyhow::Result<()> {
let ghidra_dir = state
.ghidra_install_dir
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Ghidra installation directory not configured"))?;
// Ensure project directory exists (Ghidra requires it)
if !state.project_path.exists() {
std::fs::create_dir_all(&state.project_path)
.map_err(|e| anyhow::anyhow!("Failed to create project directory: {}", e))?;
}
// Stop existing bridge if any
{
let mut bridge_guard = state.bridge.lock().await;
if let Some(mut b) = bridge_guard.take() {
info!("Stopping existing bridge before import");
b.stop().ok();
}
}
let mut new_bridge = GhidraBridge::new(
ghidra_dir.clone(),
state.project_path.clone(),
project.to_string(),
);
info!("Starting bridge in import mode for: {}", binary_path);
new_bridge.start(BridgeStartMode::Import {
binary_path: binary_path.to_string(),
})?;
let mut bridge_guard = state.bridge.lock().await;
*bridge_guard = Some(new_bridge);
Ok(())
}
/// Start the bridge in process mode (for analyze/query after import).
async fn start_bridge_for_process(
state: &Arc<DaemonState>,
project: &str,
program: &str,
) -> anyhow::Result<()> {
let ghidra_dir = state
.ghidra_install_dir
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Ghidra installation directory not configured"))?;
// Ensure project directory exists
if !state.project_path.exists() {
std::fs::create_dir_all(&state.project_path)
.map_err(|e| anyhow::anyhow!("Failed to create project directory: {}", e))?;
}
// Stop existing bridge if any
{
let mut bridge_guard = state.bridge.lock().await;
if let Some(mut b) = bridge_guard.take() {
info!("Stopping existing bridge before starting process mode");
b.stop().ok();
}
}
let mut new_bridge = GhidraBridge::new(
ghidra_dir.clone(),
state.project_path.clone(),
project.to_string(),
);
info!("Starting bridge in process mode for: {}", program);
new_bridge.start(BridgeStartMode::Process {
program_name: program.to_string(),
})?;
let mut bridge_guard = state.bridge.lock().await;
*bridge_guard = Some(new_bridge);
Ok(())
}
async fn handle_command_inner(
state: &Arc<DaemonState>,
command: Command,
) -> anyhow::Result<serde_json::Value> {
match command {
Command::Ping => Ok(json!({"status": "ok"})),
Command::Status => {
let bridge_guard = state.bridge.lock().await;
let bridge_running = bridge_guard
.as_ref()
.map(|b| b.is_running())
.unwrap_or(false);
Ok(json!({
"bridge_running": bridge_running,
}))
}
Command::ClearCache => {
// TODO: Implement cache clearing
Ok(json!({"cleared": true}))
}
Command::Shutdown => {
// Shutdown is handled at a higher level
Ok(json!({"status": "shutting_down"}))
}
// === Import: reuses running bridge if available, otherwise lazy-starts ===
Command::Import {
binary_path,
project,
program,
} => {
// Check if bridge is already running
let bridge_running = {
let bridge_guard = state.bridge.lock().await;
bridge_guard.as_ref().map(|b| b.is_running()).unwrap_or(false)
};
if bridge_running {
// Bridge already running — import via bridge command, no JVM restart
let mut bridge_guard = state.bridge.lock().await;
let bridge = bridge_guard.as_mut().unwrap();
let import_response = bridge.send_command::<serde_json::Value>(
"import",
Some(json!({
"binary_path": binary_path,
"program": program,
})),
)?;
if import_response.status != "success" {
let msg = import_response.message.unwrap_or_else(|| "Import failed".to_string());
anyhow::bail!("{}", msg);
}
let program_name = program.unwrap_or_else(|| {
import_response
.data
.as_ref()
.and_then(|d| d.get("program"))
.and_then(|n| n.as_str())
.unwrap_or("unknown")
.to_string()
});
// Switch to the newly imported program
let open_response = bridge.send_command::<serde_json::Value>(
"open_program",
Some(json!({"program": program_name})),
)?;
if open_response.status != "success" {
let msg = open_response.message.unwrap_or_else(|| "Failed to switch program".to_string());
anyhow::bail!("{}", msg);
}
Ok(json!({"program": program_name}))
} else {
// No bridge running — start one in import mode
start_bridge_for_import(state, &binary_path, &project).await?;
let mut bridge_guard = state.bridge.lock().await;
let bridge = bridge_guard
.as_mut()
.ok_or_else(|| anyhow::anyhow!("Bridge failed to start"))?;
let response = bridge.send_command::<serde_json::Value>(
"program_info",
None,
)?;
if response.status == "success" {
let program_name = program.unwrap_or_else(|| {
response
.data
.as_ref()
.and_then(|d| d.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("unknown")
.to_string()
});
Ok(json!({"program": program_name}))
} else {
let msg = response.message.unwrap_or_else(|| "Import failed".to_string());
anyhow::bail!("{}", msg)
}
}
}
// === Analyze: reuses running bridge, Python side handles program switching ===
Command::Analyze { project, program } => {
// Check if bridge is already running
let bridge_running = {
let bridge_guard = state.bridge.lock().await;
bridge_guard.as_ref().map(|b| b.is_running()).unwrap_or(false)
};
if bridge_running {
// Bridge already running — analyze command handles open_program internally
let mut bridge_guard = state.bridge.lock().await;
let bridge = bridge_guard.as_mut().unwrap();
let response = bridge.send_command::<serde_json::Value>(
"analyze",
Some(json!({"project": project, "program": program})),
)?;
if response.status == "success" {
Ok(response.data.unwrap_or(json!({"status": "analysis_complete"})))
} else {
let msg = response.message.unwrap_or_else(|| "Analysis failed".to_string());
anyhow::bail!("{}", msg)
}
} else {
// Bridge not running, start it in process mode
start_bridge_for_process(state, &project, &program).await?;
Ok(json!({"status": "bridge_started", "program": program}))
}
}
// === Program management commands ===
Command::ListPrograms => {
require_bridge(&state.bridge).await?;
execute_bridge_command(state, "list_programs", None).await
}
Command::OpenProgram { program } => {
require_bridge(&state.bridge).await?;
execute_bridge_command(
state,
"open_program",
Some(json!({"program": program})),
)
.await
}
// === All other commands require bridge to be running ===
Command::ListFunctions { limit, filter } => {
require_bridge(&state.bridge).await?;
execute_bridge_command(
state,
"list_functions",
Some(json!({
"limit": limit,
"filter": filter,
})),
)
.await
}
Command::Decompile { address } => {
require_bridge(&state.bridge).await?;
execute_bridge_command(
state,
"decompile",
Some(json!({
"address": address,
})),
)
.await
}
Command::ListStrings { limit } => {
require_bridge(&state.bridge).await?;
execute_bridge_command(
state,
"list_strings",
Some(json!({
"limit": limit,
})),
)
.await
}
Command::ListImports => {
require_bridge(&state.bridge).await?;
execute_bridge_command(state, "list_imports", None).await
}
Command::ListExports => {
require_bridge(&state.bridge).await?;
execute_bridge_command(state, "list_exports", None).await
}
Command::MemoryMap => {
require_bridge(&state.bridge).await?;
execute_bridge_command(state, "memory_map", None).await
}
Command::ProgramInfo => {
require_bridge(&state.bridge).await?;
execute_bridge_command(state, "program_info", None).await
}
Command::XRefsTo { address } => {
require_bridge(&state.bridge).await?;
execute_bridge_command(
state,
"xrefs_to",
Some(json!({
"address": address,
})),
)
.await
}
Command::XRefsFrom { address } => {
require_bridge(&state.bridge).await?;
execute_bridge_command(
state,
"xrefs_from",
Some(json!({
"address": address,
})),
)
.await
}
Command::ExecuteCli { command_json } => {
// Deserialize and execute CLI command through the queue handlers
let cli_command: crate::cli::Commands = serde_json::from_str(&command_json)
.map_err(|e| anyhow::anyhow!("Failed to deserialize CLI command: {}", e))?;
// Ensure bridge is running for ExecuteCli commands
require_bridge(&state.bridge).await?;
// Execute using the queue's command execution logic
let result = crate::daemon::queue::execute_command_direct(&state.bridge, &cli_command).await?;
// Parse the result as JSON (handlers return JSON strings)
Ok(serde_json::from_str(&result).unwrap_or_else(|_| json!({"output": result})))
}
}
}
/// Execute a command on the Ghidra bridge.
///
/// If the bridge process dies during command execution, triggers daemon shutdown.
async fn execute_bridge_command(
state: &Arc<DaemonState>,
command: &str,
args: Option<serde_json::Value>,
) -> anyhow::Result<serde_json::Value> {
let mut bridge_guard = state.bridge.lock().await;
let bridge = bridge_guard
.as_mut()
.ok_or_else(|| anyhow::anyhow!("No program loaded. Run 'ghidra import <binary>' first."))?;
if !bridge.is_running() {
anyhow::bail!("Bridge is not running");
}
debug!("Executing bridge command: {}", command);
let response = match bridge.send_command::<serde_json::Value>(command, args) {
Ok(resp) => resp,
Err(e) => {
// Check if bridge process died - trigger daemon shutdown
let err_msg = e.to_string();
if err_msg.contains("process died") || !bridge.is_running() {
info!("Bridge process died, triggering daemon shutdown");
let _ = state.shutdown_tx.send(());
}
return Err(e);
}
};
if response.status == "success" {
Ok(response.data.unwrap_or(json!({})))
} else {
let message = response
.message
.unwrap_or_else(|| "Unknown error".to_string());
anyhow::bail!("{}", message)
}
}
-54
View File
@@ -1,54 +0,0 @@
//! Batch operation handler.
use anyhow::{Context, Result};
use serde_json::json;
use std::fs;
use std::path::Path;
pub async fn handle_batch(file_path: &str) -> Result<String> {
let path = Path::new(file_path);
if !path.exists() {
anyhow::bail!("Batch file not found: {}", file_path);
}
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read batch file: {}", file_path))?;
let mut results = Vec::new();
let mut line_number = 0;
for line in content.lines() {
line_number += 1;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
results.push(json!({
"line": line_number,
"command": trimmed,
"status": "not_implemented",
"message": "Batch command execution not yet implemented"
}));
}
let response = json!({
"file": file_path,
"commands_parsed": results.len(),
"results": results
});
serde_json::to_string(&response).context("Failed to serialize batch results")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_batch_placeholder() {
assert!(true);
}
}
-86
View File
@@ -1,86 +0,0 @@
//! Comment operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_comment_list(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("comment_list", None)
.context("Failed to list comments")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to list comments".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_comment_get(bridge: &mut GhidraBridge, address: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("comment_get", Some(json!({"address": address})))
.context("Failed to get comment")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get comment".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_comment_set(
bridge: &mut GhidraBridge,
address: &str,
text: &str,
comment_type: Option<&str>,
) -> Result<String> {
let mut args = json!({
"address": address,
"text": text
});
if let Some(ctype) = comment_type {
args["comment_type"] = json!(ctype);
}
let response = bridge
.send_command::<serde_json::Value>("comment_set", Some(args))
.context("Failed to set comment")?;
if response.status == "success" {
Ok(json!({"status": "set", "address": address}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to set comment".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_comment_delete(bridge: &mut GhidraBridge, address: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("comment_delete", Some(json!({"address": address})))
.context("Failed to delete comment")?;
if response.status == "success" {
Ok(json!({"status": "deleted", "address": address}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to delete comment".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
}
-61
View File
@@ -1,61 +0,0 @@
//! Diff operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_diff_programs(
bridge: &mut GhidraBridge,
prog1: &str,
prog2: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"diff_programs",
Some(json!({"prog1": prog1, "prog2": prog2})),
)
.context("Failed to diff programs")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to diff programs".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_diff_functions(
bridge: &mut GhidraBridge,
func1: &str,
func2: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"diff_functions",
Some(json!({"func1": func1, "func2": func2})),
)
.context("Failed to diff functions")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to diff functions".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diff_handlers_exist() {
assert!(true);
}
}
-41
View File
@@ -1,41 +0,0 @@
//! Disassembly operation handler.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_disasm(
bridge: &mut GhidraBridge,
address: &str,
count: Option<usize>,
) -> Result<String> {
let mut args = json!({"address": address});
if let Some(num) = count {
args["count"] = json!(num);
}
let response = bridge
.send_command::<serde_json::Value>("disasm", Some(args))
.context("Failed to disassemble")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to disassemble".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_disasm_placeholder() {
assert!(true);
}
}
-111
View File
@@ -1,111 +0,0 @@
//! Find/search operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_find_string(bridge: &mut GhidraBridge, pattern: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("find_string", Some(json!({"pattern": pattern})))
.context("Failed to find string")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to find string".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_find_bytes(bridge: &mut GhidraBridge, hex: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("find_bytes", Some(json!({"hex": hex})))
.context("Failed to find bytes")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to find bytes".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_find_function(bridge: &mut GhidraBridge, pattern: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("find_function", Some(json!({"pattern": pattern})))
.context("Failed to find function")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to find function".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_find_calls(bridge: &mut GhidraBridge, function: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("find_calls", Some(json!({"function": function})))
.context("Failed to find calls")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to find calls".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_find_crypto(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("find_crypto", None)
.context("Failed to find crypto constants")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to find crypto constants".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_find_interesting(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("find_interesting", None)
.context("Failed to find interesting functions")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to find interesting functions".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_placeholder() {
assert!(true);
}
}
-94
View File
@@ -1,94 +0,0 @@
//! Graph operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_graph_calls(bridge: &mut GhidraBridge, limit: Option<usize>) -> Result<String> {
let args = limit.map(|lim| json!({"limit": lim}));
let response = bridge
.send_command::<serde_json::Value>("graph_calls", args)
.context("Failed to get call graph")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get call graph".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_graph_callers(
bridge: &mut GhidraBridge,
function: &str,
depth: Option<usize>,
) -> Result<String> {
let mut args = json!({"function": function});
if let Some(d) = depth {
args["depth"] = json!(d);
}
let response = bridge
.send_command::<serde_json::Value>("graph_callers", Some(args))
.context("Failed to get callers")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get callers".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_graph_callees(
bridge: &mut GhidraBridge,
function: &str,
depth: Option<usize>,
) -> Result<String> {
let mut args = json!({"function": function});
if let Some(d) = depth {
args["depth"] = json!(d);
}
let response = bridge
.send_command::<serde_json::Value>("graph_callees", Some(args))
.context("Failed to get callees")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get callees".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_graph_export(bridge: &mut GhidraBridge, format: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("graph_export", Some(json!({"format": format})))
.context("Failed to export graph")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to export graph".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
}
-14
View File
@@ -1,14 +0,0 @@
//! Handler modules for daemon commands grouped by category.
pub mod batch;
pub mod comments;
pub mod diff;
pub mod disasm;
pub mod find;
pub mod graph;
pub mod patch;
pub mod program;
pub mod script;
pub mod stats;
pub mod symbols;
pub mod types;
-73
View File
@@ -1,73 +0,0 @@
//! Patch operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_patch_bytes(
bridge: &mut GhidraBridge,
address: &str,
hex: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"patch_bytes",
Some(json!({
"address": address,
"hex": hex
})),
)
.context("Failed to patch bytes")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to patch bytes".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_patch_nop(bridge: &mut GhidraBridge, address: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("patch_nop", Some(json!({"address": address})))
.context("Failed to patch NOP")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to patch NOP".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_patch_export(bridge: &mut GhidraBridge, output: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("patch_export", Some(json!({"output": output})))
.context("Failed to export patched binary")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to export patched binary".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_placeholder() {
assert!(true);
}
}
-132
View File
@@ -1,132 +0,0 @@
//! Program operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_program_list(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("list_programs", None)
.context("Failed to list programs")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to list programs".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_program_open(
bridge: &mut GhidraBridge,
program_name: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"open_program",
Some(json!({
"program": program_name
})),
)
.context("Failed to open program")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to open program".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_program_close(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("program_close", None)
.context("Failed to close program")?;
if response.status == "success" {
Ok(json!({"status": "closed"}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to close program".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_program_delete(
bridge: &mut GhidraBridge,
program_name: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"program_delete",
Some(json!({
"program": program_name
})),
)
.context("Failed to delete program")?;
if response.status == "success" {
Ok(json!({"status": "deleted", "program": program_name}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to delete program".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_program_info(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("program_info", None)
.context("Failed to get program info")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get program info".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_program_export(
bridge: &mut GhidraBridge,
format: &str,
output: Option<&str>,
) -> Result<String> {
let mut args = json!({
"format": format
});
if let Some(output_path) = output {
args["output"] = json!(output_path);
}
let response = bridge
.send_command::<serde_json::Value>("program_export", Some(args))
.context("Failed to export program")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to export program".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
}
-83
View File
@@ -1,83 +0,0 @@
//! Script execution handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_script_run(
bridge: &mut GhidraBridge,
path: &str,
args: &[String],
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("script_run", Some(json!({"path": path, "args": args})))
.context("Failed to run script")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to run script".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_script_python(bridge: &mut GhidraBridge, code: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("script_python", Some(json!({"code": code})))
.context("Failed to execute Python code")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to execute Python code".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_script_java(bridge: &mut GhidraBridge, code: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("script_java", Some(json!({"code": code})))
.context("Failed to execute Java code")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to execute Java code".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_script_list(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("script_list", None)
.context("Failed to list scripts")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to list scripts".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_script_placeholder() {
assert!(true);
}
}
-30
View File
@@ -1,30 +0,0 @@
//! Program statistics handler.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
pub async fn handle_stats(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("stats", None)
.context("Failed to get program statistics")?;
if response.status == "success" {
let data = response.data.unwrap_or(serde_json::json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get program statistics".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats_placeholder() {
assert!(true);
}
}
-127
View File
@@ -1,127 +0,0 @@
//! Symbol operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_symbol_list(bridge: &mut GhidraBridge, filter: Option<&str>) -> Result<String> {
let args = filter.map(|f| json!({"filter": f}));
let response = bridge
.send_command::<serde_json::Value>("symbol_list", args)
.context("Failed to list symbols")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to list symbols".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_symbol_get(bridge: &mut GhidraBridge, name: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("symbol_get", Some(json!({"name": name})))
.context("Failed to get symbol")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get symbol".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_symbol_create(
bridge: &mut GhidraBridge,
address: &str,
name: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"symbol_create",
Some(json!({
"address": address,
"name": name
})),
)
.context("Failed to create symbol")?;
if response.status == "success" {
Ok(json!({"status": "created", "address": address, "name": name}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to create symbol".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_symbol_delete(bridge: &mut GhidraBridge, name: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("symbol_delete", Some(json!({"name": name})))
.context("Failed to delete symbol")?;
if response.status == "success" {
Ok(json!({"status": "deleted", "name": name}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to delete symbol".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_symbol_rename(
bridge: &mut GhidraBridge,
old_name: &str,
new_name: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"symbol_rename",
Some(json!({
"old_name": old_name,
"new_name": new_name
})),
)
.context("Failed to rename symbol")?;
if response.status == "success" {
Ok(json!({"status": "renamed", "old_name": old_name, "new_name": new_name}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to rename symbol".to_string());
anyhow::bail!("{}", message)
}
}
/// Resolve address input - handles both hex addresses and symbol names.
/// The actual resolution is done on the Python side.
#[allow(dead_code)]
fn resolve_address(input: &str) -> Result<String> {
// Pass-through to Python layer which handles both hex addresses and symbol name lookups
Ok(input.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolve_address_hex() {
assert_eq!(resolve_address("0x1000").unwrap(), "0x1000");
}
#[test]
fn test_resolve_address_name() {
assert_eq!(resolve_address("main").unwrap(), "main");
}
}
-82
View File
@@ -1,82 +0,0 @@
//! Type operation handlers.
use crate::ghidra::bridge::GhidraBridge;
use anyhow::{Context, Result};
use serde_json::json;
pub async fn handle_type_list(bridge: &mut GhidraBridge) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("type_list", None)
.context("Failed to list types")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to list types".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_type_get(bridge: &mut GhidraBridge, name: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("type_get", Some(json!({"name": name})))
.context("Failed to get type")?;
if response.status == "success" {
let data = response.data.unwrap_or(json!({}));
serde_json::to_string(&data).context("Failed to serialize response")
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to get type".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_type_create(bridge: &mut GhidraBridge, name: &str) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>("type_create", Some(json!({"name": name})))
.context("Failed to create type")?;
if response.status == "success" {
Ok(json!({"status": "created", "name": name}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to create type".to_string());
anyhow::bail!("{}", message)
}
}
pub async fn handle_type_apply(
bridge: &mut GhidraBridge,
address: &str,
type_name: &str,
) -> Result<String> {
let response = bridge
.send_command::<serde_json::Value>(
"type_apply",
Some(json!({
"address": address,
"type_name": type_name
})),
)
.context("Failed to apply type")?;
if response.status == "success" {
Ok(json!({"status": "applied", "address": address, "type": type_name}).to_string())
} else {
let message = response
.message
.unwrap_or_else(|| "Failed to apply type".to_string());
anyhow::bail!("{}", message)
}
}
#[cfg(test)]
mod tests {
use super::*;
}
-163
View File
@@ -1,163 +0,0 @@
//! IPC server for daemon communication.
//!
//! Uses local sockets (Unix domain sockets / Windows named pipes) with
//! the new IPC layer instead of TCP.
//!
//! Each project gets its own socket for concurrent daemon operation.
#![allow(dead_code)]
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use interprocess::local_socket::traits::tokio::Listener as ListenerTrait;
use tokio::io::BufReader;
use tokio::sync::broadcast;
use tracing::{debug, error, info};
use crate::ipc::protocol::{Command, Request, Response};
use crate::ipc::transport;
use super::handler;
use super::DaemonState;
/// IPC server state
pub struct IpcServer {
/// Shared daemon state (bridge + config)
daemon_state: Arc<DaemonState>,
/// Shutdown signal sender
shutdown_tx: broadcast::Sender<()>,
/// Server start time
started_at: Instant,
}
impl IpcServer {
/// Create a new IPC server.
pub fn new(
daemon_state: Arc<DaemonState>,
shutdown_tx: broadcast::Sender<()>,
) -> Self {
Self {
daemon_state,
shutdown_tx,
started_at: Instant::now(),
}
}
/// Handle a single client connection.
async fn handle_client(&self, stream: transport::platform::Stream) -> anyhow::Result<bool> {
let (reader, mut writer) = tokio::io::split(stream);
let mut reader = BufReader::new(reader);
loop {
// Read request with timeout
let request_data = tokio::select! {
result = transport::recv_message(&mut reader) => {
match result {
Ok(data) => data,
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
debug!("Client disconnected");
return Ok(false);
}
Err(e) => {
error!("Error reading request: {}", e);
return Ok(false);
}
}
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => {
debug!("Client timeout");
return Ok(false);
}
};
// Parse request
let request: Request = match serde_json::from_slice(&request_data) {
Ok(req) => req,
Err(e) => {
error!("Invalid request: {}", e);
let response = Response::error(0, format!("Invalid request: {}", e));
let json = serde_json::to_vec(&response)?;
transport::send_message(&mut writer, &json).await?;
continue;
}
};
debug!("Received command: {:?}", request.command);
// Check for shutdown command
if matches!(request.command, Command::Shutdown) {
let response = Response::ok(request.id);
let json = serde_json::to_vec(&response)?;
transport::send_message(&mut writer, &json).await?;
return Ok(true); // Signal shutdown
}
// Handle command
let response = handler::handle_command(&self.daemon_state, request.id, request.command).await;
// Send response
let json = serde_json::to_vec(&response)?;
transport::send_message(&mut writer, &json).await?;
}
}
}
/// Run the IPC server for a specific project.
pub async fn run_ipc_server(
daemon_state: Arc<DaemonState>,
shutdown_tx: broadcast::Sender<()>,
project_path: &Path,
) -> anyhow::Result<()> {
// Create the IPC listener for this project
let listener = transport::create_listener_for_project(project_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to create IPC listener: {}", e))?;
info!(
"IPC server listening on {}",
transport::socket_name_for_project(project_path)
);
let server = Arc::new(IpcServer::new(daemon_state, shutdown_tx.clone()));
let mut shutdown_rx = shutdown_tx.subscribe();
loop {
tokio::select! {
accept_result = listener.accept() => {
match accept_result {
Ok(stream) => {
info!("Accepted IPC connection");
let server = server.clone();
let shutdown_tx = shutdown_tx.clone();
tokio::spawn(async move {
match server.handle_client(stream).await {
Ok(should_shutdown) if should_shutdown => {
info!("Shutdown requested via IPC");
let _ = shutdown_tx.send(());
}
Ok(_) => {}
Err(e) => {
error!("Connection error: {}", e);
}
}
});
}
Err(e) => {
error!("Accept error: {}", e);
}
}
}
_ = shutdown_rx.recv() => {
info!("IPC server shutting down");
break;
}
}
}
// Clean up socket for this project
transport::remove_socket_for_project(project_path).ok();
Ok(())
}
+49 -168
View File
@@ -1,183 +1,64 @@
//! Daemon core logic.
//! Bridge management module.
//!
//! The daemon is the main runtime that:
//! - Manages a persistent Ghidra bridge process
//! - Serves commands via local socket IPC
//! - Handles graceful shutdown
//! Manages the lifecycle of the Java GhidraCliBridge process.
//! The "daemon" is just the long-running Ghidra/Java bridge process -
//! there is no separate Rust daemon. The CLI connects directly to
//! the bridge via TCP.
use std::path::PathBuf;
use std::sync::Arc;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use tokio::sync::{broadcast, Mutex};
use tracing::{error, info, warn};
use anyhow::Result;
use crate::daemon::process::{acquire_daemon_lock, get_data_dir, remove_info_file, write_daemon_info, DaemonInfo};
use crate::ghidra::bridge::GhidraBridge;
use crate::ghidra::bridge::{self, BridgeStartMode, BridgeStatus};
pub mod cache;
pub mod handler;
pub mod handlers;
pub mod ipc_server;
pub mod process;
pub mod queue;
pub mod state;
/// Daemon configuration.
pub struct DaemonConfig {
/// Path to the project directory
/// Bridge configuration (replaces old DaemonConfig).
pub struct BridgeConfig {
/// Path to the Ghidra project directory
pub project_path: PathBuf,
/// Ghidra installation directory
pub ghidra_install_dir: Option<PathBuf>,
/// Log file path
pub log_file: PathBuf,
pub ghidra_install_dir: PathBuf,
}
/// Shared daemon state accessible by handlers.
pub struct DaemonState {
/// The Ghidra bridge instance (None until first import/analyze)
pub bridge: Arc<Mutex<Option<GhidraBridge>>>,
/// Ghidra installation directory
pub ghidra_install_dir: Option<PathBuf>,
/// Project path on disk
pub project_path: PathBuf,
/// Shutdown signal sender - handlers can trigger daemon shutdown on bridge death
pub shutdown_tx: broadcast::Sender<()>,
/// Ensure a bridge is running for the given project.
/// If import mode, starts with the binary. If process mode, opens existing program.
/// Returns the port number for connecting.
pub fn ensure_bridge(
config: &BridgeConfig,
mode: BridgeStartMode,
) -> Result<u16> {
bridge::ensure_bridge_running(
&config.project_path,
&config.ghidra_install_dir,
mode,
)
}
/// 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 shared daemon state - bridge starts as None, lazy-started on first command
let daemon_state = Arc::new(DaemonState {
bridge: Arc::new(Mutex::new(None)),
ghidra_install_dir: config.ghidra_install_dir.clone(),
project_path: config.project_path.clone(),
shutdown_tx: shutdown_tx.clone(),
});
info!("Bridge will be started on first import/analyze command");
// Acquire OS-level lock (atomic liveness check)
let _lock = acquire_daemon_lock(&data_dir, &config.project_path)
.context("Failed to acquire daemon lock")?;
// Write daemon info to separate file
let daemon_info = DaemonInfo::new(&config.project_path, &config.log_file);
write_daemon_info(&data_dir, &config.project_path, &daemon_info)
.context("Failed to write daemon info")?;
// Start IPC server task
let ipc_state = daemon_state.clone();
let ipc_shutdown_tx = shutdown_tx.clone();
let ipc_project_path = config.project_path.clone();
let ipc_handle = tokio::spawn(async move {
if let Err(e) =
ipc_server::run_ipc_server(ipc_state, ipc_shutdown_tx, &ipc_project_path).await
{
error!("IPC server error: {}", e);
}
});
// 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 if it was started
{
let mut bridge_guard = daemon_state.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 info file; lock file is released when _lock drops at end of scope
remove_info_file(&data_dir, &config.project_path).ok();
info!("Daemon stopped");
Ok(())
/// Start a new bridge for the given project.
/// Returns the port number for connecting.
pub fn start_bridge(
config: &BridgeConfig,
mode: BridgeStartMode,
) -> Result<u16> {
bridge::start_bridge(
&config.project_path,
&config.ghidra_install_dir,
mode,
)
}
/// The reason for shutdown.
#[derive(Debug, Clone)]
pub enum ShutdownReason {
/// SIGINT (Ctrl+C)
Interrupt,
/// SIGTERM
Terminate,
/// RPC shutdown request
RpcRequest,
/// Stop the bridge for a project.
pub fn stop_bridge(project_path: &Path) -> Result<()> {
bridge::stop_bridge(project_path)
}
/// Wait for a shutdown signal.
async fn wait_for_shutdown(shutdown_tx: broadcast::Sender<()>) -> ShutdownReason {
let mut shutdown_rx = shutdown_tx.subscribe();
/// Get bridge status for a project.
pub fn get_bridge_status(project_path: &Path) -> Result<BridgeStatus> {
bridge::bridge_status(project_path)
}
#[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
}
}
}
/// Check if a bridge is running for a project.
pub fn is_bridge_running(project_path: &Path) -> bool {
bridge::is_bridge_running(project_path)
}
#[cfg(test)]
@@ -185,13 +66,13 @@ mod tests {
use super::*;
#[test]
fn test_daemon_config() {
let config = DaemonConfig {
fn test_bridge_config() {
let config = BridgeConfig {
project_path: PathBuf::from("/test/project"),
ghidra_install_dir: None,
log_file: PathBuf::from("/test/logs/daemon.log"),
ghidra_install_dir: PathBuf::from("/opt/ghidra"),
};
assert_eq!(config.project_path, PathBuf::from("/test/project"));
assert_eq!(config.ghidra_install_dir, PathBuf::from("/opt/ghidra"));
}
}
-246
View File
@@ -1,246 +0,0 @@
//! Process management for the daemon.
//!
//! Handles lock files and daemon process information using OS-level file locking
//! via `fslock` for atomic daemon liveness detection.
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use chrono::{DateTime, Utc};
use fslock::LockFile;
use serde::{Deserialize, Serialize};
/// Daemon information stored in the info file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonInfo {
/// Process ID of the daemon
pub pid: u32,
/// Project path being managed
pub project_path: PathBuf,
/// Log file path
pub log_file: PathBuf,
/// When the daemon was started
pub started_at: DateTime<Utc>,
}
impl DaemonInfo {
/// Create new daemon info.
pub fn new(project_path: &Path, log_file: &Path) -> Self {
Self {
pid: std::process::id(),
project_path: project_path.to_path_buf(),
log_file: log_file.to_path_buf(),
started_at: Utc::now(),
}
}
}
/// Get the data directory for daemon files.
///
/// Checks GHIDRA_CLI_DATA_DIR env var first (used for testing), then falls back to default.
pub fn get_data_dir() -> Result<PathBuf> {
let data_dir = if let Ok(path) = std::env::var("GHIDRA_CLI_DATA_DIR") {
PathBuf::from(path)
} else {
dirs::data_local_dir()
.context("Failed to get local data directory")?
.join("ghidra-cli")
};
fs::create_dir_all(&data_dir).context("Failed to create data directory")?;
Ok(data_dir)
}
/// Get the lock file path for a project (used for OS-level locking only).
fn get_lock_file_path(data_dir: &Path, project_path: &Path) -> PathBuf {
let project_hash = format!(
"{:x}",
md5::compute(project_path.to_string_lossy().as_bytes())
);
data_dir.join(format!("daemon-{}.lock", project_hash))
}
/// Get the info file path for a project (stores DaemonInfo JSON).
fn get_info_file_path(data_dir: &Path, project_path: &Path) -> PathBuf {
let project_hash = format!(
"{:x}",
md5::compute(project_path.to_string_lossy().as_bytes())
);
data_dir.join(format!("daemon-{}.info", project_hash))
}
/// Acquire an exclusive OS-level lock for the daemon.
///
/// Returns the held `LockFile` — the caller must keep it alive for the daemon's
/// entire lifetime. The lock is automatically released when the `LockFile` is dropped
/// (including on crash).
pub fn acquire_daemon_lock(
data_dir: &Path,
project_path: &Path,
) -> Result<LockFile> {
let lock_path = get_lock_file_path(data_dir, project_path);
let mut lock = LockFile::open(&lock_path)
.context("Failed to open lock file")?;
if !lock.try_lock_with_pid()
.context("Failed to acquire lock")? {
bail!("Daemon is already running for this project");
}
Ok(lock)
}
/// Write daemon info to the info file (separate from the lock file).
pub fn write_daemon_info(data_dir: &Path, project_path: &Path, info: &DaemonInfo) -> Result<()> {
let info_path = get_info_file_path(data_dir, project_path);
let json = serde_json::to_string_pretty(info).context("Failed to serialize daemon info")?;
let mut file = fs::File::create(&info_path).context("Failed to create info file")?;
file.write_all(json.as_bytes())
.context("Failed to write info file")?;
Ok(())
}
/// Remove the info file for a project.
///
/// The `.lock` file is released automatically when the daemon's `LockFile` handle drops.
/// Stale `.lock` files are harmless (empty, unlocked) and cleaned up by `get_running_daemon_info()`.
pub fn remove_info_file(data_dir: &Path, project_path: &Path) -> Result<()> {
let info_path = get_info_file_path(data_dir, project_path);
if info_path.exists() {
fs::remove_file(&info_path).context("Failed to remove info file")?;
}
Ok(())
}
/// Get daemon info if running, or clean up stale files.
///
/// Uses OS-level locking for atomic liveness detection:
/// - If we can acquire the lock, no daemon holds it — clean up stale files.
/// - If we cannot acquire the lock, a daemon is alive — read the info file.
pub fn get_running_daemon_info(data_dir: &Path, project_path: &Path) -> Result<Option<DaemonInfo>> {
let lock_path = get_lock_file_path(data_dir, project_path);
if !lock_path.exists() {
return Ok(None);
}
let mut lock = LockFile::open(&lock_path)
.context("Failed to open lock file for status check")?;
if lock.try_lock().context("Failed to check lock")? {
// We got the lock — no daemon is holding it. Clean up stale files.
lock.unlock().context("Failed to release lock")?;
fs::remove_file(&lock_path).ok();
let info_path = get_info_file_path(data_dir, project_path);
fs::remove_file(&info_path).ok();
// Also clean up stale socket file (daemon may have crashed without cleanup)
crate::ipc::transport::remove_socket_for_project(project_path).ok();
return Ok(None);
}
// Lock is held by another process — daemon is running. Read the info file.
let info_path = get_info_file_path(data_dir, project_path);
let contents = fs::read_to_string(&info_path)
.context("Lock is held but info file is missing")?;
let info: DaemonInfo = serde_json::from_str(&contents)
.context("Failed to parse daemon info file")?;
Ok(Some(info))
}
/// Ensure no daemon is currently running for this project.
pub fn ensure_not_running(data_dir: &Path, project_path: &Path) -> Result<()> {
if let Some(info) = get_running_daemon_info(data_dir, project_path)? {
bail!("Daemon is already running (PID: {})", info.pid);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_daemon_info_creation() {
let info = DaemonInfo::new(
Path::new("/test/project"),
Path::new("/test/logs/daemon.log"),
);
assert_eq!(info.project_path, PathBuf::from("/test/project"));
}
#[test]
fn test_lock_and_info_file_operations() -> Result<()> {
let temp_dir = tempdir()?;
let data_dir = temp_dir.path();
let project_path = PathBuf::from("/test/project");
// Acquire lock
let _lock = acquire_daemon_lock(data_dir, &project_path)?;
// Write info
let info = DaemonInfo::new(&project_path, Path::new("/test/logs/daemon.log"));
write_daemon_info(data_dir, &project_path, &info)?;
// Info file should exist
let info_path = get_info_file_path(data_dir, &project_path);
assert!(info_path.exists());
// Remove info file
remove_info_file(data_dir, &project_path)?;
assert!(!info_path.exists());
Ok(())
}
#[test]
fn test_cannot_acquire_lock_twice() -> Result<()> {
let temp_dir = tempdir()?;
let data_dir = temp_dir.path();
let project_path = PathBuf::from("/test/project");
// First lock succeeds
let _lock = acquire_daemon_lock(data_dir, &project_path)?;
// Second lock should fail
let result = acquire_daemon_lock(data_dir, &project_path);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already running"));
Ok(())
}
#[test]
fn test_stale_lock_cleaned_up() -> Result<()> {
let temp_dir = tempdir()?;
let data_dir = temp_dir.path();
let project_path = PathBuf::from("/test/project");
// Create a lock file but don't hold the lock (simulates crashed daemon)
let lock_path = get_lock_file_path(data_dir, &project_path);
fs::File::create(&lock_path)?;
// Also create a stale info file
let info_path = get_info_file_path(data_dir, &project_path);
let info = DaemonInfo::new(&project_path, Path::new("/test/logs/daemon.log"));
let json = serde_json::to_string_pretty(&info)?;
fs::write(&info_path, json)?;
// get_running_daemon_info should detect no lock holder and clean up
let result = get_running_daemon_info(data_dir, &project_path)?;
assert!(result.is_none());
// Stale files should be cleaned up
assert!(!lock_path.exists());
assert!(!info_path.exists());
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More