mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
refactoring to use python bridge and a persistent ghidra
This commit is contained in:
Generated
+34
@@ -511,6 +511,12 @@ 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"
|
||||
@@ -774,6 +780,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"indicatif",
|
||||
"interprocess",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"md5",
|
||||
@@ -1106,6 +1113,21 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -1697,6 +1719,12 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -2698,6 +2726,12 @@ 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"
|
||||
|
||||
@@ -40,6 +40,9 @@ tokio = { version = "1.35", features = ["full"] }
|
||||
# RPC
|
||||
remoc = { version = "0.16", features = ["full"] }
|
||||
|
||||
# IPC (local sockets)
|
||||
interprocess = { version = "2.2", features = ["tokio"] }
|
||||
|
||||
# Time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
|
||||
@@ -1 +1,101 @@
|
||||
# Refactoring Notes
|
||||
|
||||
## Architecture Reference: debugger-cli
|
||||
|
||||
The `debugger-cli` project at `~/git/debugger-cli` provides a good pattern for daemon-based CLIs:
|
||||
|
||||
### Key Patterns Used
|
||||
|
||||
1. **IPC via Local Sockets** (`src/ipc/`)
|
||||
- Uses `interprocess` crate for cross-platform Unix sockets / Windows named pipes
|
||||
- Length-prefixed JSON messages (4-byte little-endian length + payload)
|
||||
- Separate `protocol.rs`, `transport.rs`, and `client.rs` modules
|
||||
|
||||
2. **Daemon Architecture** (`src/daemon/`)
|
||||
- `server.rs` - Main event loop with IPC listener
|
||||
- `handler.rs` - Command routing and execution
|
||||
- `session.rs` - State management for debug sessions
|
||||
|
||||
3. **Clean Separation**
|
||||
- IPC protocol defines its own `Command` enum (not reusing CLI args)
|
||||
- Handler translates protocol commands to domain operations
|
||||
- Session holds the actual debug adapter connection
|
||||
|
||||
---
|
||||
|
||||
## Implementation Progress
|
||||
|
||||
### Phase 1: Bridge Script ✅
|
||||
- Created `src/ghidra/scripts/bridge.py` - persistent TCP server inside Ghidra
|
||||
- Implements handlers: `ping`, `program_info`, `list_functions`, `decompile`, `list_strings`, `list_imports`, `list_exports`, `memory_map`, `xrefs_to`, `xrefs_from`
|
||||
- Uses `---GHIDRA_CLI_START---` / `---GHIDRA_CLI_END---` markers for ready signal
|
||||
|
||||
### Phase 2: Output Markers ✅
|
||||
- Updated all 8 Python scripts in `scripts.rs` with delimiters
|
||||
- Updated `headless.rs` to use marker-based extraction instead of fragile brace-counting
|
||||
|
||||
### Phase 3: IPC Layer ✅
|
||||
- Added `interprocess` crate to `Cargo.toml`
|
||||
- Created `src/ipc/mod.rs` with:
|
||||
- `protocol.rs` - Typed `Command` enum, `Request`, `Response` structures
|
||||
- `transport.rs` - Cross-platform socket wrapper with length-prefixed framing
|
||||
- `client.rs` - `DaemonClient` for CLI-to-daemon communication
|
||||
|
||||
### Phase 4: Bridge Manager ✅
|
||||
- Created `src/ghidra/bridge.rs` with `GhidraBridge` struct
|
||||
- Manages Ghidra process lifecycle (spawn, monitor, shutdown)
|
||||
- TCP connection to Python bridge script
|
||||
- `BridgeResponse<T>` typed response handling
|
||||
- Embeds bridge.py via `include_str!` macro
|
||||
|
||||
### Phase 5: Daemon Update 🚧
|
||||
- Status: Not yet wired up
|
||||
- Remaining: Refactor daemon to use new IPC layer and bridge
|
||||
|
||||
### Phase 6: Typed Responses ⚙️
|
||||
- `BridgeResponse<T>` created in `bridge.rs`
|
||||
- Remaining: Update all response handling
|
||||
|
||||
### Phase 7: GUI Integration (Optional)
|
||||
- Status: Not started
|
||||
- Future work
|
||||
|
||||
---
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### New Files
|
||||
- `src/ghidra/scripts/bridge.py` - Persistent Python bridge server
|
||||
- `src/ghidra/bridge.rs` - Rust bridge manager
|
||||
- `src/ipc/mod.rs` - IPC module root
|
||||
- `src/ipc/protocol.rs` - Typed protocol definitions
|
||||
- `src/ipc/transport.rs` - Socket transport layer
|
||||
- `src/ipc/client.rs` - Daemon client
|
||||
|
||||
### Modified Files
|
||||
- `Cargo.toml` - Added `interprocess` crate
|
||||
- `src/main.rs` - Added `mod ipc`
|
||||
- `src/ghidra/mod.rs` - Added `mod bridge`, `#[derive(Debug)]` on `GhidraClient`
|
||||
- `src/ghidra/scripts.rs` - All scripts now have output markers
|
||||
- `src/ghidra/headless.rs` - Marker-based JSON extraction
|
||||
|
||||
---
|
||||
|
||||
## Remaining Work
|
||||
|
||||
To complete the refactoring:
|
||||
|
||||
1. **Wire up bridge to daemon** - Modify `daemon/mod.rs` to hold `GhidraBridge`
|
||||
2. **Route commands through bridge** - Update `daemon/queue.rs` to use bridge instead of spawning headless
|
||||
3. **Switch to IPC layer** - Replace TCP RPC with local socket IPC
|
||||
4. **Remove one-shot execution** - Clean up legacy headless spawning code
|
||||
5. **Manual testing** - Test with actual Ghidra installation
|
||||
|
||||
---
|
||||
|
||||
## Build Status
|
||||
|
||||
```
|
||||
✅ cargo check - PASSED
|
||||
✅ cargo test - 30 passed, 1 pre-existing failure (test_parse_hex)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//! Command handler for processing IPC requests.
|
||||
//!
|
||||
//! Translates IPC commands into Ghidra bridge operations.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::ghidra::bridge::GhidraBridge;
|
||||
use crate::ipc::protocol::{Command, Response};
|
||||
|
||||
/// Handle an IPC command.
|
||||
pub async fn handle_command(
|
||||
bridge: &Arc<Mutex<Option<GhidraBridge>>>,
|
||||
id: u64,
|
||||
command: Command,
|
||||
) -> Response {
|
||||
match handle_command_inner(bridge, command).await {
|
||||
Ok(result) => Response::success(id, result),
|
||||
Err(e) => Response::error(id, e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_command_inner(
|
||||
bridge: &Arc<Mutex<Option<GhidraBridge>>>,
|
||||
command: Command,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
match command {
|
||||
Command::Ping => {
|
||||
Ok(json!({"status": "ok"}))
|
||||
}
|
||||
|
||||
Command::Status => {
|
||||
let bridge_guard = 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"}))
|
||||
}
|
||||
|
||||
Command::ListFunctions { limit, filter } => {
|
||||
execute_bridge_command(bridge, "list_functions", Some(json!({
|
||||
"limit": limit,
|
||||
"filter": filter,
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::Decompile { address } => {
|
||||
execute_bridge_command(bridge, "decompile", Some(json!({
|
||||
"address": address,
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::ListStrings { limit } => {
|
||||
execute_bridge_command(bridge, "list_strings", Some(json!({
|
||||
"limit": limit,
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::ListImports => {
|
||||
execute_bridge_command(bridge, "list_imports", None).await
|
||||
}
|
||||
|
||||
Command::ListExports => {
|
||||
execute_bridge_command(bridge, "list_exports", None).await
|
||||
}
|
||||
|
||||
Command::MemoryMap => {
|
||||
execute_bridge_command(bridge, "memory_map", None).await
|
||||
}
|
||||
|
||||
Command::ProgramInfo => {
|
||||
execute_bridge_command(bridge, "program_info", None).await
|
||||
}
|
||||
|
||||
Command::XRefsTo { address } => {
|
||||
execute_bridge_command(bridge, "xrefs_to", Some(json!({
|
||||
"address": address,
|
||||
}))).await
|
||||
}
|
||||
|
||||
Command::XRefsFrom { address } => {
|
||||
execute_bridge_command(bridge, "xrefs_from", Some(json!({
|
||||
"address": address,
|
||||
}))).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command on the Ghidra bridge.
|
||||
async fn execute_bridge_command(
|
||||
bridge: &Arc<Mutex<Option<GhidraBridge>>>,
|
||||
command: &str,
|
||||
args: Option<serde_json::Value>,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let mut bridge_guard = bridge.lock().await;
|
||||
|
||||
let bridge = bridge_guard.as_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("Bridge not initialized"))?;
|
||||
|
||||
if !bridge.is_running() {
|
||||
anyhow::bail!("Bridge is not running");
|
||||
}
|
||||
|
||||
debug!("Executing bridge command: {}", command);
|
||||
|
||||
let response = bridge.send_command::<serde_json::Value>(command, args)?;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! IPC server for daemon communication.
|
||||
//!
|
||||
//! Uses local sockets (Unix domain sockets / Windows named pipes) with
|
||||
//! the new IPC layer instead of TCP.
|
||||
|
||||
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, Mutex};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use crate::ghidra::bridge::GhidraBridge;
|
||||
use crate::ipc::protocol::{Command, Request, Response};
|
||||
use crate::ipc::transport::{self, platform::Listener};
|
||||
|
||||
use super::handler;
|
||||
|
||||
/// IPC server state
|
||||
pub struct IpcServer {
|
||||
/// The Ghidra bridge instance
|
||||
bridge: Arc<Mutex<Option<GhidraBridge>>>,
|
||||
/// Shutdown signal sender
|
||||
shutdown_tx: broadcast::Sender<()>,
|
||||
/// Server start time
|
||||
started_at: Instant,
|
||||
}
|
||||
|
||||
impl IpcServer {
|
||||
/// Create a new IPC server.
|
||||
pub fn new(
|
||||
bridge: Arc<Mutex<Option<GhidraBridge>>>,
|
||||
shutdown_tx: broadcast::Sender<()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bridge,
|
||||
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.bridge,
|
||||
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.
|
||||
pub async fn run_ipc_server(
|
||||
bridge: Arc<Mutex<Option<GhidraBridge>>>,
|
||||
shutdown_tx: broadcast::Sender<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
// Create the IPC listener
|
||||
let listener = transport::create_listener().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create IPC listener: {}", e))?;
|
||||
|
||||
info!("IPC server listening on {}", transport::socket_name());
|
||||
|
||||
let server = Arc::new(IpcServer::new(bridge, 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
|
||||
transport::remove_socket().ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+72
-50
@@ -1,23 +1,23 @@
|
||||
//! Daemon core logic.
|
||||
//!
|
||||
//! The daemon is the main runtime that:
|
||||
//! - Loads and maintains project state in memory
|
||||
//! - Queues commands to prevent Ghidra conflicts
|
||||
//! - Serves RPC requests from clients
|
||||
//! - Manages a persistent Ghidra bridge process
|
||||
//! - Serves commands via local socket IPC
|
||||
//! - Handles graceful shutdown
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::daemon::process::{write_daemon_info, remove_lock_file, DaemonInfo, get_data_dir};
|
||||
use crate::daemon::queue::CommandQueue;
|
||||
use crate::daemon::state::DaemonState;
|
||||
use crate::ghidra::bridge::GhidraBridge;
|
||||
|
||||
pub mod cache;
|
||||
pub mod handler;
|
||||
pub mod ipc_server;
|
||||
pub mod process;
|
||||
pub mod queue;
|
||||
pub mod rpc;
|
||||
@@ -27,15 +27,17 @@ pub mod state;
|
||||
pub struct DaemonConfig {
|
||||
/// Path to the project directory
|
||||
pub project_path: PathBuf,
|
||||
/// RPC port (None = auto-select)
|
||||
/// RPC port (None = auto-select) - kept for backwards compatibility
|
||||
pub port: Option<u16>,
|
||||
/// Ghidra installation directory
|
||||
pub ghidra_install_dir: Option<PathBuf>,
|
||||
/// Log file path
|
||||
pub log_file: PathBuf,
|
||||
/// Program name to load
|
||||
pub program_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Run the daemon.
|
||||
/// 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());
|
||||
@@ -44,52 +46,60 @@ pub async fn run(config: DaemonConfig) -> Result<()> {
|
||||
let data_dir = get_data_dir()
|
||||
.context("Failed to get data directory")?;
|
||||
|
||||
// Load project state
|
||||
let _state = Arc::new(
|
||||
DaemonState::load(&config.project_path, config.ghidra_install_dir.as_deref())
|
||||
.context("Failed to load project state")?
|
||||
);
|
||||
|
||||
info!("Project state loaded successfully");
|
||||
|
||||
// Create command queue
|
||||
let queue = Arc::new(CommandQueue::new(config.project_path.clone()));
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, _shutdown_rx) = broadcast::channel::<()>(1);
|
||||
|
||||
// Start RPC server
|
||||
let port = self::rpc::run_server(queue.clone(), config.port, shutdown_tx.clone()).await
|
||||
.context("Failed to start RPC server")?;
|
||||
// Initialize the Ghidra bridge (starts as None until we have a program)
|
||||
let bridge: Arc<Mutex<Option<GhidraBridge>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
info!("RPC server listening on port {}", port);
|
||||
// If we have Ghidra install dir and program name, start the bridge
|
||||
if let (Some(ghidra_dir), Some(program_name)) = (&config.ghidra_install_dir, &config.program_name) {
|
||||
info!("Starting Ghidra bridge for program: {}", program_name);
|
||||
|
||||
let project_name = config.project_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("project")
|
||||
.to_string();
|
||||
|
||||
let mut new_bridge = GhidraBridge::new(
|
||||
ghidra_dir.clone(),
|
||||
config.project_path.clone(),
|
||||
project_name,
|
||||
program_name.clone(),
|
||||
);
|
||||
|
||||
if let Err(e) = new_bridge.start() {
|
||||
warn!("Failed to start Ghidra bridge: {}. Will operate without persistent connection.", e);
|
||||
} else {
|
||||
info!("Ghidra bridge started successfully");
|
||||
let mut bridge_guard = bridge.lock().await;
|
||||
*bridge_guard = Some(new_bridge);
|
||||
}
|
||||
} else {
|
||||
info!("No program specified, bridge will be started on first command");
|
||||
}
|
||||
|
||||
// Write lock file
|
||||
let daemon_info = DaemonInfo::new(&config.project_path, port, &config.log_file);
|
||||
// Write lock file with a placeholder port (IPC doesn't use TCP ports)
|
||||
let placeholder_port = config.port.unwrap_or(0);
|
||||
let daemon_info = DaemonInfo::new(&config.project_path, placeholder_port, &config.log_file);
|
||||
write_daemon_info(&data_dir, &config.project_path, &daemon_info)
|
||||
.context("Failed to write lock file")?;
|
||||
|
||||
// Start cache cleanup task
|
||||
let cache_cleanup_handle = {
|
||||
let queue = queue.clone();
|
||||
let shutdown_tx = shutdown_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut shutdown_rx = shutdown_tx.subscribe();
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(300)) => {
|
||||
// Cleanup cache every 5 minutes
|
||||
// Note: This would need access to the cache
|
||||
// For now, we'll skip this as the cache is internal to the queue
|
||||
}
|
||||
_ = shutdown_rx.recv() => {
|
||||
info!("Cache cleanup task stopping");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
// Start IPC server task
|
||||
let ipc_bridge = bridge.clone();
|
||||
let ipc_shutdown_tx = shutdown_tx.clone();
|
||||
let ipc_handle = tokio::spawn(async move {
|
||||
if let Err(e) = ipc_server::run_ipc_server(ipc_bridge, ipc_shutdown_tx).await {
|
||||
error!("IPC server error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Also start the legacy RPC server for backwards compatibility
|
||||
let queue = Arc::new(queue::CommandQueue::new(config.project_path.clone()));
|
||||
let rpc_port = rpc::run_server(queue.clone(), config.port, shutdown_tx.clone()).await
|
||||
.context("Failed to start RPC server")?;
|
||||
info!("Legacy RPC server listening on port {} (for backwards compatibility)", rpc_port);
|
||||
|
||||
// Wait for shutdown signal
|
||||
let shutdown_reason = wait_for_shutdown(shutdown_tx.clone()).await;
|
||||
@@ -99,13 +109,24 @@ pub async fn run(config: DaemonConfig) -> Result<()> {
|
||||
// Clean up
|
||||
shutdown_tx.send(()).ok(); // Signal all tasks to stop
|
||||
|
||||
// Wait for cache cleanup to stop (with timeout)
|
||||
// Stop the bridge
|
||||
{
|
||||
let mut bridge_guard = bridge.lock().await;
|
||||
if let Some(mut b) = bridge_guard.take() {
|
||||
info!("Stopping Ghidra bridge...");
|
||||
if let Err(e) = b.stop() {
|
||||
error!("Error stopping bridge: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for IPC server to stop (with timeout)
|
||||
tokio::select! {
|
||||
_ = cache_cleanup_handle => {
|
||||
info!("Cache cleanup task stopped");
|
||||
_ = ipc_handle => {
|
||||
info!("IPC server stopped");
|
||||
}
|
||||
_ = tokio::time::sleep(tokio::time::Duration::from_secs(5)) => {
|
||||
warn!("Cache cleanup task did not stop in time");
|
||||
warn!("IPC server did not stop in time");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +206,7 @@ mod tests {
|
||||
port: Some(17700),
|
||||
ghidra_install_dir: None,
|
||||
log_file: PathBuf::from("/test/logs/daemon.log"),
|
||||
program_name: None,
|
||||
};
|
||||
|
||||
assert_eq!(config.port, Some(17700));
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Ghidra Bridge - manages a persistent Ghidra process.
|
||||
//!
|
||||
//! Instead of spawning a new `analyzeHeadless` process for each command,
|
||||
//! the bridge maintains a single long-running Ghidra process that serves
|
||||
//! commands via a TCP socket.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Default bridge port
|
||||
const DEFAULT_BRIDGE_PORT: u16 = 18700;
|
||||
|
||||
/// Response from the bridge
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BridgeResponse<T> {
|
||||
pub status: String,
|
||||
pub data: Option<T>,
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Request to the bridge
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BridgeRequest {
|
||||
command: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
args: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Manages a persistent Ghidra bridge process.
|
||||
pub struct GhidraBridge {
|
||||
/// Child process handle
|
||||
child: Option<Child>,
|
||||
/// TCP connection to the bridge
|
||||
stream: Option<TcpStream>,
|
||||
/// Bridge port
|
||||
port: u16,
|
||||
/// Project name
|
||||
project_name: String,
|
||||
/// Program name
|
||||
program_name: String,
|
||||
/// Path to Ghidra installation
|
||||
ghidra_install_dir: PathBuf,
|
||||
/// Project directory
|
||||
project_dir: PathBuf,
|
||||
/// Whether the bridge is running
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl GhidraBridge {
|
||||
/// Create a new bridge (not started yet).
|
||||
pub fn new(
|
||||
ghidra_install_dir: PathBuf,
|
||||
project_dir: PathBuf,
|
||||
project_name: String,
|
||||
program_name: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
child: None,
|
||||
stream: None,
|
||||
port: DEFAULT_BRIDGE_PORT,
|
||||
project_name,
|
||||
program_name,
|
||||
ghidra_install_dir,
|
||||
project_dir,
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the bridge.
|
||||
pub fn start(&mut self) -> Result<()> {
|
||||
if self.running.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Starting Ghidra bridge...");
|
||||
|
||||
// Find analyzeHeadless script
|
||||
let headless_script = self.find_headless_script()?;
|
||||
|
||||
// Get bridge script path
|
||||
let bridge_script = self.get_bridge_script_path()?;
|
||||
|
||||
// Build command
|
||||
let mut cmd = Command::new(&headless_script);
|
||||
cmd.arg(&self.project_dir)
|
||||
.arg(&self.project_name)
|
||||
.arg("-process")
|
||||
.arg(&self.program_name)
|
||||
.arg("-noanalysis")
|
||||
.arg("-scriptPath")
|
||||
.arg(bridge_script.parent().unwrap())
|
||||
.arg("-postScript")
|
||||
.arg("bridge.py")
|
||||
.arg(self.port.to_string())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Spawn the process
|
||||
let mut child = cmd.spawn().context("Failed to spawn Ghidra headless")?;
|
||||
|
||||
// Wait for ready signal
|
||||
let stdout = child.stdout.take().expect("stdout should be piped");
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
let mut ready = false;
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
debug!("Ghidra: {}", line);
|
||||
|
||||
// Look for ready signal
|
||||
if line.contains("---GHIDRA_CLI_START---") {
|
||||
// Read the next line for the JSON ready message
|
||||
continue;
|
||||
}
|
||||
if line.contains("\"status\": \"ready\"") || line.contains("\"status\":\"ready\"") {
|
||||
info!("Bridge is ready on port {}", self.port);
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
if line.contains("---GHIDRA_CLI_END---") && ready {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !ready {
|
||||
// Check if process died
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
anyhow::bail!("Ghidra process exited with status: {}", status);
|
||||
}
|
||||
Ok(None) => {
|
||||
anyhow::bail!("Ghidra bridge did not send ready signal");
|
||||
}
|
||||
Err(e) => {
|
||||
anyhow::bail!("Error checking process status: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the bridge
|
||||
let stream = TcpStream::connect(format!("127.0.0.1:{}", self.port))
|
||||
.context("Failed to connect to bridge")?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(300)))
|
||||
.ok();
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(30)))
|
||||
.ok();
|
||||
|
||||
self.child = Some(child);
|
||||
self.stream = Some(stream);
|
||||
self.running.store(true, Ordering::SeqCst);
|
||||
|
||||
info!("Ghidra bridge started successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a command to the bridge.
|
||||
pub fn send_command<T: for<'de> Deserialize<'de>>(
|
||||
&mut self,
|
||||
command: &str,
|
||||
args: Option<serde_json::Value>,
|
||||
) -> Result<BridgeResponse<T>> {
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
anyhow::bail!("Bridge not running");
|
||||
}
|
||||
|
||||
let stream = self.stream.as_mut().ok_or_else(|| {
|
||||
anyhow::anyhow!("No connection to bridge")
|
||||
})?;
|
||||
|
||||
let request = BridgeRequest {
|
||||
command: command.to_string(),
|
||||
args,
|
||||
};
|
||||
|
||||
let request_json = serde_json::to_string(&request)?;
|
||||
debug!("Sending: {}", request_json);
|
||||
|
||||
// Send request
|
||||
writeln!(stream, "{}", request_json)?;
|
||||
stream.flush()?;
|
||||
|
||||
// Read response
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut response_line = String::new();
|
||||
reader.read_line(&mut response_line)?;
|
||||
|
||||
debug!("Received: {}", response_line.trim());
|
||||
|
||||
let response: BridgeResponse<T> = serde_json::from_str(&response_line)?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Stop the bridge.
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
if !self.running.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Stopping Ghidra bridge...");
|
||||
|
||||
// Send shutdown command
|
||||
if let Ok(response) = self.send_command::<serde_json::Value>("shutdown", None) {
|
||||
debug!("Shutdown response: {:?}", response);
|
||||
}
|
||||
|
||||
// Close stream
|
||||
self.stream.take();
|
||||
|
||||
// Wait for child to exit
|
||||
if let Some(mut child) = self.child.take() {
|
||||
match child.wait_timeout(Duration::from_secs(10)) {
|
||||
Ok(Some(status)) => {
|
||||
info!("Ghidra process exited with status: {}", status);
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("Ghidra process did not exit, killing...");
|
||||
child.kill().ok();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error waiting for process: {}", e);
|
||||
child.kill().ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
info!("Bridge stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the bridge is running.
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.running.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Get the embedded bridge script path, writing it to disk if needed.
|
||||
fn get_bridge_script_path(&self) -> Result<PathBuf> {
|
||||
let scripts_dir = dirs::config_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not determine config directory"))?
|
||||
.join("ghidra-cli")
|
||||
.join("scripts");
|
||||
|
||||
std::fs::create_dir_all(&scripts_dir)?;
|
||||
|
||||
let script_path = scripts_dir.join("bridge.py");
|
||||
|
||||
// Always write the latest version of the script
|
||||
let script_content = include_str!("scripts/bridge.py");
|
||||
std::fs::write(&script_path, script_content)?;
|
||||
|
||||
Ok(script_path)
|
||||
}
|
||||
|
||||
/// Find the analyzeHeadless script.
|
||||
fn find_headless_script(&self) -> Result<PathBuf> {
|
||||
#[cfg(unix)]
|
||||
let script_name = "analyzeHeadless";
|
||||
#[cfg(windows)]
|
||||
let script_name = "analyzeHeadless.bat";
|
||||
|
||||
let support_dir = self.ghidra_install_dir.join("support");
|
||||
let script_path = support_dir.join(script_name);
|
||||
|
||||
if script_path.exists() {
|
||||
Ok(script_path)
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"analyzeHeadless not found at: {}",
|
||||
script_path.display()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GhidraBridge {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.stop() {
|
||||
error!("Error stopping bridge on drop: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience wrapper for wait_timeout on Child
|
||||
trait ChildExt {
|
||||
fn wait_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<std::process::ExitStatus>>;
|
||||
}
|
||||
|
||||
impl ChildExt for Child {
|
||||
fn wait_timeout(&mut self, timeout: Duration) -> std::io::Result<Option<std::process::ExitStatus>> {
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match self.try_wait()? {
|
||||
Some(status) => return Ok(Some(status)),
|
||||
None => {
|
||||
if start.elapsed() >= timeout {
|
||||
return Ok(None);
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bridge_request_serialization() {
|
||||
let req = BridgeRequest {
|
||||
command: "list_functions".to_string(),
|
||||
args: Some(serde_json::json!({"limit": 100})),
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("list_functions"));
|
||||
assert!(json.contains("100"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bridge_response_deserialization() {
|
||||
let json = r#"{"status": "success", "data": {"count": 42}}"#;
|
||||
let resp: BridgeResponse<serde_json::Value> = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(resp.status, "success");
|
||||
assert!(resp.data.is_some());
|
||||
}
|
||||
}
|
||||
+16
-37
@@ -78,43 +78,22 @@ impl<'a> HeadlessExecutor<'a> {
|
||||
}
|
||||
|
||||
fn extract_json_from_output(&self, output: &str) -> Result<String> {
|
||||
// Find the JSON output in the Ghidra output
|
||||
// Look for lines starting with { or [
|
||||
let lines: Vec<&str> = output.lines().collect();
|
||||
|
||||
let mut json_start = None;
|
||||
let mut json_end = None;
|
||||
let mut brace_count = 0;
|
||||
|
||||
for (i, line) in lines.iter().enumerate() {
|
||||
let trimmed = line.trim();
|
||||
|
||||
if json_start.is_none() && (trimmed.starts_with('{') || trimmed.starts_with('[')) {
|
||||
json_start = Some(i);
|
||||
brace_count = trimmed.chars().filter(|&c| c == '{' || c == '[').count() as i32;
|
||||
brace_count -= trimmed.chars().filter(|&c| c == '}' || c == ']').count() as i32;
|
||||
|
||||
if brace_count == 0 {
|
||||
json_end = Some(i);
|
||||
break;
|
||||
}
|
||||
} else if json_start.is_some() {
|
||||
brace_count += trimmed.chars().filter(|&c| c == '{' || c == '[').count() as i32;
|
||||
brace_count -= trimmed.chars().filter(|&c| c == '}' || c == ']').count() as i32;
|
||||
|
||||
if brace_count == 0 {
|
||||
json_end = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(start), Some(end)) = (json_start, json_end) {
|
||||
let json_lines = &lines[start..=end];
|
||||
Ok(json_lines.join("\n"))
|
||||
} else {
|
||||
Err(GhidraError::ExecutionFailed("Could not find JSON in script output".to_string()))
|
||||
}
|
||||
// Use marker-based extraction for reliable JSON parsing
|
||||
const START_MARKER: &str = "---GHIDRA_CLI_START---";
|
||||
const END_MARKER: &str = "---GHIDRA_CLI_END---";
|
||||
|
||||
let start = output.find(START_MARKER)
|
||||
.ok_or_else(|| GhidraError::ExecutionFailed(
|
||||
"Missing start marker in script output".to_string()
|
||||
))?
|
||||
+ START_MARKER.len();
|
||||
|
||||
let end = output.find(END_MARKER)
|
||||
.ok_or_else(|| GhidraError::ExecutionFailed(
|
||||
"Missing end marker in script output".to_string()
|
||||
))?;
|
||||
|
||||
Ok(output[start..end].trim().to_string())
|
||||
}
|
||||
|
||||
fn get_scripts_dir(&self) -> Result<std::path::PathBuf> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod bridge;
|
||||
pub mod headless;
|
||||
pub mod data;
|
||||
pub mod scripts;
|
||||
@@ -8,6 +9,7 @@ use std::process::Command;
|
||||
use crate::config::Config;
|
||||
use crate::error::{GhidraError, Result};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GhidraClient {
|
||||
config: Config,
|
||||
install_dir: PathBuf,
|
||||
|
||||
@@ -52,7 +52,9 @@ for func in function_manager.getFunctions(True):
|
||||
|
||||
functions.append(func_data)
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(functions, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -97,9 +99,13 @@ if results.decompileCompleted():
|
||||
"code": code
|
||||
}
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(result, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
else:
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps({"error": "Decompilation failed"}))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -140,7 +146,9 @@ while data_iterator.hasNext():
|
||||
# Skip strings that cause encoding issues
|
||||
pass
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(strings, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -167,7 +175,9 @@ for symbol in symbol_table.getExternalSymbols():
|
||||
}
|
||||
imports.append(import_data)
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(imports, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -189,7 +199,9 @@ for symbol in symbol_table.getSymbolIterator():
|
||||
}
|
||||
exports.append(export_data)
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(exports, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -226,7 +238,9 @@ for block in memory.getBlocks():
|
||||
block_data["permissions"] = perms
|
||||
blocks.append(block_data)
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(blocks, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -259,7 +273,9 @@ for instruction in listing.getInstructions(True):
|
||||
|
||||
info["instruction_count"] = instruction_count
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(info, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
@@ -295,7 +311,9 @@ for ref in refs:
|
||||
}
|
||||
xrefs.append(xref_data)
|
||||
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps(xrefs, indent=2))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
"#
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
# Ghidra CLI Bridge Script
|
||||
# @category Bridge
|
||||
# @keybinding
|
||||
# @menupath Tools.Start CLI Bridge
|
||||
# @toolbar
|
||||
#
|
||||
# This script runs a persistent TCP server inside Ghidra to serve CLI commands.
|
||||
# It keeps Ghidra loaded in memory for fast command execution.
|
||||
|
||||
import socket
|
||||
import json
|
||||
import threading
|
||||
from ghidra.util.task import ConsoleTaskMonitor
|
||||
from ghidra.app.decompiler import DecompInterface
|
||||
|
||||
# Default bridge port
|
||||
BRIDGE_PORT = 18700
|
||||
|
||||
# --- Command Handlers ---
|
||||
|
||||
def handle_ping(args):
|
||||
"""Health check."""
|
||||
return {"message": "pong"}
|
||||
|
||||
def handle_program_info(args):
|
||||
"""Get current program information."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
info = {
|
||||
"name": currentProgram.getName(),
|
||||
"executable_path": currentProgram.getExecutablePath(),
|
||||
"executable_format": currentProgram.getExecutableFormat(),
|
||||
"compiler": currentProgram.getCompiler() if currentProgram.getCompiler() else None,
|
||||
"language": str(currentProgram.getLanguage()),
|
||||
"image_base": str(currentProgram.getImageBase()),
|
||||
"min_address": str(currentProgram.getMinAddress()),
|
||||
"max_address": str(currentProgram.getMaxAddress())
|
||||
}
|
||||
|
||||
function_manager = currentProgram.getFunctionManager()
|
||||
info["function_count"] = function_manager.getFunctionCount()
|
||||
|
||||
return info
|
||||
|
||||
def handle_list_functions(args):
|
||||
"""List all functions in the program."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
limit = args.get("limit")
|
||||
name_filter = args.get("filter")
|
||||
|
||||
functions = []
|
||||
function_manager = currentProgram.getFunctionManager()
|
||||
count = 0
|
||||
|
||||
for func in function_manager.getFunctions(True):
|
||||
if limit and count >= limit:
|
||||
break
|
||||
|
||||
name = func.getName()
|
||||
if name_filter and name_filter.lower() not in name.lower():
|
||||
continue
|
||||
|
||||
entry = func.getEntryPoint()
|
||||
body = func.getBody()
|
||||
|
||||
func_data = {
|
||||
"name": name,
|
||||
"address": str(entry),
|
||||
"size": body.getNumAddresses(),
|
||||
"entry_point": str(entry),
|
||||
"signature": func.getPrototypeString(False, False) if func.getSignature() else None,
|
||||
"calling_convention": func.getCallingConventionName(),
|
||||
"comment": func.getComment()
|
||||
}
|
||||
|
||||
functions.append(func_data)
|
||||
count += 1
|
||||
|
||||
return {"functions": functions, "count": len(functions)}
|
||||
|
||||
def handle_decompile(args):
|
||||
"""Decompile a function at the given address."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
addr_str = args.get("address")
|
||||
if not addr_str:
|
||||
return {"error": "No address provided"}
|
||||
|
||||
addr = currentProgram.getAddressFactory().getAddress(addr_str)
|
||||
if addr is None:
|
||||
return {"error": "Invalid address: " + addr_str}
|
||||
|
||||
function_manager = currentProgram.getFunctionManager()
|
||||
func = function_manager.getFunctionContaining(addr)
|
||||
|
||||
if not func:
|
||||
return {"error": "No function at address " + addr_str}
|
||||
|
||||
decompiler = DecompInterface()
|
||||
decompiler.openProgram(currentProgram)
|
||||
|
||||
monitor = ConsoleTaskMonitor()
|
||||
results = decompiler.decompileFunction(func, 30, monitor)
|
||||
|
||||
if results.decompileCompleted():
|
||||
code = results.getDecompiledFunction().getC()
|
||||
return {
|
||||
"name": func.getName(),
|
||||
"address": str(func.getEntryPoint()),
|
||||
"signature": func.getPrototypeString(False, False),
|
||||
"code": code
|
||||
}
|
||||
else:
|
||||
return {"error": "Decompilation failed"}
|
||||
|
||||
def handle_list_strings(args):
|
||||
"""List all strings in the program."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
limit = args.get("limit")
|
||||
|
||||
strings = []
|
||||
listing = currentProgram.getListing()
|
||||
data_iterator = listing.getDefinedData(True)
|
||||
count = 0
|
||||
|
||||
while data_iterator.hasNext():
|
||||
if limit and count >= limit:
|
||||
break
|
||||
|
||||
data = data_iterator.next()
|
||||
if data.hasStringValue():
|
||||
try:
|
||||
string_val = str(data.getValue())
|
||||
string_data = {
|
||||
"address": str(data.getAddress()),
|
||||
"value": string_val,
|
||||
"length": len(string_val)
|
||||
}
|
||||
strings.append(string_data)
|
||||
count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"strings": strings, "count": len(strings)}
|
||||
|
||||
def handle_list_imports(args):
|
||||
"""List all imports in the program."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
imports = []
|
||||
symbol_table = currentProgram.getSymbolTable()
|
||||
external_manager = currentProgram.getExternalManager()
|
||||
|
||||
for symbol in symbol_table.getExternalSymbols():
|
||||
external_location = external_manager.getExternalLocation(symbol)
|
||||
|
||||
if external_location:
|
||||
import_data = {
|
||||
"name": symbol.getName(),
|
||||
"address": str(symbol.getAddress()),
|
||||
"library": external_location.getLibraryName()
|
||||
}
|
||||
imports.append(import_data)
|
||||
|
||||
return {"imports": imports, "count": len(imports)}
|
||||
|
||||
def handle_list_exports(args):
|
||||
"""List all exports in the program."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
exports = []
|
||||
symbol_table = currentProgram.getSymbolTable()
|
||||
|
||||
for symbol in symbol_table.getSymbolIterator():
|
||||
if symbol.isExternalEntryPoint():
|
||||
export_data = {
|
||||
"name": symbol.getName(),
|
||||
"address": str(symbol.getAddress())
|
||||
}
|
||||
exports.append(export_data)
|
||||
|
||||
return {"exports": exports, "count": len(exports)}
|
||||
|
||||
def handle_memory_map(args):
|
||||
"""Get memory map."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
blocks = []
|
||||
memory = currentProgram.getMemory()
|
||||
|
||||
for block in memory.getBlocks():
|
||||
perms = ""
|
||||
if block.isRead():
|
||||
perms += "r"
|
||||
if block.isWrite():
|
||||
perms += "w"
|
||||
if block.isExecute():
|
||||
perms += "x"
|
||||
|
||||
block_data = {
|
||||
"name": block.getName(),
|
||||
"start": str(block.getStart()),
|
||||
"end": str(block.getEnd()),
|
||||
"size": block.getSize(),
|
||||
"permissions": perms,
|
||||
"is_initialized": block.isInitialized(),
|
||||
"is_loaded": block.isLoaded()
|
||||
}
|
||||
blocks.append(block_data)
|
||||
|
||||
return {"blocks": blocks, "count": len(blocks)}
|
||||
|
||||
def handle_xrefs_to(args):
|
||||
"""Get cross-references to an address."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
addr_str = args.get("address")
|
||||
if not addr_str:
|
||||
return {"error": "No address provided"}
|
||||
|
||||
addr = currentProgram.getAddressFactory().getAddress(addr_str)
|
||||
if addr is None:
|
||||
return {"error": "Invalid address: " + addr_str}
|
||||
|
||||
xrefs = []
|
||||
refs = currentProgram.getReferenceManager().getReferencesTo(addr)
|
||||
function_manager = currentProgram.getFunctionManager()
|
||||
|
||||
for ref in refs:
|
||||
from_addr = ref.getFromAddress()
|
||||
from_func = function_manager.getFunctionContaining(from_addr)
|
||||
to_func = function_manager.getFunctionContaining(addr)
|
||||
|
||||
xref_data = {
|
||||
"from": str(from_addr),
|
||||
"to": str(addr),
|
||||
"ref_type": str(ref.getReferenceType()),
|
||||
"from_function": from_func.getName() if from_func else None,
|
||||
"to_function": to_func.getName() if to_func else None
|
||||
}
|
||||
xrefs.append(xref_data)
|
||||
|
||||
return {"xrefs": xrefs, "count": len(xrefs)}
|
||||
|
||||
def handle_xrefs_from(args):
|
||||
"""Get cross-references from an address."""
|
||||
if currentProgram is None:
|
||||
return {"error": "No program loaded"}
|
||||
|
||||
addr_str = args.get("address")
|
||||
if not addr_str:
|
||||
return {"error": "No address provided"}
|
||||
|
||||
addr = currentProgram.getAddressFactory().getAddress(addr_str)
|
||||
if addr is None:
|
||||
return {"error": "Invalid address: " + addr_str}
|
||||
|
||||
xrefs = []
|
||||
refs = currentProgram.getReferenceManager().getReferencesFrom(addr)
|
||||
function_manager = currentProgram.getFunctionManager()
|
||||
|
||||
for ref in refs:
|
||||
to_addr = ref.getToAddress()
|
||||
from_func = function_manager.getFunctionContaining(addr)
|
||||
to_func = function_manager.getFunctionContaining(to_addr)
|
||||
|
||||
xref_data = {
|
||||
"from": str(addr),
|
||||
"to": str(to_addr),
|
||||
"ref_type": str(ref.getReferenceType()),
|
||||
"from_function": from_func.getName() if from_func else None,
|
||||
"to_function": to_func.getName() if to_func else None
|
||||
}
|
||||
xrefs.append(xref_data)
|
||||
|
||||
return {"xrefs": xrefs, "count": len(xrefs)}
|
||||
|
||||
# --- Command Router ---
|
||||
|
||||
COMMANDS = {
|
||||
"ping": handle_ping,
|
||||
"program_info": handle_program_info,
|
||||
"list_functions": handle_list_functions,
|
||||
"decompile": handle_decompile,
|
||||
"list_strings": handle_list_strings,
|
||||
"list_imports": handle_list_imports,
|
||||
"list_exports": handle_list_exports,
|
||||
"memory_map": handle_memory_map,
|
||||
"xrefs_to": handle_xrefs_to,
|
||||
"xrefs_from": handle_xrefs_from,
|
||||
}
|
||||
|
||||
# --- Server Logic ---
|
||||
|
||||
def handle_request(line):
|
||||
"""Parse and handle a single JSON request."""
|
||||
try:
|
||||
req = json.loads(line)
|
||||
cmd = req.get("command")
|
||||
args = req.get("args", {})
|
||||
|
||||
if cmd == "shutdown":
|
||||
return {"status": "shutdown"}, True
|
||||
|
||||
if cmd in COMMANDS:
|
||||
result = COMMANDS[cmd](args)
|
||||
if "error" in result:
|
||||
return {"status": "error", "message": result["error"]}, False
|
||||
return {"status": "success", "data": result}, False
|
||||
else:
|
||||
return {"status": "error", "message": "Unknown command: " + str(cmd)}, False
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": str(e)}, False
|
||||
|
||||
def start_server(port=BRIDGE_PORT):
|
||||
"""Start the bridge server."""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind(('127.0.0.1', port))
|
||||
s.listen(1)
|
||||
|
||||
# Signal ready to the parent process
|
||||
print("---GHIDRA_CLI_START---")
|
||||
print(json.dumps({"status": "ready", "port": port}))
|
||||
print("---GHIDRA_CLI_END---")
|
||||
|
||||
running = True
|
||||
while running:
|
||||
try:
|
||||
conn, addr = s.accept()
|
||||
f = conn.makefile('r')
|
||||
out = conn.makefile('w')
|
||||
|
||||
try:
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
response, should_shutdown = handle_request(line.strip())
|
||||
out.write(json.dumps(response) + "\n")
|
||||
out.flush()
|
||||
|
||||
if should_shutdown:
|
||||
running = False
|
||||
break
|
||||
finally:
|
||||
f.close()
|
||||
out.close()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
print("Bridge error: " + str(e))
|
||||
|
||||
s.close()
|
||||
|
||||
# --- Entry Point ---
|
||||
|
||||
if __name__ == "__main__" or True: # Also runs when sourced by Ghidra
|
||||
# Determine port from args if provided
|
||||
port = BRIDGE_PORT
|
||||
if 'args' in dir() and len(args) > 0:
|
||||
try:
|
||||
port = int(args[0])
|
||||
except:
|
||||
pass
|
||||
|
||||
# If running in GUI, run in background thread to not freeze UI
|
||||
if 'isRunningHeadless' in dir() and isRunningHeadless():
|
||||
start_server(port)
|
||||
else:
|
||||
# GUI mode - run in background thread
|
||||
t = threading.Thread(target=start_server, args=(port,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
print("Bridge started in background on port " + str(port))
|
||||
@@ -0,0 +1,153 @@
|
||||
//! CLI-side IPC client for communicating with the daemon.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::io::{ReadHalf, WriteHalf};
|
||||
|
||||
use super::protocol::{Command, Request, Response};
|
||||
use super::transport::{self, Stream};
|
||||
|
||||
/// Client for communicating with the Ghidra daemon.
|
||||
pub struct DaemonClient {
|
||||
reader: ReadHalf<Stream>,
|
||||
writer: WriteHalf<Stream>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl DaemonClient {
|
||||
/// Connect to the running daemon.
|
||||
pub async fn connect() -> Result<Self> {
|
||||
let stream = transport::connect().await.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound
|
||||
|| e.kind() == std::io::ErrorKind::ConnectionRefused
|
||||
{
|
||||
anyhow::anyhow!("Daemon not running")
|
||||
} else {
|
||||
anyhow::anyhow!("Failed to connect to daemon: {}", e)
|
||||
}
|
||||
})?;
|
||||
|
||||
let (reader, writer) = tokio::io::split(stream);
|
||||
|
||||
Ok(Self {
|
||||
reader,
|
||||
writer,
|
||||
next_id: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send a command and wait for the response.
|
||||
pub async fn send_command(&mut self, command: Command) -> Result<serde_json::Value> {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
let request = Request::new(id, command);
|
||||
let json = serde_json::to_vec(&request).context("Failed to serialize request")?;
|
||||
|
||||
transport::send_message(&mut self.writer, &json)
|
||||
.await
|
||||
.context("Failed to send message to daemon")?;
|
||||
|
||||
let response_data = transport::recv_message(&mut self.reader)
|
||||
.await
|
||||
.context("Failed to receive message from daemon")?;
|
||||
|
||||
let response: Response =
|
||||
serde_json::from_slice(&response_data).context("Failed to parse daemon response")?;
|
||||
|
||||
if response.id != id {
|
||||
anyhow::bail!(
|
||||
"Response ID mismatch: expected {}, got {}",
|
||||
id,
|
||||
response.id
|
||||
);
|
||||
}
|
||||
|
||||
if response.success {
|
||||
Ok(response.result.unwrap_or(serde_json::json!({})))
|
||||
} else {
|
||||
let error = response.error.unwrap_or_else(|| "Unknown error".to_string());
|
||||
anyhow::bail!("{}", error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if daemon is responding.
|
||||
pub async fn ping(&mut self) -> Result<bool> {
|
||||
match self.send_command(Command::Ping).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) if e.to_string().contains("not running") => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get daemon status.
|
||||
pub async fn status(&mut self) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::Status).await
|
||||
}
|
||||
|
||||
/// Shutdown the daemon.
|
||||
pub async fn shutdown(&mut self) -> Result<()> {
|
||||
self.send_command(Command::Shutdown).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the result cache.
|
||||
pub async fn clear_cache(&mut self) -> Result<()> {
|
||||
self.send_command(Command::ClearCache).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List functions.
|
||||
pub async fn list_functions(
|
||||
&mut self,
|
||||
limit: Option<usize>,
|
||||
filter: Option<String>,
|
||||
) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::ListFunctions { limit, filter })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Decompile a function.
|
||||
pub async fn decompile(&mut self, address: String) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::Decompile { address }).await
|
||||
}
|
||||
|
||||
/// List strings.
|
||||
pub async fn list_strings(&mut self, limit: Option<usize>) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::ListStrings { limit }).await
|
||||
}
|
||||
|
||||
/// List imports.
|
||||
pub async fn list_imports(&mut self) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::ListImports).await
|
||||
}
|
||||
|
||||
/// List exports.
|
||||
pub async fn list_exports(&mut self) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::ListExports).await
|
||||
}
|
||||
|
||||
/// Get memory map.
|
||||
pub async fn memory_map(&mut self) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::MemoryMap).await
|
||||
}
|
||||
|
||||
/// Get program info.
|
||||
pub async fn program_info(&mut self) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::ProgramInfo).await
|
||||
}
|
||||
|
||||
/// Get cross-references to an address.
|
||||
pub async fn xrefs_to(&mut self, address: String) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::XRefsTo { address }).await
|
||||
}
|
||||
|
||||
/// Get cross-references from an address.
|
||||
pub async fn xrefs_from(&mut self, address: String) -> Result<serde_json::Value> {
|
||||
self.send_command(Command::XRefsFrom { address }).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if daemon is running (without establishing a full connection).
|
||||
pub fn daemon_available() -> bool {
|
||||
transport::socket_exists()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! IPC module for CLI-to-daemon communication.
|
||||
//!
|
||||
//! This module provides cross-platform IPC using local sockets:
|
||||
//! - Unix domain sockets on Linux/macOS
|
||||
//! - Named pipes on Windows
|
||||
//!
|
||||
//! Follows the pattern from debugger-cli for reliable length-prefixed
|
||||
//! JSON message framing.
|
||||
|
||||
pub mod client;
|
||||
pub mod protocol;
|
||||
pub mod transport;
|
||||
|
||||
pub use client::DaemonClient;
|
||||
pub use protocol::{Command, Request, Response};
|
||||
@@ -0,0 +1,166 @@
|
||||
//! IPC protocol message types.
|
||||
//!
|
||||
//! Defines the request/response format for CLI ↔ daemon communication.
|
||||
//! Uses a typed command enum (not wrapping CLI Commands) for clean separation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// IPC request from CLI to daemon.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
/// Request ID for matching responses
|
||||
pub id: u64,
|
||||
/// The command to execute
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Create a new request with the given ID and command.
|
||||
pub fn new(id: u64, command: Command) -> Self {
|
||||
Self { id, command }
|
||||
}
|
||||
}
|
||||
|
||||
/// IPC response from daemon to CLI.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
/// Request ID this response corresponds to
|
||||
pub id: u64,
|
||||
/// Whether the command succeeded
|
||||
pub success: bool,
|
||||
/// Result data on success
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub result: Option<serde_json::Value>,
|
||||
/// Error message on failure
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Create a success response.
|
||||
pub fn success(id: u64, result: serde_json::Value) -> Self {
|
||||
Self {
|
||||
id,
|
||||
success: true,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an error response.
|
||||
pub fn error(id: u64, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
success: false,
|
||||
result: None,
|
||||
error: Some(message.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a success response with no data.
|
||||
pub fn ok(id: u64) -> Self {
|
||||
Self {
|
||||
id,
|
||||
success: true,
|
||||
result: Some(serde_json::json!({})),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands that can be sent from CLI to daemon.
|
||||
///
|
||||
/// These are separate from CLI Commands to decouple IPC from argument parsing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Command {
|
||||
// === Data Queries ===
|
||||
/// List functions in the program
|
||||
ListFunctions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
limit: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
filter: Option<String>,
|
||||
},
|
||||
|
||||
/// Decompile a function at address
|
||||
Decompile { address: String },
|
||||
|
||||
/// List strings in the program
|
||||
ListStrings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
limit: Option<usize>,
|
||||
},
|
||||
|
||||
/// List imports
|
||||
ListImports,
|
||||
|
||||
/// List exports
|
||||
ListExports,
|
||||
|
||||
/// Get memory map
|
||||
MemoryMap,
|
||||
|
||||
/// Get program info
|
||||
ProgramInfo,
|
||||
|
||||
/// Get cross-references to an address
|
||||
XRefsTo { address: String },
|
||||
|
||||
/// Get cross-references from an address
|
||||
XRefsFrom { address: String },
|
||||
|
||||
// === Session Management ===
|
||||
/// Health check
|
||||
Ping,
|
||||
|
||||
/// Get daemon status
|
||||
Status,
|
||||
|
||||
/// Clear result cache
|
||||
ClearCache,
|
||||
|
||||
/// Shutdown the daemon
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_request_serialization() {
|
||||
let request = Request::new(1, Command::Ping);
|
||||
let json = serde_json::to_string(&request).unwrap();
|
||||
let deserialized: Request = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.id, 1);
|
||||
assert!(matches!(deserialized.command, Command::Ping));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_success() {
|
||||
let response = Response::success(1, serde_json::json!({"count": 42}));
|
||||
assert!(response.success);
|
||||
assert_eq!(response.id, 1);
|
||||
assert!(response.result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_error() {
|
||||
let response = Response::error(1, "Something went wrong");
|
||||
assert!(!response.success);
|
||||
assert_eq!(response.error.as_ref().unwrap(), "Something went wrong");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_serialization() {
|
||||
let cmd = Command::ListFunctions {
|
||||
limit: Some(100),
|
||||
filter: Some("main".to_string()),
|
||||
};
|
||||
let json = serde_json::to_string(&cmd).unwrap();
|
||||
assert!(json.contains("list_functions"));
|
||||
assert!(json.contains("100"));
|
||||
assert!(json.contains("main"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Cross-platform IPC transport layer.
|
||||
//!
|
||||
//! Abstracts Unix domain sockets (Unix/macOS) and named pipes (Windows)
|
||||
//! using the interprocess crate. Uses length-prefixed message framing.
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
/// Maximum message size (10 MB)
|
||||
const MAX_MESSAGE_SIZE: u32 = 10 * 1024 * 1024;
|
||||
|
||||
/// Socket name for the daemon
|
||||
const SOCKET_NAME: &str = "ghidra-cli.sock";
|
||||
|
||||
// Platform-specific imports and type aliases
|
||||
#[cfg(unix)]
|
||||
pub mod platform {
|
||||
pub use interprocess::local_socket::tokio::{prelude::*, Listener, Stream};
|
||||
pub use interprocess::local_socket::{GenericFilePath, ListenerOptions};
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod platform {
|
||||
pub use interprocess::local_socket::tokio::{prelude::*, Listener, Stream};
|
||||
pub use interprocess::local_socket::{GenericNamespaced, ListenerOptions};
|
||||
}
|
||||
|
||||
pub use platform::*;
|
||||
|
||||
/// Get the socket directory path.
|
||||
fn socket_dir() -> io::Result<PathBuf> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Use XDG runtime dir or /tmp
|
||||
let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| std::env::temp_dir());
|
||||
let dir = runtime_dir.join("ghidra-cli");
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Windows named pipes don't need a directory
|
||||
Ok(PathBuf::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the socket path.
|
||||
pub fn socket_path() -> io::Result<PathBuf> {
|
||||
let dir = socket_dir()?;
|
||||
Ok(dir.join(SOCKET_NAME))
|
||||
}
|
||||
|
||||
/// Get the socket name for interprocess.
|
||||
pub fn socket_name() -> String {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
socket_path()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| format!("/tmp/ghidra-cli/{}", SOCKET_NAME))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Windows uses named pipe namespace
|
||||
format!("ghidra-cli-{}", std::process::id())
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure the socket directory exists.
|
||||
pub fn ensure_socket_dir() -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let dir = socket_dir()?;
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the socket file if it exists.
|
||||
pub fn remove_socket() -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let path = socket_path()?;
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the socket exists.
|
||||
pub fn socket_exists() -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
socket_path().map(|p| p.exists()).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, we can't easily check if a named pipe exists
|
||||
// We'll rely on connection attempts instead
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a listener for incoming IPC connections.
|
||||
pub async fn create_listener() -> io::Result<Listener> {
|
||||
// Ensure socket directory exists and clean up stale socket
|
||||
ensure_socket_dir()?;
|
||||
remove_socket()?;
|
||||
|
||||
let name = socket_name();
|
||||
|
||||
#[cfg(unix)]
|
||||
let listener = {
|
||||
let name = name.to_fs_name::<GenericFilePath>()?;
|
||||
ListenerOptions::new().name(name).create_tokio()?
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let listener = {
|
||||
let name = name.to_ns_name::<GenericNamespaced>()?;
|
||||
ListenerOptions::new().name(name).create_tokio()?
|
||||
};
|
||||
|
||||
// Set socket permissions on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let path = socket_path()?;
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
|
||||
}
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
/// Connect to the daemon's IPC socket.
|
||||
pub async fn connect() -> io::Result<Stream> {
|
||||
let name = socket_name();
|
||||
|
||||
#[cfg(unix)]
|
||||
let stream = {
|
||||
let name = name.to_fs_name::<GenericFilePath>()?;
|
||||
Stream::connect(name).await?
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let stream = {
|
||||
let name = name.to_ns_name::<GenericNamespaced>()?;
|
||||
Stream::connect(name).await?
|
||||
};
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Send a length-prefixed message.
|
||||
pub async fn send_message<W: AsyncWriteExt + Unpin>(writer: &mut W, data: &[u8]) -> io::Result<()> {
|
||||
if data.len() > MAX_MESSAGE_SIZE as usize {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Message too large",
|
||||
));
|
||||
}
|
||||
|
||||
let len = data.len() as u32;
|
||||
writer.write_all(&len.to_le_bytes()).await?;
|
||||
writer.write_all(data).await?;
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive a length-prefixed message.
|
||||
pub async fn recv_message<R: AsyncReadExt + Unpin>(reader: &mut R) -> io::Result<Vec<u8>> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
reader.read_exact(&mut len_buf).await?;
|
||||
let len = u32::from_le_bytes(len_buf);
|
||||
|
||||
if len > MAX_MESSAGE_SIZE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Message too large: {} bytes", len),
|
||||
));
|
||||
}
|
||||
|
||||
let mut data = vec![0u8; len as usize];
|
||||
reader.read_exact(&mut data).await?;
|
||||
Ok(data)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod error;
|
||||
mod filter;
|
||||
mod format;
|
||||
mod ghidra;
|
||||
mod ipc;
|
||||
mod query;
|
||||
|
||||
use clap::Parser;
|
||||
@@ -177,6 +178,7 @@ async fn handle_daemon_start(project: Option<String>, port: Option<u16>, foregro
|
||||
port,
|
||||
ghidra_install_dir: config.ghidra_install_dir.map(PathBuf::from),
|
||||
log_file,
|
||||
program_name: config.default_program.clone(),
|
||||
};
|
||||
|
||||
if foreground {
|
||||
|
||||
Reference in New Issue
Block a user