mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
v2 wip
This commit is contained in:
Generated
+1
-118
@@ -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
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
}
|
||||
@@ -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
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user