refactor: Remove HeadlessExecutor in favor of daemon-only architecture

HeadlessExecutor spawned a new Ghidra process per command, which was
slow. The daemon maintains a persistent connection and is now the only
way to execute queries.

Changes:
- Delete src/ghidra/headless.rs entirely
- Remove dead handler functions that used HeadlessExecutor:
  - handle_query, handle_function_command, handle_decompile,
  - handle_decompile_impl, handle_strings_command,
  - handle_memory_command, handle_dump_command, handle_summary
- Update handle_quick to inform user about daemon requirement
- Query::execute replaced with Query::process_results for
  post-processing of daemon results

All query commands (function list, decompile, strings, memory, etc.)
now require the daemon to be running. The CLI prints clear instructions
when daemon is not available.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alexander Kiselev
2026-01-25 04:53:30 -08:00
co-authored by Claude Opus 4.5
parent 3db0af7a3c
commit 0470caf015
4 changed files with 30 additions and 560 deletions
-195
View File
@@ -1,195 +0,0 @@
use std::path::Path;
use std::process::Command;
use serde_json::Value as JsonValue;
use crate::error::{GhidraError, Result};
use super::GhidraClient;
use super::scripts;
#[deprecated(
since = "0.2.0",
note = "Use daemon for query operations. HeadlessExecutor spawns a new Ghidra process per command, which is slow. The daemon maintains a persistent connection."
)]
pub struct HeadlessExecutor<'a> {
client: &'a GhidraClient,
}
impl<'a> HeadlessExecutor<'a> {
pub fn new(client: &'a GhidraClient) -> Self {
Self { client }
}
pub fn execute_script(
&self,
project_name: &str,
program_name: &str,
script_content: &str,
script_name: &str,
args: &[String],
) -> Result<JsonValue> {
// Save script to disk
let scripts_dir = self.get_scripts_dir()?;
let script_path = scripts::save_script(script_name, script_content, &scripts_dir)?;
// Execute script
let output = self.run_ghidra_script(project_name, program_name, &script_path, args)?;
// Parse JSON output
let json: JsonValue = serde_json::from_str(&output)
.map_err(|e| GhidraError::ExecutionFailed(format!("Failed to parse script output: {}", e)))?;
Ok(json)
}
fn run_ghidra_script(
&self,
project_name: &str,
program_name: &str,
script_path: &Path,
args: &[String],
) -> Result<String> {
let project_path = self.client.get_project_path(project_name);
let headless = self.client.get_headless_script();
let mut cmd = Command::new(&headless);
cmd.arg(project_path.to_str().unwrap())
.arg(project_name)
.arg("-process")
.arg(program_name)
.arg("-noanalysis")
.arg("-scriptPath")
.arg(script_path.parent().unwrap().to_str().unwrap())
.arg("-postScript")
.arg(script_path.file_name().unwrap().to_str().unwrap());
for arg in args {
cmd.arg(arg);
}
// Capture output
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(GhidraError::ExecutionFailed(format!("Script execution failed: {}", stderr)));
}
// Extract JSON from output (Ghidra adds some logging we need to skip)
let stdout = String::from_utf8_lossy(&output.stdout);
let json_output = self.extract_json_from_output(&stdout)?;
Ok(json_output)
}
fn extract_json_from_output(&self, output: &str) -> Result<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> {
let config_dir = dirs::config_dir()
.ok_or_else(|| GhidraError::ConfigError("Could not determine config directory".to_string()))?;
let scripts_dir = config_dir.join("ghidra-cli").join("scripts");
if !scripts_dir.exists() {
std::fs::create_dir_all(&scripts_dir)?;
}
Ok(scripts_dir)
}
pub fn list_functions(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_functions_script(),
"list_functions",
&[],
)
}
pub fn decompile_function(&self, project_name: &str, program_name: &str, address: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_decompile_function_script(),
"decompile_function",
&[address.to_string()],
)
}
pub fn list_strings(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_strings_script(),
"list_strings",
&[],
)
}
pub fn list_imports(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_imports_script(),
"list_imports",
&[],
)
}
pub fn list_exports(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_list_exports_script(),
"list_exports",
&[],
)
}
pub fn get_memory_map(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_memory_map_script(),
"memory_map",
&[],
)
}
pub fn get_program_info(&self, project_name: &str, program_name: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_program_info_script(),
"program_info",
&[],
)
}
pub fn get_xrefs_to(&self, project_name: &str, program_name: &str, address: &str) -> Result<JsonValue> {
self.execute_script(
project_name,
program_name,
scripts::get_xrefs_to_script(),
"xrefs_to",
&[address.to_string()],
)
}
}
-1
View File
@@ -1,7 +1,6 @@
#![allow(dead_code)]
pub mod bridge;
pub mod headless;
pub mod data;
pub mod scripts;
pub mod setup;
+21 -332
View File
@@ -9,15 +9,13 @@ mod ipc;
mod query;
use clap::Parser;
use cli::{Cli, Commands, DaemonCommands, QueryArgs, QueryOptions, SetupArgs};
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 format::OutputFormat;
use ghidra::GhidraClient;
use query::{Query, DataType, FieldSelector, SortKey};
use std::path::PathBuf;
use tracing::info;
@@ -52,22 +50,27 @@ async fn main() {
fn run(cli: Cli) -> anyhow::Result<()> {
match cli.command {
Commands::Query(args) => handle_query(args),
// Non-daemon commands
Commands::Init => handle_init(),
Commands::Doctor => handle_doctor(),
Commands::Version => handle_version(),
Commands::Import(args) => handle_import(args),
Commands::Analyze(args) => handle_analyze(args),
Commands::Summary(args) => handle_summary(args.options),
Commands::Quick(args) => handle_quick(args),
Commands::Function(cmd) => handle_function_command(cmd),
Commands::Strings(cmd) => handle_strings_command(cmd),
Commands::Memory(cmd) => handle_memory_command(cmd),
Commands::Dump(cmd) => handle_dump_command(cmd),
Commands::Decompile(args) => handle_decompile(args),
Commands::Config(cmd) => handle_config_command(cmd),
Commands::SetDefault(args) => handle_set_default(args),
Commands::Project(args) => handle_project_command(args.command),
// Commands requiring daemon are handled by run_with_daemon_check
Commands::Query(_)
| Commands::Summary(_)
| Commands::Function(_)
| Commands::Strings(_)
| Commands::Memory(_)
| Commands::Dump(_)
| Commands::Decompile(_)
| Commands::XRef(_) => {
unreachable!("Daemon-required commands should go through run_with_daemon_check")
}
_ => {
println!("Command not yet implemented");
Ok(())
@@ -538,280 +541,6 @@ fn daemonize_windows(daemon_config: DaemonConfig, port: Option<u16>) -> anyhow::
Ok(())
}
fn handle_query(args: QueryArgs) -> anyhow::Result<()> {
let config = Config::load()?;
let client = GhidraClient::new(config.clone())?;
// Resolve program and project
let program = resolve_program(&args.program, &config)?;
let project = resolve_project(&args.project, &config, &program)?;
// Parse data type
let data_type = DataType::from_str(&args.data_type)?;
// Build query
let mut query = Query::new(data_type);
// Add filter if provided
if let Some(filter_str) = args.filter {
let filter = filter::Filter::parse(&filter_str)?;
query = query.with_filter(filter);
}
// Add format
let format = if args.json {
OutputFormat::Json
} else if let Some(fmt) = args.format {
OutputFormat::from_str(&fmt)?
} else {
// Auto-detect based on TTY
format::auto_detect_format(atty::is(atty::Stream::Stdout))
};
query = query.with_format(format);
// Add limit
if let Some(limit) = args.limit {
query = query.with_limit(limit);
}
// Add offset
if let Some(offset) = args.offset {
query = query.with_offset(offset);
}
// Add sort
if let Some(sort_str) = args.sort {
let sort_keys = SortKey::parse(&sort_str);
query.sort = Some(sort_keys);
}
// Add field selection
if let Some(fields_str) = args.fields {
let selector = FieldSelector::parse(&fields_str)?;
query.fields = Some(selector);
}
// Count only?
if args.count {
query = query.count_only();
}
// Execute query
let result = query.execute(&client, &project, &program)?;
println!("{}", result);
Ok(())
}
fn handle_function_command(cmd: cli::FunctionCommands) -> anyhow::Result<()> {
use cli::FunctionCommands;
match cmd {
FunctionCommands::List(opts) => {
let args = QueryArgs {
data_type: "functions".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
FunctionCommands::Decompile(args) => {
// Decompile specific function
let query_args = QueryArgs {
data_type: "functions".to_string(),
program: args.options.program,
project: args.options.project,
filter: Some(format!("address={} OR name={}", args.target, args.target)),
fields: args.options.fields,
format: args.options.format,
limit: Some(1),
offset: None,
sort: None,
count: false,
json: args.options.json,
};
handle_decompile_impl(args.target, query_args)
}
_ => {
println!("Function subcommand not yet implemented");
Ok(())
}
}
}
fn handle_decompile(args: cli::DecompileArgs) -> anyhow::Result<()> {
let query_args = QueryArgs {
data_type: "functions".to_string(),
program: args.options.program,
project: args.options.project,
filter: None,
fields: None,
format: Some("c".to_string()),
limit: None,
offset: None,
sort: None,
count: false,
json: false,
};
handle_decompile_impl(args.target, query_args)
}
fn handle_decompile_impl(target: String, args: QueryArgs) -> anyhow::Result<()> {
let config = Config::load()?;
let client = GhidraClient::new(config.clone())?;
let program = resolve_program(&args.program, &config)?;
let project = resolve_project(&args.project, &config, &program)?;
// Use headless executor to decompile
let executor = ghidra::headless::HeadlessExecutor::new(&client);
let result = executor.decompile_function(&project, &program, &target)?;
// Format output
if let Some(code) = result.get("code") {
if let Some(code_str) = code.as_str() {
println!("{}", code_str);
return Ok(());
}
}
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
fn handle_strings_command(cmd: cli::StringsCommands) -> anyhow::Result<()> {
use cli::StringsCommands;
match cmd {
StringsCommands::List(opts) => {
let args = QueryArgs {
data_type: "strings".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
_ => {
println!("Strings subcommand not yet implemented");
Ok(())
}
}
}
fn handle_memory_command(cmd: cli::MemoryCommands) -> anyhow::Result<()> {
use cli::MemoryCommands;
match cmd {
MemoryCommands::Map(opts) => {
let args = QueryArgs {
data_type: "memory".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
_ => {
println!("Memory subcommand not yet implemented");
Ok(())
}
}
}
fn handle_dump_command(cmd: cli::DumpCommands) -> anyhow::Result<()> {
use cli::DumpCommands;
match cmd {
DumpCommands::Imports(opts) => {
let args = QueryArgs {
data_type: "imports".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
DumpCommands::Exports(opts) => {
let args = QueryArgs {
data_type: "exports".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
DumpCommands::Functions(opts) => {
let args = QueryArgs {
data_type: "functions".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
DumpCommands::Strings(opts) => {
let args = QueryArgs {
data_type: "strings".to_string(),
program: opts.program,
project: opts.project,
filter: opts.filter,
fields: opts.fields,
format: opts.format,
limit: opts.limit,
offset: opts.offset,
sort: opts.sort,
count: opts.count,
json: opts.json,
};
handle_query(args)
}
}
}
fn handle_init() -> anyhow::Result<()> {
println!("Ghidra CLI Initialization");
println!("========================\n");
@@ -958,39 +687,6 @@ fn handle_analyze(args: cli::AnalyzeArgs) -> anyhow::Result<()> {
Ok(())
}
fn handle_summary(opts: QueryOptions) -> anyhow::Result<()> {
let config = Config::load()?;
let client = GhidraClient::new(config.clone())?;
let program = resolve_program(&opts.program, &config)?;
let project = resolve_project(&opts.project, &config, &program)?;
println!("Program Summary");
println!("===============\n");
// Get program info
let executor = ghidra::headless::HeadlessExecutor::new(&client);
let info = executor.get_program_info(&project, &program)?;
if let Some(name) = info.get("name") {
println!("Name: {}", name);
}
if let Some(format) = info.get("executable_format") {
println!("Format: {}", format);
}
if let Some(lang) = info.get("language") {
println!("Language: {}", lang);
}
if let Some(count) = info.get("function_count") {
println!("Functions: {}", count);
}
if let Some(count) = info.get("instruction_count") {
println!("Instructions: {}", count);
}
Ok(())
}
fn handle_quick(args: cli::QuickArgs) -> anyhow::Result<()> {
let config = Config::load()?;
let client = GhidraClient::new(config.clone())?;
@@ -1008,21 +704,14 @@ fn handle_quick(args: cli::QuickArgs) -> anyhow::Result<()> {
println!("[2/3] Running analysis...");
client.analyze_program(&project, &program_name)?;
// Summary
println!("[3/3] Generating summary...\n");
let opts = QueryOptions {
program: Some(program_name),
project: Some(project),
filter: None,
fields: None,
format: None,
limit: None,
offset: None,
sort: None,
count: false,
json: false,
};
handle_summary(opts)?;
// Done
println!("[3/3] Done!\n");
println!("Analysis complete. To query the binary, start the daemon:");
println!(" ghidra daemon start --project {} --program {}", project, program_name);
println!("\nThen run queries like:");
println!(" ghidra function list");
println!(" ghidra decompile main");
println!(" ghidra summary");
Ok(())
}
+9 -32
View File
@@ -1,9 +1,9 @@
#![allow(dead_code)]
use serde_json::Value as JsonValue;
use crate::error::{GhidraError, Result};
use crate::filter::Filter;
use crate::format::{OutputFormat, Formatter, DefaultFormatter};
use crate::ghidra::GhidraClient;
use crate::ghidra::headless::HeadlessExecutor;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DataType {
@@ -97,39 +97,16 @@ impl Query {
self
}
pub fn execute(&self, client: &GhidraClient, project: &str, program: &str) -> Result<String> {
let executor = HeadlessExecutor::new(client);
// Fetch data based on type
let data = match self.data_type {
DataType::Functions => executor.list_functions(project, program)?,
DataType::Strings => executor.list_strings(project, program)?,
DataType::Imports => executor.list_imports(project, program)?,
DataType::Exports => executor.list_exports(project, program)?,
DataType::Memory => executor.get_memory_map(project, program)?,
_ => {
return Err(GhidraError::Other(format!(
"Data type {:?} not yet implemented",
self.data_type
)));
}
};
// Data should be an array
let data_array = match data {
JsonValue::Array(arr) => arr,
_ => {
return Err(GhidraError::ExecutionFailed(
"Expected array from Ghidra script".to_string()
));
}
};
/// Process query results from pre-fetched data.
///
/// Note: Data fetching is now handled by the daemon via IPC.
/// This method only handles filtering, field selection, sorting, and formatting.
pub fn process_results(&self, data: Vec<JsonValue>) -> Result<String> {
// Apply filter
let filtered = if let Some(filter) = &self.filter {
self.apply_filter(&data_array, filter)?
self.apply_filter(&data, filter)?
} else {
data_array
data
};
// Apply field selection