mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
fix: add port file fallback for bridge ready detection on Windows
On Windows, stdout piping through analyzeHeadless.bat -> cmd.exe -> java.exe can fail due to buffering, causing start_bridge() to block forever waiting for the JSON ready signal that never arrives. Move stdout reading to a background thread and poll both the stdout channel AND port file + TCP connect in parallel. The Java bridge writes the port file before sending the stdout signal, so port file polling is a reliable fallback that unblocks Windows bridge startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
78c7c83a8e
commit
24763e8328
+74
-19
@@ -251,44 +251,91 @@ pub fn start_bridge(
|
||||
stderr_output
|
||||
});
|
||||
|
||||
// Wait for ready signal from stdout
|
||||
// Wait for bridge to become ready.
|
||||
//
|
||||
// Two mechanisms run in parallel:
|
||||
// 1. Stdout reader thread - watches for the JSON ready signal (fast path)
|
||||
// 2. Port file poller - polls port file + TCP ping as fallback
|
||||
//
|
||||
// On Windows, stdout piping through analyzeHeadless.bat → cmd.exe → java.exe
|
||||
// can fail due to buffering, so the port file fallback is essential.
|
||||
let stdout = child.stdout.take().expect("stdout should be piped");
|
||||
let reader = BufReader::new(stdout);
|
||||
let (stdout_tx, stdout_rx) = std::sync::mpsc::channel();
|
||||
let stdout_handle = std::thread::spawn(move || {
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut last_error = String::new();
|
||||
let mut stdout_lines = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = match line {
|
||||
Ok(l) => l,
|
||||
Err(_) => break,
|
||||
};
|
||||
info!("[Ghidra stdout] {}", line);
|
||||
stdout_lines.push(line.clone());
|
||||
|
||||
if line.contains("ERROR") || line.contains("Exception") || line.contains("SEVERE") {
|
||||
last_error = line.clone();
|
||||
}
|
||||
|
||||
if line.contains("---GHIDRA_CLI_START---") {
|
||||
continue;
|
||||
}
|
||||
if line.contains("\"status\"") && line.contains("\"ready\"") {
|
||||
info!("Bridge is ready (stdout signal)");
|
||||
let _ = stdout_tx.send(true);
|
||||
return (true, last_error, stdout_lines);
|
||||
}
|
||||
if line.contains("---GHIDRA_CLI_END---") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = stdout_tx.send(false);
|
||||
(false, last_error, stdout_lines)
|
||||
});
|
||||
|
||||
// Poll for readiness: check stdout channel and port file + TCP connect
|
||||
let ready_timeout = Duration::from_secs(120);
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut ready = false;
|
||||
let mut last_error = String::new();
|
||||
let mut stdout_lines = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
info!("[Ghidra stdout] {}", line);
|
||||
stdout_lines.push(line.clone());
|
||||
let mut ready_via_port_file = false;
|
||||
|
||||
if line.contains("ERROR") || line.contains("Exception") || line.contains("SEVERE") {
|
||||
last_error = line.clone();
|
||||
}
|
||||
|
||||
if line.contains("---GHIDRA_CLI_START---") {
|
||||
continue;
|
||||
}
|
||||
if line.contains("\"status\"") && line.contains("\"ready\"") {
|
||||
info!("Bridge is ready");
|
||||
while start_time.elapsed() < ready_timeout {
|
||||
// Check if stdout thread got the ready signal
|
||||
if let Ok(true) = stdout_rx.try_recv() {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
if line.contains("---GHIDRA_CLI_END---") && ready {
|
||||
|
||||
// Check if the process has exited (error case)
|
||||
if let Ok(Some(_)) = child.try_wait() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Fallback: check if port file exists and bridge responds to TCP
|
||||
if let Ok(Some(port)) = read_port_file(project_path) {
|
||||
let addr: std::net::SocketAddr =
|
||||
format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
if TcpStream::connect_timeout(&addr, Duration::from_secs(5)).is_ok() {
|
||||
info!("Bridge is ready (port file fallback on port {})", port);
|
||||
ready = true;
|
||||
ready_via_port_file = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
|
||||
if !ready {
|
||||
// Prevent orphaned JVM process: if child is still running but didn't
|
||||
// send the ready signal, kill it and clean up stale files
|
||||
// become ready, kill it and clean up stale files
|
||||
if let Ok(None) = child.try_wait() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
cleanup_stale_files(project_path).ok();
|
||||
|
||||
let (_, last_error, stdout_lines) = stdout_handle.join().unwrap_or_default();
|
||||
let stderr_output = stderr_handle.join().unwrap_or_default();
|
||||
let detail = if !last_error.is_empty() {
|
||||
format!(": {}", last_error)
|
||||
@@ -312,6 +359,14 @@ pub fn start_bridge(
|
||||
}
|
||||
}
|
||||
|
||||
// If we discovered readiness via port file, the stdout thread may still be
|
||||
// blocking. That's fine - it will terminate when the bridge eventually exits
|
||||
// and closes stdout. We intentionally don't join it here.
|
||||
if !ready_via_port_file {
|
||||
// Stdout thread delivered the signal, safe to join
|
||||
drop(stdout_handle);
|
||||
}
|
||||
|
||||
// Read port from port file
|
||||
let port = read_port_file(project_path)?
|
||||
.ok_or_else(|| anyhow::anyhow!("Port file not created by bridge"))?;
|
||||
|
||||
Reference in New Issue
Block a user