Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions src/ipc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -131,12 +132,28 @@ fn connect_with_retry(addr: &std::net::SocketAddr) -> Result<TcpStream> {
/// 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.
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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...");
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
};
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down