ripping out rpc and stuff

This commit is contained in:
Alexander Kiselev
2026-01-25 22:55:56 -08:00
parent 55bfb5b60e
commit cf0d15686d
13 changed files with 353 additions and 370 deletions
Generated
-10
View File
@@ -459,15 +459,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "daemonize"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e"
dependencies = [
"libc",
]
[[package]]
name = "deranged"
version = "0.5.5"
@@ -800,7 +791,6 @@ dependencies = [
"clap",
"comfy-table",
"csv",
"daemonize",
"dirs",
"dunce",
"env_logger",
-3
View File
@@ -80,9 +80,6 @@ zip = "0.6"
futures-util = "0.3"
indicatif = "0.17"
# Daemonization (Unix only)
[target.'cfg(unix)'.dependencies]
daemonize = "0.5"
[dev-dependencies]
assert_cmd = "2.0"
+3 -14
View File
@@ -21,15 +21,12 @@ pub mod handlers;
pub mod ipc_server;
pub mod process;
pub mod queue;
pub mod rpc;
pub mod state;
/// Daemon configuration.
pub struct DaemonConfig {
/// Path to the project directory
pub project_path: PathBuf,
/// RPC port (None = auto-select) - kept for backwards compatibility
pub port: Option<u16>,
/// Ghidra installation directory
pub ghidra_install_dir: Option<PathBuf>,
/// Log file path
@@ -81,9 +78,8 @@ pub async fn run(config: DaemonConfig) -> Result<()> {
info!("No program specified, bridge will be started on first command");
}
// Write lock file with a placeholder port (IPC doesn't use TCP ports)
let placeholder_port = config.port.unwrap_or(0);
let daemon_info = DaemonInfo::new(&config.project_path, placeholder_port, &config.log_file);
// Write lock 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 lock file")?;
@@ -96,12 +92,6 @@ pub async fn run(config: DaemonConfig) -> Result<()> {
}
});
// Also start the legacy RPC server for backwards compatibility
let queue = Arc::new(queue::CommandQueue::new(config.project_path.clone(), bridge.clone()));
let rpc_port = rpc::run_server(queue.clone(), config.port, shutdown_tx.clone()).await
.context("Failed to start RPC server")?;
info!("Legacy RPC server listening on port {} (for backwards compatibility)", rpc_port);
// Wait for shutdown signal
let shutdown_reason = wait_for_shutdown(shutdown_tx.clone()).await;
@@ -204,12 +194,11 @@ mod tests {
fn test_daemon_config() {
let config = DaemonConfig {
project_path: PathBuf::from("/test/project"),
port: Some(17700),
ghidra_install_dir: None,
log_file: PathBuf::from("/test/logs/daemon.log"),
program_name: None,
};
assert_eq!(config.port, Some(17700));
assert_eq!(config.project_path, PathBuf::from("/test/project"));
}
}
+3 -13
View File
@@ -16,8 +16,6 @@ use sysinfo::{System, Pid, ProcessRefreshKind};
pub struct DaemonInfo {
/// Process ID of the daemon
pub pid: u32,
/// RPC port the daemon is listening on
pub port: u16,
/// Project path being managed
pub project_path: PathBuf,
/// Log file path
@@ -28,10 +26,9 @@ pub struct DaemonInfo {
impl DaemonInfo {
/// Create new daemon info.
pub fn new(project_path: &Path, port: u16, log_file: &Path) -> Self {
pub fn new(project_path: &Path, log_file: &Path) -> Self {
Self {
pid: std::process::id(),
port,
project_path: project_path.to_path_buf(),
log_file: log_file.to_path_buf(),
started_at: Utc::now(),
@@ -132,11 +129,7 @@ pub fn get_running_daemon_info(data_dir: &Path, project_path: &Path) -> Result<O
/// 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: {}, port: {})",
info.pid,
info.port
);
bail!("Daemon is already running (PID: {})", info.pid);
}
Ok(())
}
@@ -150,11 +143,9 @@ mod tests {
fn test_daemon_info_creation() {
let info = DaemonInfo::new(
Path::new("/test/project"),
17700,
Path::new("/test/logs/daemon.log"),
);
assert_eq!(info.port, 17700);
assert_eq!(info.project_path, PathBuf::from("/test/project"));
}
@@ -164,7 +155,7 @@ mod tests {
let data_dir = temp_dir.path();
let project_path = PathBuf::from("/test/project");
let info = DaemonInfo::new(&project_path, 17700, Path::new("/test/logs/daemon.log"));
let info = DaemonInfo::new(&project_path, Path::new("/test/logs/daemon.log"));
// Write
write_daemon_info(data_dir, &project_path, &info)?;
@@ -173,7 +164,6 @@ mod tests {
let read_info = read_daemon_info(data_dir, &project_path)?;
assert!(read_info.is_some());
let read_info = read_info.unwrap();
assert_eq!(read_info.port, 17700);
assert_eq!(read_info.pid, info.pid);
// Remove
-277
View File
@@ -1,277 +0,0 @@
//! RPC protocol for daemon communication using JSON over TCP.
//!
//! Defines the request/response types and RPC server/client implementations.
#![allow(dead_code)]
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;
use tracing::{info, error};
use crate::cli::Commands;
use crate::daemon::queue::CommandQueue;
/// RPC request from client to daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DaemonRequest {
/// Execute a CLI command
Execute(Commands),
/// Shutdown the daemon
Shutdown,
/// Get daemon status
Status,
/// Ping the daemon
Ping,
}
/// RPC response from daemon to client.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DaemonResponse {
/// Command executed successfully with output
Success(String),
/// Command failed with error
Error(String),
/// Daemon status information
Status(DaemonStatus),
/// Pong response
Pong,
}
/// Daemon status information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonStatus {
/// Number of queued commands
pub queue_depth: usize,
/// Number of completed commands
pub completed_commands: usize,
/// Daemon uptime in seconds
pub uptime_seconds: u64,
/// Current project path
pub project_path: String,
}
/// RPC server implementation.
pub struct DaemonServer {
queue: Arc<CommandQueue>,
shutdown_tx: broadcast::Sender<()>,
started_at: std::time::Instant,
}
impl DaemonServer {
/// Create a new RPC server.
pub fn new(queue: Arc<CommandQueue>, shutdown_tx: broadcast::Sender<()>) -> Self {
Self {
queue,
shutdown_tx,
started_at: std::time::Instant::now(),
}
}
/// Handle a request and return a response.
async fn handle_request(&self, request: DaemonRequest) -> DaemonResponse {
match request {
DaemonRequest::Execute(command) => {
match self.queue.submit(command).await {
Ok(result) => DaemonResponse::Success(result),
Err(e) => DaemonResponse::Error(e.to_string()),
}
}
DaemonRequest::Shutdown => {
info!("Received shutdown request via RPC");
let _ = self.shutdown_tx.send(());
DaemonResponse::Success("Daemon shutting down".to_string())
}
DaemonRequest::Status => {
let status = DaemonStatus {
queue_depth: 0, // TODO: Get actual queue depth
completed_commands: 0, // TODO: Get actual completed count
uptime_seconds: self.started_at.elapsed().as_secs(),
project_path: self.queue.project_path().to_string_lossy().to_string(),
};
DaemonResponse::Status(status)
}
DaemonRequest::Ping => {
DaemonResponse::Pong
}
}
}
}
/// Run the RPC server.
pub async fn run_server(
queue: Arc<CommandQueue>,
port: Option<u16>,
shutdown_tx: broadcast::Sender<()>,
) -> Result<u16> {
let addr = SocketAddr::from(([127, 0, 0, 1], port.unwrap_or(0)));
let listener = TcpListener::bind(addr).await
.context("Failed to bind TCP listener")?;
let actual_port = listener.local_addr()
.context("Failed to get local address")?
.port();
info!("RPC server listening on port {}", actual_port);
let server = Arc::new(DaemonServer::new(queue, shutdown_tx.clone()));
let mut shutdown_rx = shutdown_tx.subscribe();
tokio::spawn(async move {
loop {
tokio::select! {
result = listener.accept() => {
match result {
Ok((stream, addr)) => {
info!("Accepted connection from {}", addr);
let server = server.clone();
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, server).await {
error!("Connection error: {}", e);
}
});
}
Err(e) => {
error!("Failed to accept connection: {}", e);
}
}
}
_ = shutdown_rx.recv() => {
info!("RPC server shutting down");
break;
}
}
}
});
Ok(actual_port)
}
/// Handle a single client connection using JSON over TCP.
async fn handle_connection(
stream: TcpStream,
server: Arc<DaemonServer>,
) -> Result<()> {
let (reader, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
loop {
line.clear();
let n = reader.read_line(&mut line).await
.context("Failed to read from stream")?;
if n == 0 {
// Connection closed
break;
}
// Parse request
let request: DaemonRequest = serde_json::from_str(&line)
.context("Failed to parse request")?;
// Handle request
let response = server.handle_request(request).await;
// Serialize and send response
let response_json = serde_json::to_string(&response)
.context("Failed to serialize response")?;
writer.write_all(response_json.as_bytes()).await
.context("Failed to write response")?;
writer.write_all(b"\n").await
.context("Failed to write newline")?;
}
Ok(())
}
/// RPC client for connecting to the daemon.
pub struct DaemonClient {
stream: TcpStream,
}
impl DaemonClient {
/// Connect to the daemon at the given port.
pub async fn connect(port: u16) -> Result<Self> {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let stream = TcpStream::connect(addr).await
.context("Failed to connect to daemon")?;
Ok(Self { stream })
}
/// Send a request to the daemon.
pub async fn request(&mut self, request: DaemonRequest) -> Result<DaemonResponse> {
// Serialize and send request
let request_json = serde_json::to_string(&request)
.context("Failed to serialize request")?;
self.stream.write_all(request_json.as_bytes()).await
.context("Failed to write request")?;
self.stream.write_all(b"\n").await
.context("Failed to write newline")?;
// Read response
let (reader, _) = self.stream.split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
reader.read_line(&mut line).await
.context("Failed to read response")?;
// Parse response
let response: DaemonResponse = serde_json::from_str(&line)
.context("Failed to parse response")?;
Ok(response)
}
/// Execute a command on the daemon.
pub async fn execute(&mut self, command: Commands) -> Result<String> {
match self.request(DaemonRequest::Execute(command)).await? {
DaemonResponse::Success(output) => Ok(output),
DaemonResponse::Error(e) => Err(anyhow::anyhow!(e)),
_ => Err(anyhow::anyhow!("Unexpected response")),
}
}
/// Get daemon status.
pub async fn status(&mut self) -> Result<DaemonStatus> {
match self.request(DaemonRequest::Status).await? {
DaemonResponse::Status(status) => Ok(status),
_ => Err(anyhow::anyhow!("Unexpected response")),
}
}
/// Shutdown the daemon.
pub async fn shutdown(&mut self) -> Result<()> {
self.request(DaemonRequest::Shutdown).await?;
Ok(())
}
/// Ping the daemon.
pub async fn ping(&mut self) -> Result<()> {
match self.request(DaemonRequest::Ping).await? {
DaemonResponse::Pong => Ok(()),
_ => Err(anyhow::anyhow!("Unexpected response")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_serialization() {
let request = DaemonRequest::Ping;
let json = serde_json::to_string(&request).unwrap();
let deserialized: DaemonRequest = serde_json::from_str(&json).unwrap();
matches!(deserialized, DaemonRequest::Ping);
}
}
+46 -39
View File
@@ -12,15 +12,12 @@ use clap::Parser;
use cli::{Cli, Commands, DaemonCommands, SetupArgs};
use config::Config;
use daemon::process::{get_data_dir, get_running_daemon_info, ensure_not_running};
use daemon::rpc as daemon_rpc;
use daemon::{DaemonConfig, run as run_daemon};
use error::{GhidraError, Result};
use ghidra::GhidraClient;
use std::path::PathBuf;
use tracing::info;
#[cfg(unix)]
use daemonize::Daemonize;
#[tokio::main]
async fn main() {
@@ -270,7 +267,6 @@ async fn handle_daemon_start(project: Option<String>, program: Option<String>, p
let daemon_config = DaemonConfig {
project_path: project_path.clone(),
port,
ghidra_install_dir: config.ghidra_install_dir.map(PathBuf::from),
log_file,
program_name,
@@ -286,7 +282,7 @@ async fn handle_daemon_start(project: Option<String>, program: Option<String>, p
#[cfg(unix)]
{
daemonize_unix(daemon_config)?;
daemonize_unix(daemon_config, port)?;
}
#[cfg(windows)]
@@ -309,10 +305,10 @@ async fn handle_daemon_stop(project: Option<String>) -> anyhow::Result<()> {
let project_path = resolve_project_path(&project, &config)?;
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
println!("Stopping daemon (PID: {}, port: {})...", daemon_info.pid, daemon_info.port);
println!("Stopping daemon (PID: {})...", daemon_info.pid);
// Connect and send shutdown
let mut client = daemon_rpc::DaemonClient::connect(daemon_info.port).await?;
// Connect via IPC and send shutdown
let mut client = ipc::client::DaemonClient::connect().await?;
client.shutdown().await?;
println!("Daemon stopped successfully");
@@ -344,18 +340,16 @@ async fn handle_daemon_status(project: Option<String>) -> anyhow::Result<()> {
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
println!("Daemon is running:");
println!(" PID: {}", daemon_info.pid);
println!(" Port: {}", daemon_info.port);
println!(" Project: {}", daemon_info.project_path.display());
println!(" Started: {}", daemon_info.started_at);
println!(" Log file: {}", daemon_info.log_file.display());
// Try to get detailed status from daemon
if let Ok(mut client) = daemon_rpc::DaemonClient::connect(daemon_info.port).await {
// Try to get detailed status from daemon via IPC
if let Ok(mut client) = ipc::client::DaemonClient::connect().await {
if let Ok(status) = client.status().await {
println!("\nDaemon status:");
println!(" Queue depth: {}", status.queue_depth);
println!(" Completed commands: {}", status.completed_commands);
println!(" Uptime: {} seconds", status.uptime_seconds);
if let Some(bridge_running) = status.get("bridge_running").and_then(|v| v.as_bool()) {
println!(" Bridge: {}", if bridge_running { "running" } else { "stopped" });
}
}
}
} else {
@@ -371,10 +365,10 @@ async fn handle_daemon_ping(project: Option<String>) -> anyhow::Result<()> {
let data_dir = get_data_dir()?;
let project_path = resolve_project_path(&project, &config)?;
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
let mut client = daemon_rpc::DaemonClient::connect(daemon_info.port).await?;
if let Some(_daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
let mut client = ipc::client::DaemonClient::connect().await?;
client.ping().await?;
println!("Daemon is responsive (port: {})", daemon_info.port);
println!("Daemon is responsive");
} else {
println!("No daemon running for project: {}", project_path.display());
}
@@ -460,13 +454,16 @@ async fn handle_setup(args: SetupArgs) -> anyhow::Result<()> {
Ok(())
}
/// Daemonize on Unix systems using fork and detach.
/// Daemonize by spawning a detached process (cross-platform).
///
/// This approach spawns a new process with --foreground flag instead of forking,
/// which avoids issues with Tokio runtime inheritance after fork.
#[cfg(unix)]
fn daemonize_unix(daemon_config: DaemonConfig) -> anyhow::Result<()> {
fn daemonize_unix(daemon_config: DaemonConfig, port: Option<u16>) -> anyhow::Result<()> {
use std::fs::OpenOptions;
use std::process::{Command, Stdio};
let log_file_path = daemon_config.log_file.clone();
let project_path = daemon_config.project_path.clone();
// Open log file for stdout/stderr
let log_file = OpenOptions::new()
@@ -477,27 +474,37 @@ fn daemonize_unix(daemon_config: DaemonConfig) -> anyhow::Result<()> {
let stdout = log_file.try_clone()?;
let stderr = log_file;
// Get PID file path - use hash of project path like lock file
let data_dir = get_data_dir()?;
let project_hash = format!("{:x}", md5::compute(project_path.to_string_lossy().as_bytes()));
let pid_file = data_dir.join(format!("daemon-{}.pid", project_hash));
// Get the current executable path
let exe_path = std::env::current_exe()?;
// Configure daemonization
let daemonize = Daemonize::new()
.pid_file(pid_file)
.working_directory("/")
.stdout(stdout)
.stderr(stderr);
// Build the command to spawn ourselves with --foreground flag
let mut cmd = Command::new(exe_path);
cmd.arg("daemon")
.arg("start")
.arg("--foreground");
// Fork and daemonize
daemonize.start()
.map_err(|e| anyhow::anyhow!("Failed to daemonize: {}", e))?;
// Add project path
cmd.arg("--project")
.arg(daemon_config.project_path.to_string_lossy().to_string());
// We're now in the daemon process - initialize tokio runtime and run
let runtime = tokio::runtime::Runtime::new()?;
runtime.block_on(async {
run_daemon(daemon_config).await
})?;
// Add program if specified
if let Some(program) = &daemon_config.program_name {
cmd.arg("--program").arg(program);
}
// Add port if specified
if let Some(p) = port {
cmd.arg("--port").arg(p.to_string());
}
// Redirect stdout/stderr to log file, detach stdin
cmd.stdin(Stdio::null());
cmd.stdout(stdout);
cmd.stderr(stderr);
// Spawn the detached process
cmd.spawn()
.map_err(|e| anyhow::anyhow!("Failed to spawn daemon process: {}", e))?;
Ok(())
}
+3 -3
View File
@@ -78,7 +78,7 @@ cargo test --test project_tests --lib
### Ghidra Installation
Tests check for Ghidra availability using `skip_if_no_ghidra!()` macro. Tests skip with clear message if `ghidra doctor` fails.
Tests assume Ghidra is installed. Use `require_ghidra!()` in tests that need a fast, explicit availability check; it fails the test if `ghidra doctor` fails.
### Test Fixtures
@@ -100,7 +100,7 @@ Add to appropriate file (`command_tests.rs`, `project_tests.rs`):
```rust
#[test]
fn test_my_command() {
skip_if_no_ghidra!();
require_ghidra!();
Command::cargo_bin("ghidra")
.unwrap()
@@ -118,7 +118,7 @@ Add to `daemon_tests.rs` or `query_tests.rs`:
#[test]
#[serial]
fn test_my_query() {
skip_if_no_ghidra!();
require_ghidra!();
let harness = &*HARNESS; // Shared daemon instance
+66
View File
@@ -3,10 +3,13 @@
use assert_cmd::Command;
use predicates::prelude::*;
#[macro_use]
mod common;
#[test]
fn test_version() {
require_ghidra!();
Command::cargo_bin("ghidra")
.unwrap()
.arg("version")
@@ -17,6 +20,8 @@ fn test_version() {
#[test]
fn test_doctor() {
require_ghidra!();
Command::cargo_bin("ghidra")
.unwrap()
.arg("doctor")
@@ -27,6 +32,8 @@ fn test_doctor() {
#[test]
fn test_config_list() {
require_ghidra!();
Command::cargo_bin("ghidra")
.unwrap()
.arg("config")
@@ -38,6 +45,8 @@ fn test_config_list() {
#[test]
fn test_config_get() {
require_ghidra!();
Command::cargo_bin("ghidra")
.unwrap()
.arg("config")
@@ -49,6 +58,8 @@ fn test_config_get() {
#[test]
fn test_config_set() {
require_ghidra!();
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.yaml");
@@ -65,6 +76,8 @@ fn test_config_set() {
#[test]
fn test_config_reset() {
require_ghidra!();
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.yaml");
@@ -76,3 +89,56 @@ fn test_config_reset() {
.assert()
.success();
}
#[test]
fn test_init() {
require_ghidra!();
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.yaml");
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_CONFIG", &config_path)
.arg("init")
.assert()
.success();
assert!(config_path.exists());
}
#[test]
fn test_set_default_program() {
require_ghidra!();
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.yaml");
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_CONFIG", &config_path)
.arg("set-default")
.arg("program")
.arg("sample_binary")
.assert()
.success()
.stdout(predicate::str::contains("Default program set"));
}
#[test]
fn test_set_default_project() {
require_ghidra!();
let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.yaml");
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_CONFIG", &config_path)
.arg("set-default")
.arg("project")
.arg("test-project")
.assert()
.success()
.stdout(predicate::str::contains("Default project set"));
}
+4 -4
View File
@@ -114,20 +114,20 @@ ensure_test_project("my-project", "sample_binary");
Handles "already exists" errors gracefully. Safe to call from multiple tests.
## skip_if_no_ghidra! Macro
## require_ghidra! Macro
Tests should call this macro to skip gracefully when Ghidra unavailable:
Tests should call this macro to assert Ghidra availability up front:
```rust
#[test]
fn test_something() {
skip_if_no_ghidra!();
require_ghidra!();
// Test code runs only if ghidra doctor succeeds
}
```
Runs `ghidra doctor` and returns early if fails. Prints message: "Skipping test: Ghidra not available"
Runs `ghidra doctor` and fails the test if Ghidra is unavailable, including doctor output.
## Design Decisions
+1
View File
@@ -170,6 +170,7 @@ impl GhidraCommand {
/// Configure for daemon connection.
pub fn with_daemon(self, harness: &DaemonTestHarness) -> Self {
self.env("GHIDRA_CLI_SOCKET", harness.socket_path().to_string_lossy())
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir().to_string_lossy())
}
/// Set project and program arguments.
+18 -7
View File
@@ -201,6 +201,11 @@ impl DaemonTestHarness {
&self.socket_path
}
/// Get data directory for this daemon instance.
pub fn data_dir(&self) -> &PathBuf {
&self.data_dir
}
/// Get project name.
pub fn project(&self) -> &str {
&self.project
@@ -247,14 +252,20 @@ fn get_unique_data_dir() -> PathBuf {
dir
}
/// Skip test if Ghidra is not available.
/// Require Ghidra to be available for tests to proceed.
#[macro_export]
macro_rules! skip_if_no_ghidra {
macro_rules! require_ghidra {
() => {
let doctor = assert_cmd::Command::cargo_bin("ghidra").unwrap().arg("doctor").output();
if doctor.is_err() || !doctor.unwrap().status.success() {
eprintln!("Skipping test: Ghidra not available");
return;
}
let doctor = assert_cmd::Command::cargo_bin("ghidra")
.unwrap()
.arg("doctor")
.output()
.expect("Failed to run `ghidra doctor`");
assert!(
doctor.status.success(),
"Ghidra is not available for tests.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&doctor.stdout),
String::from_utf8_lossy(&doctor.stderr)
);
};
}
+112
View File
@@ -14,6 +14,7 @@ const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_daemon_start() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -22,6 +23,7 @@ fn test_daemon_start() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
@@ -36,6 +38,7 @@ fn test_daemon_start() {
#[test]
#[serial]
fn test_daemon_status() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -44,6 +47,7 @@ fn test_daemon_status() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
@@ -59,6 +63,7 @@ fn test_daemon_status() {
#[test]
#[serial]
fn test_daemon_ping() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -67,6 +72,7 @@ fn test_daemon_ping() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("ping")
@@ -81,6 +87,7 @@ fn test_daemon_ping() {
#[test]
#[serial]
fn test_daemon_clear_cache() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -89,6 +96,7 @@ fn test_daemon_clear_cache() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("clear-cache")
@@ -103,6 +111,7 @@ fn test_daemon_clear_cache() {
#[test]
#[serial]
fn test_daemon_lifecycle() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
@@ -111,6 +120,7 @@ fn test_daemon_lifecycle() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
@@ -122,6 +132,7 @@ fn test_daemon_lifecycle() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("ping")
@@ -132,6 +143,7 @@ fn test_daemon_lifecycle() {
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("stop")
@@ -140,3 +152,103 @@ fn test_daemon_lifecycle() {
.assert()
.success();
}
#[test]
#[serial]
fn test_daemon_stop() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("stop")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("status")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success()
.stdout(predicate::str::contains("No daemon running"));
drop(harness);
}
#[test]
#[serial]
fn test_daemon_restart() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("restart")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("stop")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_daemon_start_when_running() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.env("GHIDRA_CLI_DATA_DIR", harness.data_dir())
.env("GHIDRA_CLI_SOCKET", harness.socket_path())
.arg("daemon")
.arg("start")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.failure()
.stderr(predicate::str::contains("already running"));
drop(harness);
}
+97
View File
@@ -4,6 +4,7 @@ use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
#[macro_use]
mod common;
/// Generate unique project name for test isolation.
@@ -14,6 +15,8 @@ fn unique_project_name(prefix: &str) -> String {
#[test]
fn test_project_create() {
require_ghidra!();
let project = unique_project_name("create");
Command::cargo_bin("ghidra")
@@ -37,6 +40,8 @@ fn test_project_create() {
#[test]
fn test_project_list() {
require_ghidra!();
Command::cargo_bin("ghidra")
.unwrap()
.arg("project")
@@ -47,6 +52,8 @@ fn test_project_list() {
#[test]
fn test_project_info() {
require_ghidra!();
let project = unique_project_name("info");
Command::cargo_bin("ghidra")
@@ -77,6 +84,8 @@ fn test_project_info() {
#[test]
fn test_project_lifecycle() {
require_ghidra!();
let project = unique_project_name("lifecycle");
Command::cargo_bin("ghidra")
@@ -107,6 +116,8 @@ fn test_project_lifecycle() {
#[test]
#[serial]
fn test_import_binary() {
require_ghidra!();
let project = unique_project_name("import");
let binary = common::fixture_binary();
@@ -135,6 +146,8 @@ fn test_import_binary() {
#[test]
#[serial]
fn test_analyze_program() {
require_ghidra!();
let project = unique_project_name("analyze");
let binary = common::fixture_binary();
@@ -169,3 +182,87 @@ fn test_analyze_program() {
.assert()
.success();
}
#[test]
fn test_project_delete_nonexistent() {
require_ghidra!();
let project = unique_project_name("missing");
Command::cargo_bin("ghidra")
.unwrap()
.arg("project")
.arg("delete")
.arg(&project)
.assert()
.success()
.stdout(predicate::str::contains("not found"));
}
#[test]
#[serial]
fn test_import_existing_program() {
require_ghidra!();
let project = unique_project_name("import-existing");
let binary = common::fixture_binary();
let mut cmd = Command::cargo_bin("ghidra").unwrap();
cmd.arg("import")
.arg(binary.to_str().unwrap())
.arg("--project")
.arg(&project)
.arg("--program")
.arg("sample_binary")
.timeout(std::time::Duration::from_secs(300))
.assert()
.success()
.stdout(predicate::str::contains("Successfully imported as"));
let mut cmd = Command::cargo_bin("ghidra").unwrap();
cmd.arg("import")
.arg(binary.to_str().unwrap())
.arg("--project")
.arg(&project)
.arg("--program")
.arg("sample_binary")
.timeout(std::time::Duration::from_secs(300))
.assert()
.success()
.stdout(predicate::str::contains("Successfully imported as"));
Command::cargo_bin("ghidra")
.unwrap()
.arg("project")
.arg("delete")
.arg(&project)
.assert()
.success();
}
#[test]
#[serial]
fn test_quick() {
require_ghidra!();
let project = unique_project_name("quick");
let binary = common::fixture_binary();
Command::cargo_bin("ghidra")
.unwrap()
.arg("quick")
.arg(binary.to_str().unwrap())
.arg("--project")
.arg(&project)
.timeout(std::time::Duration::from_secs(300))
.assert()
.success();
Command::cargo_bin("ghidra")
.unwrap()
.arg("project")
.arg("delete")
.arg(&project)
.assert()
.success();
}