From 6941a881922d79300110c5ad5424d211ee40f31d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 10 Sep 2026 17:38:23 +0200 Subject: [PATCH] ipc: auto-restart bridge on crash (no retry) When the Java bridge crashes mid-command (EOF on socket), clients created with BridgeClient::new_with_restart() will: 1. Detect the crash 2. Call ensure_bridge_running() to bring the JVM back up 3. Return an error telling the user to re-run their command This eliminates the manual 'ghidra restart' step after a crash. The command is NOT retried automatically (a crash during a large import will crash again), but the bridge is ready for the next invocation. --- src/ipc/client.rs | 60 ++++++++++++++++++++++++++++++++++++++++++----- src/main.rs | 10 ++++---- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 54d9204c..3b92c84d 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -5,6 +5,7 @@ use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; +use std::path::PathBuf; use std::time::Duration; use anyhow::Result; @@ -131,12 +132,28 @@ fn connect_with_retry(addr: &std::net::SocketAddr) -> Result { /// Client for communicating with the Ghidra Java bridge. pub struct BridgeClient { port: u16, + /// Context needed to restart the bridge if it crashes. + restart_ctx: Option<(PathBuf, PathBuf)>, } impl BridgeClient { /// Create a client for a known port. pub fn new(port: u16) -> Self { - Self { port } + Self { port, restart_ctx: None } + } + + /// Create a client with crash-recovery context. On bridge crash, the client + /// will attempt to restart the bridge (via `ensure_bridge_running`) so the + /// next command works without manual `ghidra restart`. + pub fn new_with_restart( + port: u16, + project_path: &std::path::Path, + ghidra_install_dir: &std::path::Path, + ) -> Self { + Self { + port, + restart_ctx: Some((project_path.to_path_buf(), ghidra_install_dir.to_path_buf())), + } } /// Get the port this client connects to. @@ -193,11 +210,42 @@ impl BridgeClient { let mut response_line = String::new(); match reader.read_line(&mut response_line) { // EOF before any response: bridge closed the socket without replying. - Ok(0) => anyhow::bail!( - "Bridge closed the connection without responding to '{}' \ - (it may have crashed or been restarted). Retry, or check `ghidra status`.", - command - ), + Ok(0) => { + // If we have restart context, try to bring the bridge back up so + // the user's next command works without manual `ghidra restart`. + match &self.restart_ctx { + Some((project_path, ghidra_dir)) => { + eprintln!( + "Bridge crashed during '{}'. Restarting bridge...", + command + ); + match crate::ghidra::bridge::ensure_bridge_running( + project_path, + ghidra_dir, + crate::ghidra::bridge::BridgeStartMode::Project, + ) { + Ok(new_port) => { + eprintln!( + "Bridge restarted on port {}. Re-run your command.", + new_port + ); + } + Err(restart_err) => { + eprintln!( + "Bridge restart failed: {}. Run `ghidra restart` manually.", + restart_err + ); + } + } + } + None => {} + } + return Err(anyhow::anyhow!( + "Bridge closed the connection without responding to '{}' \ + (it may have crashed). The bridge has been restarted; re-run your command.", + command + )); + } Ok(_) => {} // A read timeout here means the bridge is up (we connected) but hasn't // reached our queued request in time — almost always because it is busy diff --git a/src/main.rs b/src/main.rs index fa4fe81b..49f0911c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -535,7 +535,7 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { // is_bridge_running() already proved the bridge process is alive // and its socket is accepting; a busy bridge just queues this // request, so there is no pre-flight ping gate to fail here. - let client = BridgeClient::new(port); + let client = BridgeClient::new_with_restart(port, &project_path, &ghidra_install_dir); if !cli.quiet { eprintln!("Importing into running bridge..."); } @@ -558,7 +558,7 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { &ghidra_install_dir, BridgeStartMode::Project, )?; - let client = BridgeClient::new(port); + let client = BridgeClient::new_with_restart(port, &project_path, &ghidra_install_dir); let result = client.import_binary(&args.binary, args.program.as_deref())?; let name = args.program.clone().unwrap_or_else(|| { result @@ -592,7 +592,7 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { program_name: name.clone(), }, )?; - let client = BridgeClient::new(port); + let client = BridgeClient::new_with_restart(port, &project_path, &ghidra_install_dir); client.open_program(&name)?; (client, name) }; @@ -633,7 +633,7 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { // Liveness already proven by is_bridge_running() (PID alive + socket // accepting). A busy bridge queues the request rather than failing a // pre-flight ping, so connect directly and let it wait its turn. - BridgeClient::new(port) + BridgeClient::new_with_restart(port, &project_path, &ghidra_install_dir) } else { // Auto-start bridge - use specific program if available, otherwise project mode let mode = if let Some(program) = extract_program_from_command(&cli.command) @@ -654,7 +654,7 @@ fn run_with_bridge(cli: Cli) -> anyhow::Result<()> { if !cli.quiet { eprintln!("Bridge ready."); } - BridgeClient::new(port) + BridgeClient::new_with_restart(port, &project_path, &ghidra_install_dir) }; // Switch to requested program if it differs from the bridge's current program