mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
ripping out rpc and stuff
This commit is contained in:
Generated
-10
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user