mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
CI/CD fixes
This commit is contained in:
@@ -123,8 +123,6 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
# Windows Ghidra tests may exceed timeout on GH Actions free runners
|
|
||||||
continue-on-error: ${{ matrix.os == 'windows-latest' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -196,7 +194,6 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
continue-on-error: ${{ matrix.os == 'windows-latest' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -268,7 +265,6 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
continue-on-error: ${{ matrix.os == 'windows-latest' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@@ -405,6 +405,32 @@ pub fn stop_bridge(project_path: &Path) -> Result<()> {
|
|||||||
.args(["/PID", &pid.to_string(), "/F", "/T"])
|
.args(["/PID", &pid.to_string(), "/F", "/T"])
|
||||||
.output();
|
.output();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for the process to actually die after SIGTERM/taskkill.
|
||||||
|
// Without this, the JVM may still hold the project lock when the
|
||||||
|
// next bridge tries to start (causes intermittent CI failures).
|
||||||
|
for _ in 0..100 {
|
||||||
|
if !is_pid_alive(pid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: SIGKILL if SIGTERM wasn't enough
|
||||||
|
#[cfg(unix)]
|
||||||
|
if is_pid_alive(pid) {
|
||||||
|
warn!("SIGKILL bridge process {} (SIGTERM didn't work)", pid);
|
||||||
|
unsafe {
|
||||||
|
libc::kill(pid as i32, libc::SIGKILL);
|
||||||
|
}
|
||||||
|
// Brief wait for SIGKILL to take effect
|
||||||
|
for _ in 0..20 {
|
||||||
|
if !is_pid_alive(pid) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1113,6 +1113,9 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
JsonArray results = new JsonArray();
|
JsonArray results = new JsonArray();
|
||||||
|
|
||||||
|
// Phase 1: Search pre-analyzed string data types from the listing.
|
||||||
|
// This is fast and returns strings Ghidra's analyzer has already classified.
|
||||||
Listing listing = currentProgram.getListing();
|
Listing listing = currentProgram.getListing();
|
||||||
DataIterator dataIter = listing.getDefinedData(true);
|
DataIterator dataIter = listing.getDefinedData(true);
|
||||||
|
|
||||||
@@ -1132,6 +1135,32 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: If listing search found nothing and we have a pattern,
|
||||||
|
// fall back to raw memory scanning. This catches strings that Ghidra's
|
||||||
|
// analyzer didn't classify as string data types (common on PE binaries).
|
||||||
|
if (results.size() == 0 && !pattern.isEmpty()) {
|
||||||
|
Memory memory = currentProgram.getMemory();
|
||||||
|
byte[] searchBytes = pattern.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
Address addr = memory.getMinAddress();
|
||||||
|
while (addr != null && results.size() < 100) {
|
||||||
|
Address found = memory.findBytes(addr, searchBytes, null, true, monitor);
|
||||||
|
if (found == null) break;
|
||||||
|
|
||||||
|
// Try to extract the full null-terminated string at this address
|
||||||
|
String extracted = extractStringAt(memory, found, 4096);
|
||||||
|
if (extracted != null && !extracted.isEmpty()) {
|
||||||
|
JsonObject item = new JsonObject();
|
||||||
|
item.addProperty("address", found.toString());
|
||||||
|
item.addProperty("value", extracted);
|
||||||
|
item.addProperty("length", extracted.length());
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = found.add(Math.max(1, extracted != null ? extracted.length() : 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JsonObject result = new JsonObject();
|
JsonObject result = new JsonObject();
|
||||||
result.add("results", results);
|
result.add("results", results);
|
||||||
result.addProperty("count", results.size());
|
result.addProperty("count", results.size());
|
||||||
@@ -1141,6 +1170,25 @@ public class GhidraCliBridge extends GhidraScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a printable string starting at the given address.
|
||||||
|
* Reads until a null byte, non-printable character, or maxLen is reached.
|
||||||
|
*/
|
||||||
|
private String extractStringAt(Memory memory, Address addr, int maxLen) {
|
||||||
|
try {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < maxLen; i++) {
|
||||||
|
byte b = memory.getByte(addr.add(i));
|
||||||
|
if (b == 0) break;
|
||||||
|
if (b < 0x20 || b > 0x7e) break; // non-printable ASCII
|
||||||
|
sb.append((char) b);
|
||||||
|
}
|
||||||
|
return sb.length() > 0 ? sb.toString() : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private JsonObject handleFindBytes(JsonObject args) {
|
private JsonObject handleFindBytes(JsonObject args) {
|
||||||
if (currentProgram == null) return errorResult("No program loaded");
|
if (currentProgram == null) return errorResult("No program loaded");
|
||||||
|
|
||||||
|
|||||||
+28
-11
@@ -133,6 +133,7 @@ pub fn ensure_test_project(project: &str, program: &str) {
|
|||||||
/// Tests connect to it via TCP using BridgeClient.
|
/// Tests connect to it via TCP using BridgeClient.
|
||||||
pub struct DaemonTestHarness {
|
pub struct DaemonTestHarness {
|
||||||
port: u16,
|
port: u16,
|
||||||
|
pid: Option<u32>,
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
project: String,
|
project: String,
|
||||||
project_path: PathBuf,
|
project_path: PathBuf,
|
||||||
@@ -166,8 +167,14 @@ impl DaemonTestHarness {
|
|||||||
// Read port from port file
|
// Read port from port file
|
||||||
let port = Self::wait_for_port(&project_path, Duration::from_secs(120))?;
|
let port = Self::wait_for_port(&project_path, Duration::from_secs(120))?;
|
||||||
|
|
||||||
|
// Store PID now so Drop can wait for it even if restart deletes the PID file
|
||||||
|
let pid = ghidra_cli::ghidra::bridge::read_pid_file(&project_path)
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
port,
|
port,
|
||||||
|
pid,
|
||||||
data_dir,
|
data_dir,
|
||||||
project: project.to_string(),
|
project: project.to_string(),
|
||||||
project_path,
|
project_path,
|
||||||
@@ -234,25 +241,35 @@ impl DaemonTestHarness {
|
|||||||
|
|
||||||
impl Drop for DaemonTestHarness {
|
impl Drop for DaemonTestHarness {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// Read PID BEFORE shutdown (Java deletes PID file during shutdown)
|
// Read current PID from file (may differ from self.pid if restart changed it)
|
||||||
let pid = ghidra_cli::ghidra::bridge::read_pid_file(&self.project_path)
|
let file_pid = ghidra_cli::ghidra::bridge::read_pid_file(&self.project_path)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
// Use stop_bridge for proper graceful shutdown + force-kill
|
// Use stop_bridge for proper graceful shutdown + force-kill
|
||||||
let _ = ghidra_cli::ghidra::bridge::stop_bridge(&self.project_path);
|
let _ = ghidra_cli::ghidra::bridge::stop_bridge(&self.project_path);
|
||||||
|
|
||||||
// Wait for process to fully exit and release project lock.
|
// Collect all PIDs we need to wait for (original + current, deduplicated)
|
||||||
// Critical on Windows where JVM cleanup is slow.
|
let mut pids_to_wait: Vec<u32> = Vec::new();
|
||||||
if let Some(pid) = pid {
|
if let Some(pid) = file_pid {
|
||||||
let max_wait = if cfg!(windows) {
|
pids_to_wait.push(pid);
|
||||||
Duration::from_secs(30)
|
}
|
||||||
} else {
|
if let Some(pid) = self.pid {
|
||||||
Duration::from_secs(10)
|
if !pids_to_wait.contains(&pid) {
|
||||||
};
|
pids_to_wait.push(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for ALL known processes to fully exit and release project lock.
|
||||||
|
let max_wait = if cfg!(windows) {
|
||||||
|
Duration::from_secs(30)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(15)
|
||||||
|
};
|
||||||
|
for pid in &pids_to_wait {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
while start.elapsed() < max_wait {
|
while start.elapsed() < max_wait {
|
||||||
if !ghidra_cli::ghidra::bridge::is_pid_alive(pid) {
|
if !ghidra_cli::ghidra::bridge::is_pid_alive(*pid) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(500));
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
|
|||||||
@@ -1267,7 +1267,9 @@ fn test_batch_invalid_file() {
|
|||||||
|
|
||||||
result.assert_failure();
|
result.assert_failure();
|
||||||
assert!(
|
assert!(
|
||||||
result.stderr.contains("not found") || result.stderr.contains("No such file"),
|
result.stderr.contains("not found")
|
||||||
|
|| result.stderr.contains("No such file")
|
||||||
|
|| result.stderr.contains("cannot find"),
|
||||||
"Should contain file-not-found error. Got: {}",
|
"Should contain file-not-found error. Got: {}",
|
||||||
result.stderr
|
result.stderr
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user