Replace the current three-layer architecture (Rust CLI → Rust Daemon → Python bridge inside Ghidra JVM) with a two-layer architecture (Rust CLI → Java GhidraScript socket server inside Ghidra JVM). The Java GhidraScript runs via analyzeHeadless -postScript, starts a TCP socket server that keeps the JVM alive, and handles all 49 commands directly via the Ghidra Java API. The Rust side collapses: the persistent daemon process is eliminated, replaced by a thin "launcher" that starts analyzeHeadless if not running, writes a port+PID file, and then the CLI connects directly to the Java bridge's TCP socket.
Chosen approach: Single .java GhidraScript file (auto-compiled by analyzeHeadless), TCP localhost with dynamic port + port/PID file, collapsed single-layer IPC (no Rust daemon process), clean break migration.
| Decision | Reasoning Chain |
|---|---|
| GhidraScript .java over Extension .zip | Extension requires Gradle build infra + versioned distribution packaging → adds build complexity for no runtime benefit → GhidraScript is auto-compiled by analyzeHeadless at startup → matches current Python pattern where scripts are written to disk and referenced via -postScript → simpler maintenance |
| Collapse to single IPC layer | Current flow: CLI → UDS → Daemon → TCP:18700 → Python bridge. Daemon exists to: (a) manage bridge process lifecycle, (b) provide lock files, (c) lazy-start bridge. With Java bridge: (a) analyzeHeadless IS the daemon process, (b) lock files move to port/PID file, (c) CLI launcher starts analyzeHeadless if port file missing → Rust daemon process becomes pure passthrough with no logic → eliminate it |
| TCP localhost + port/PID file over Unix domain socket | Java 16+ supports UnixDomainSocketAddress but: Ghidra bundles its own JDK and not all Ghidra distributions include JDK 16+ → TCP ServerSocket(0) on localhost is universally supported → dynamic port avoids fixed-port conflicts → port written to ~/.local/share/ghidra-cli/bridge-{hash}.port → PID written to bridge-{hash}.pid for killing hung bridges → same security model (localhost only) |
| Single .java file with inner classes | Multiple .java files risk classpath issues with analyzeHeadless script compilation → single file with static inner classes keeps all handler code together → file is ~2000-3000 lines which is manageable → analyzeHeadless compiles single script files reliably |
| Clean break over dual-mode | Maintaining both Python and Java bridges doubles test surface → Python bridge is the source of complexity we're eliminating → feature branch with all E2E tests passing before merge provides safety → CHANGELOG documents breaking change |
| Sequential command processing (no threading) | Ghidra headless is single-threaded for program access → concurrent writes to Program cause data corruption → Python bridge was already single-connection sequential → Java bridge processes one command at a time on accept loop → analysis queue handles concurrent import requests by queuing |
| Embed .java in binary via include_str! | Current pattern: 13 Python scripts embedded via include_str!() in bridge.rs, written to ~/.config/ghidra-cli/scripts/ at runtime → same pattern for single Java file → no separate installation step beyond ghidra-cli binary itself |
| Port file as liveness indicator | fslock-based locking was complex and had edge cases → port file + PID file is simpler: if port file exists AND PID is alive AND TCP connect succeeds → bridge is running → if any check fails, clean up stale files and start fresh |
| 300s read timeout for analysis ops | Current bridge uses 300s read timeout (bridge.rs:240) → analysis operations (especially initial auto-analysis) can take minutes → keep same timeout → individual query commands complete in <1s but timeout provides safety net |
| Alternative | Why Rejected |
|---|---|
| Ghidra Extension (.zip) packaging | Requires Gradle build infrastructure, module.manifest, extension.properties → adds build system complexity for distribution → GhidraScript auto-compilation achieves same result with zero build tooling |
| Unix domain sockets in Java | Requires Java 16+ UnixDomainSocketAddress → Ghidra may bundle older JDK → TCP localhost is universally supported and equally secure for loopback-only binding |
| Keep Rust daemon as process manager | With Java bridge handling its own lifecycle, Rust daemon becomes a passthrough that adds latency and complexity → thin launcher function in CLI achieves same lifecycle management |
| ghidra_bridge (existing library) | External dependency on Jython RPC library → still has Python layer → less control over protocol and error handling → custom Java bridge is simpler and eliminates Python entirely |
| PyGhidra JPype embedding | Requires Python + JPype installation → adds Python dependency back → ghidra-cli's goal is minimal deps (just Ghidra + Java) |
| Dual mode with --bridge flag | Doubles test matrix → Python bridge is what we're removing → clean break is simpler to reason about |
| Fixed port per project (hash-based) | Port collisions between projects with similar hashes → dynamic port with port file is collision-free → slight complexity of reading port file is worth guaranteed uniqueness |
- Ghidra 10.0+: Minimum supported version (same as current).
AutoImporter,DecompInterface,FunctionManagerAPIs stable across 10.x-12.x. - Java 17+: Required by Ghidra itself.
ServerSocket,Gson(bundled by Ghidra) available. - analyzeHeadless: Compiles
.javaGhidraScript files automatically. Script receivescurrentProgram,state,monitoras instance fields. - Gson bundled: Ghidra includes
com.google.gsonin classpath. No external JSON library needed. - Single-threaded program access: Ghidra
Programobjects are not thread-safe for writes. All command handling is sequential (same as current Python bridge). - Existing E2E tests validate migration: If Java bridge produces identical JSON responses to Python bridge, all existing E2E test files pass unchanged. The Rust-side IPC protocol changes (direct TCP instead of UDS→TCP) but the test harness connects via CLI binary which handles this transparently.
GhidraScript.getScriptArgs(): Returns command-line arguments passed after script name. Used to pass port file path.
| Risk | Mitigation | Anchor |
|---|---|---|
| Ghidra API differences across versions (10.x vs 11.x vs 12.x) | AutoImporter.importByUsingBestGuess() signature changed in 12.x → Java bridge uses try/catch with reflection fallback for import, same pattern as Python bridge |
src/ghidra/scripts/bridge.py:880-888 uses 6-arg variant |
analyzeHeadless compiles .java with errors |
Java compilation errors surface in stderr → bridge.rs already captures stderr for diagnostics → keep this pattern → add specific "Java compilation failed" error detection | |
| TCP port exhaustion on systems with many projects | Dynamic port from ServerSocket(0) draws from ephemeral range (49152-65535) → 16K+ ports available → stale port files cleaned on next launch attempt |
|
| Ghidra JVM heap exhaustion with large binaries | Same risk as current Python bridge → not a new risk → Ghidra's default heap settings apply | |
analyzeHeadless calls System.exit() after script |
GhidraScript run() method blocks in accept loop → analyzeHeadless waits for script completion → System.exit() only called after run() returns (on shutdown command) |
BEFORE (3 layers):
ghidra-cli (Rust) --[UDS]--> daemon (Rust/tokio) --[TCP:18700]--> bridge.py (Python in Ghidra JVM)
AFTER (2 layers):
ghidra-cli (Rust) --[TCP:dynamic]--> GhidraCliBridge.java (Java in Ghidra JVM via analyzeHeadless)
Lifecycle:
1. CLI reads port file (~/.local/share/ghidra-cli/bridge-{hash}.port)
2. If missing or stale: launch analyzeHeadless + GhidraCliBridge.java
3. GhidraCliBridge writes port file, enters accept loop
4. CLI connects via TCP, sends JSON command, reads JSON response
5. On shutdown: GhidraCliBridge deletes port/PID files, run() returns, analyzeHeadless exits
CLI Command (e.g., `ghidra functions --limit 10`)
│
├─ Parse CLI args → Command enum
├─ Read port file → TCP port (or launch bridge if missing)
├─ Connect TCP localhost:port
├─ Send: {"command":"list_functions","args":{"limit":10}}\n
├─ Recv: {"status":"success","data":{"functions":[...],"count":N}}\n
├─ Format output (table/json/csv)
└─ Display
Bridge Lifecycle:
analyzeHeadless project_dir project_name -import binary -postScript GhidraCliBridge.java /path/to/portfile
│
├─ GhidraCliBridge.run() called by analyzeHeadless
├─ Opens ServerSocket(0), writes port to file, writes PID to file
├─ Prints ready signal to stdout
├─ Accept loop: read command JSON → dispatch to handler → write response JSON
├─ "shutdown" command → break accept loop
├─ Cleanup: delete port/PID files
└─ run() returns → analyzeHeadless exits normally
- Single .java file:
analyzeHeadlesscompiles GhidraScript files individually. Multi-file compilation requires extension packaging. Single file with inner classes avoids this while keeping code organized. - Port file + PID file: Replaces the fslock + info file + UDS socket triple. Simpler liveness detection: read PID, check
kill -0, verify TCP connect. - No Rust daemon process: The Java bridge IS the daemon.
analyzeHeadlessis the process manager. Rust CLI is a thin client that launches the bridge if needed.
- One bridge per project: Port file path includes project hash → only one bridge instance per project directory.
- Sequential command processing: Single accept loop, one connection at a time. Ghidra
Programobjects are not thread-safe for mutation. - Import queuing: If a program is being analyzed, import commands queue the new binary and return immediately with "queued" status. Analysis proceeds in order.
- Port file lifecycle: Created AFTER ServerSocket.bind() succeeds, deleted BEFORE run() returns. Stale files detected via PID liveness check.
- Protocol compatibility: JSON wire format
{"command":"...","args":{...}}→{"status":"success|error","data":{...},"message":"..."}is identical to current Python bridge.
- Single large .java file vs extension with multiple classes: Chose single file for zero-build-tooling simplicity at cost of a large source file (~2500 lines).
- TCP vs UDS: Chose TCP for Java cross-platform compatibility at cost of port file management overhead.
- Clean break vs gradual migration: Chose clean break for code simplicity at cost of no fallback path.
Files:
src/ghidra/scripts/GhidraCliBridge.java(NEW)
Flags: needs-rationale, complex-algorithm
Requirements:
- GhidraScript that extends
ghidra.app.script.GhidraScript run()method: parse script args for port file path, bindServerSocket(0)on localhost, write port + PID files, print ready signal to stdout, enter accept loop- Accept loop: read newline-delimited JSON commands from socket, dispatch to handler, write JSON response, handle client disconnect gracefully
- Shutdown command: break accept loop, delete port/PID files, return from
run() - Core command handlers (direct ports from bridge.py):
ping— health checkshutdown— stop serverprogram_info— program metadata (name, format, language, image base, address range)list_functions— enumerate functions with limit/filter supportdecompile— decompile function at address or by namelist_strings— enumerate string datalist_imports— enumerate external symbolslist_exports— enumerate export entry pointsmemory_map— enumerate memory blocks with permissionsxrefs_to/xrefs_from— cross-reference queriesimport— import binary viaAutoImporter.importByUsingBestGuess()analyze— triggerAutoAnalysisManagerre-analysislist_programs— enumerate project domain filesopen_program— switch active programprogram_close— close current programprogram_delete— delete program from projectprogram_export— export program info as JSON
- Address resolution helper: parse hex address or resolve function name
- JSON serialization via Gson (bundled by Ghidra)
- Error handling: all handlers return
{"status":"error","message":"..."}on failure currentProgramnull checks on all program-dependent handlers
Acceptance Criteria:
analyzeHeadless /tmp/test TestProject -import /bin/ls -scriptPath . -postScript GhidraCliBridge.java /tmp/test.portstarts server- Port file contains valid integer port
- PID file contains process PID
echo '{"command":"ping"}' | nc localhost $(cat /tmp/test.port)returns{"status":"success","data":{"message":"pong"}}echo '{"command":"list_functions","args":{"limit":5}}' | nc ...returns JSON with functions arrayecho '{"command":"shutdown"}' | nc ...causes clean exit, port+PID files deleted
Tests:
- Test files: Manual validation with
analyzeHeadless+nc/curlduring development; formal E2E tests run in Milestone 4 - Test type: Manual integration
- Backing: Bootstrap — Java bridge must work standalone before Rust integration
- Scenarios:
- Normal: start server, send commands, get correct JSON responses
- Edge: send malformed JSON, send unknown command, send command with no program loaded
- Error: binary not found on import, function not found on decompile
Code Intent:
- New file
src/ghidra/scripts/GhidraCliBridge.java: Single GhidraScript class extendingghidra.app.script.GhidraScript run()method: get port file path fromgetScriptArgs()[0], createServerSocket(0, 1, InetAddress.getByName("127.0.0.1")), write port to file, write PID to file viaProcessHandle.current().pid(), print ready signal---GHIDRA_CLI_START---/ JSON /---GHIDRA_CLI_END---to stdout, enter accept loophandleRequest(String line)method: parse JSON with Gson, extract "command" and "args", dispatch to handler method, return JSON response stringresolveAddress(String addrStr)helper: trycurrentProgram.getAddressFactory().getAddress(), fall back to function name lookup- Handler methods:
handlePing(),handleProgramInfo(),handleListFunctions(JsonObject args),handleDecompile(JsonObject args),handleListStrings(JsonObject args),handleListImports(),handleListExports(),handleMemoryMap(),handleXrefsTo(JsonObject args),handleXrefsFrom(JsonObject args),handleImport(JsonObject args),handleAnalyze(JsonObject args),handleListPrograms(),handleOpenProgram(JsonObject args),handleProgramClose(),handleProgramDelete(JsonObject args),handleProgramExport(JsonObject args) - Each handler follows same pattern as Python equivalent: null-check currentProgram, call Ghidra Java API, build Gson JsonObject response
Code Changes: To be filled by Developer
Files:
src/ghidra/scripts/GhidraCliBridge.java(MODIFY — add remaining handlers)
Flags: conformance
Requirements:
- Port all remaining Python command handlers to Java methods in GhidraCliBridge:
- Find:
find_string,find_bytes,find_function,find_calls,find_crypto,find_interesting(fromfind.py) - Symbols:
symbol_list,symbol_get,symbol_create,symbol_delete,symbol_rename(fromsymbols.py) - Types:
type_list,type_get,type_create,type_apply(fromtypes.py) - Comments:
comment_list,comment_get,comment_set,comment_delete(fromcomments.py) - Graph:
graph_calls,graph_callers,graph_callees,graph_export(fromgraph.py) - Diff:
diff_programs,diff_functions(fromdiff.py) - Patch:
patch_bytes,patch_nop,patch_export(frompatch.py) - Disasm:
disasm(fromdisasm.py) - Stats:
stats(fromstats.py) - Script:
script_run,script_python,script_java,script_list(fromscript_runner.py) - Batch:
batch(frombatch.py)
- Find:
- All handlers produce identical JSON output structure to their Python equivalents
- Transaction management: handlers that modify program state (symbol_create, comment_set, patch_bytes, etc.) must use
currentProgram.startTransaction()/endTransaction()
Acceptance Criteria:
- Each handler returns same JSON structure as corresponding Python handler
- Write operations (symbol create, comment set, patch) wrapped in transactions
find_bytescorrectly handles hex pattern search across memory blocksgraph_callers/graph_calleescorrectly traverse call graph to specified depthdecompiletimeout set to 30 seconds (matching Python:decompiler.decompileFunction(func, 30, monitor))
Tests:
- Test files: Manual validation during development; formal E2E tests in Milestone 4
- Test type: Manual integration
- Backing: Behavioral parity with Python bridge
- Scenarios:
- Normal: each handler returns expected data for sample binary
- Edge: symbol operations on non-existent symbols, patch at invalid address
- Error: find with empty pattern, graph on function with no calls
Code Intent:
- Add command dispatch entries to
handleRequest()for all new commands - Find handlers:
handleFindString(args)— iterate defined data matching pattern;handleFindBytes(args)— useMemory.findBytes()with hex pattern;handleFindFunction(args)— iterate functions matching name pattern;handleFindCalls(args)— get xrefs to named function;handleFindCrypto()— scan for known crypto constants (AES S-box, SHA constants);handleFindInteresting()— heuristic scan for security-relevant functions - Symbol handlers: use
currentProgram.getSymbolTable()API —getSymbols(),createLabel(),removeSymbolSpecial(),getSymbol() - Type handlers: use
currentProgram.getDataTypeManager()—getAllDataTypes(),getDataType(),addDataType(),apply()viaDataUtilities.createData() - Comment handlers: use
currentProgram.getListing().getCodeUnitAt()—getComment(),setComment()withCodeUnit.EOL_COMMENTetc. - Graph handlers: recursive traversal of
function.getCalledFunctions()/function.getCallingFunctions()with depth limit - Diff handlers: compare two programs' function lists by name/size/signature
- Patch handlers: use
currentProgram.getMemory().setBytes()within transaction; NOP uses language-specific NOP byte(s) - Disasm handler: use
currentProgram.getListing().getInstructionAt()and iterate - Stats handler: aggregate counts (functions, strings, imports, exports, memory blocks, defined data)
- Script handlers:
script_run— useGhidraScriptUtilto find and run scripts;script_list— enumerate script directories - All write operations wrapped in
int txId = currentProgram.startTransaction("description"); try { ... } finally { currentProgram.endTransaction(txId, true); }
Code Changes: To be filled by Developer
Files:
src/ghidra/bridge.rs(REWRITE — replace Python bridge management with Java bridge management)src/daemon/mod.rs(REWRITE — eliminate daemon process, replace with launcher logic)src/daemon/handler.rs(DELETE or REWRITE — direct TCP replaces IPC→bridge delegation)src/daemon/ipc_server.rs(DELETE — no more UDS IPC server)src/daemon/process.rs(REWRITE — replace fslock/info file with port/PID file management)src/daemon/state.rs(DELETE — no daemon state needed)src/daemon/cache.rs(DELETE or keep if result caching desired)src/daemon/queue.rs(DELETE — analysis queuing moves to Java side)src/daemon/handlers/*.rs(DELETE — all handler delegation removed)src/ipc/client.rs(REWRITE — connect via TCP to Java bridge instead of UDS to daemon)src/ipc/protocol.rs(MODIFY — simplify to match Java bridge JSON protocol)src/ipc/transport.rs(SIMPLIFY — TCP only, remove UDS/named pipe abstraction)src/ghidra/bridge.rs(REWRITE — replace Python bridge management with Java bridge TCP client)src/ghidra/scripts.rs(MODIFY — embed GhidraCliBridge.java instead of Python scripts)src/lib.rs(MODIFY — update module structure)src/main.rs(MODIFY — remove daemon foreground mode, update command routing)src/cli.rs(MODIFY — removedaemon start/stopsubcommands, simplify)Cargo.toml(MODIFY — removeinterprocess,fslockdeps; may removetokioif fully sync)
Flags: error-handling, needs-rationale
Requirements:
bridge.rsrewrite:ensure_bridge_running(project_path): check port file, verify PID alive, verify TCP connect. If any fail, start new bridge.start_bridge(project_path, mode): spawnanalyzeHeadlesswith-postScript GhidraCliBridge.java, wait for ready signal on stdout, verify port file createdsend_command(port, command, args): TCP connect, send JSON, read JSON response, disconnectkill_bridge(project_path): read PID file, send shutdown command (graceful), fall back to kill PID (forced)- Port/PID file management:
~/.local/share/ghidra-cli/bridge-{md5_hash}.port,bridge-{md5_hash}.pid
- CLI command flow:
ghidra import <binary>→ ensure_bridge_running → send "import" commandghidra functions→ ensure_bridge_running → send "list_functions" commandghidra daemon stop→ read PID/port → send "shutdown" command → verify exitghidra daemon status→ check port file + PID + TCP connect → report status
- Remove Python-specific code:
- Remove
find_headless_script()pyghidraRun preference logic - Remove
install_pyghidra()from setup - Remove all
include_str!("scripts/*.py")embeds
- Remove
- Add Java-specific code:
include_str!("scripts/GhidraCliBridge.java")for embedding- Write
.javafile to~/.config/ghidra-cli/scripts/on bridge start - Always use
analyzeHeadless(no pyghidraRun)
Acceptance Criteria:
ghidra import tests/fixtures/sample_binary --project teststarts bridge if needed, imports binary, returns successghidra functions --project test --program sample_binaryconnects to running bridge, returns function listghidra daemon statusreports bridge running/stoppedghidra daemon stopgracefully stops the Java bridge- No
tokioruntime needed if all I/O is synchronous TCP - Port file created on bridge start, deleted on bridge stop
- PID file allows
ghidra daemon stopto kill hung bridges
Tests:
- Test files:
tests/daemon_tests.rs,tests/command_tests.rs(existing, should pass) - Test type: E2E integration
- Backing: Existing test suite validates behavioral parity
- Scenarios:
- Normal: full command lifecycle (import → analyze → query → shutdown)
- Edge: bridge already running (reuse), bridge crashed (restart), stale port file (cleanup)
- Error: Ghidra not installed, binary not found, invalid project path
Code Intent:
- Rewrite
src/ghidra/bridge.rs:- Remove
GhidraBridgestruct withChild,TcpStream,AtomicBool - New functions:
ensure_bridge_running(project_path) -> Result<u16>(returns port),start_bridge(project_path, ghidra_dir, mode) -> Result<u16>,send_command(port, command, args) -> Result<Value>,stop_bridge(project_path) -> Result<()> start_bridge(): write embedded Java script to disk, buildanalyzeHeadlesscommand, spawn process, read stdout for ready signal, return port from port filesend_command():TcpStream::connect(("127.0.0.1", port)), write JSON line, read JSON line, parse response- Port file path:
get_data_dir()?.join(format!("bridge-{}.port", md5_hash(project_path))) - PID file path: same pattern with
.pidextension
- Remove
- Rewrite
src/daemon/mod.rs: removeDaemonState,DaemonConfig,run()async function. Replace withensure_bridge(project_path, ghidra_dir)that callsbridge::ensure_bridge_running() - Rewrite
src/daemon/process.rs: removeacquire_daemon_lock(),DaemonInfo. New functions:read_port_file(),write_port_file(),read_pid_file(),write_pid_file(),is_pid_alive(),cleanup_stale_files() - Delete:
src/daemon/ipc_server.rs,src/daemon/state.rs,src/daemon/queue.rs,src/daemon/handlers/directory - Rewrite
src/ipc/client.rs: removeDaemonClientwith async reader/writer. NewBridgeClientwith syncTcpStream - Simplify
src/ipc/transport.rs: remove UDS/named pipe abstractions, keep only TCP helper functions - Simplify
src/ipc/protocol.rs: protocol now matches Java bridge JSON format directly:{"command":"...", "args":{...}}→{"status":"...", "data":{...}, "message":"..."} - Modify
src/main.rs: remove--foregrounddaemon mode, update command dispatch to usebridge::ensure_bridge_running()+bridge::send_command() - Modify
src/cli.rs: keepdaemon start/stop/statusas convenience commands but implement via bridge management (not separate process) - Modify
Cargo.toml: removeinterprocess,fslockdependencies. Evaluate removingtokioif all bridge I/O is synchronous.
Code Changes: To be filled by Developer
Files:
src/ghidra/setup.rs(MODIFY — removeinstall_pyghidra(), simplify setup)src/main.rs(MODIFY — updatehandle_doctor(),handle_setup())src/ghidra/scripts/bridge.py(DELETE)src/ghidra/scripts/find.py(DELETE)src/ghidra/scripts/symbols.py(DELETE)src/ghidra/scripts/types.py(DELETE)src/ghidra/scripts/comments.py(DELETE)src/ghidra/scripts/graph.py(DELETE)src/ghidra/scripts/diff.py(DELETE)src/ghidra/scripts/patch.py(DELETE)src/ghidra/scripts/disasm.py(DELETE)src/ghidra/scripts/stats.py(DELETE)src/ghidra/scripts/script_runner.py(DELETE)src/ghidra/scripts/batch.py(DELETE)src/ghidra/scripts/program.py(DELETE)src/ghidra/scripts.rs(MODIFY — remove Python script embeds, add Java script embed)
Flags: needs-rationale
Requirements:
ghidra setup:- Check Java 17+ (keep existing
check_java_requirement()) - Download + extract Ghidra (keep existing
install_ghidra()) - Remove PyGhidra installation step entirely
- Verify
analyzeHeadlessscript exists and is executable - Write GhidraCliBridge.java to scripts directory (verify Java compilation by doing a dry-run compile if possible)
- Check Java 17+ (keep existing
ghidra doctor:- Check Java version (keep)
- Check Ghidra installation (keep)
- Remove PyGhidra check
- Add: verify GhidraCliBridge.java can be found/written
- Add: verify no stale port/PID files
- Delete all 13 Python scripts from
src/ghidra/scripts/ - Update
src/ghidra/scripts.rsto embed onlyGhidraCliBridge.java
Acceptance Criteria:
ghidra setupinstalls Ghidra without any Python/PyGhidra stepsghidra doctorreports Java, Ghidra, and bridge script status (no Python checks)- All Python
.pyfiles removed from source tree cargo buildsucceeds with no references to deleted Python files
Tests:
- Test files:
tests/command_tests.rs(existing doctor/setup tests) - Test type: E2E integration
- Backing: Existing tests validate doctor/setup commands
- Scenarios:
- Normal: setup with valid Ghidra, doctor reports all green
- Edge: Ghidra not installed (doctor reports error), stale files present (doctor warns)
- Error: Java not installed, wrong Java version
Code Intent:
- Modify
src/ghidra/setup.rs: deleteinstall_pyghidra()function entirely (lines 244-345). Remove all references to Python venv, pip, PyGhidra wheel. - Modify
src/main.rshandle_setup(): remove PyGhidra installation call. Add step to verifyanalyzeHeadlessexists in Ghidra install. - Modify
src/main.rshandle_doctor(): remove PyGhidra version check. Add bridge script presence check. Add stale port/PID file detection. - Modify
src/ghidra/bridge.rs: replace allinclude_str!("scripts/*.py")embeds (lines ~391-403) with singleinclude_str!("scripts/GhidraCliBridge.java"). Update script writing logic to write only the Java file. - Modify
src/ghidra/scripts.rsif it references Python scripts: update or remove as needed. - Delete all 13
.pyfiles fromsrc/ghidra/scripts/
Code Changes: To be filled by Developer
Files:
tests/common/mod.rs(MODIFY — updateDaemonTestHarnessfor new bridge architecture)tests/common/helpers.rs(MODIFY — update helper functions if needed)tests/daemon_tests.rs(MODIFY — adapt daemon lifecycle tests)tests/e2e.rs(MODIFY — verify smoke tests pass)tests/batch_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/command_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/comment_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/diff_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/disasm_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/find_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/graph_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/output_format_integration.rs(MODIFY — if DaemonTestHarness interface changes)tests/patch_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/program_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/query_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/script_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/stats_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/symbol_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/type_tests.rs(MODIFY — if DaemonTestHarness interface changes)tests/unimplemented_tests.rs(MODIFY — if DaemonTestHarness interface changes).github/workflows/test.yml(MODIFY — remove Python/PyGhidra CI setup if present)
Requirements:
- Update
DaemonTestHarnessto work with new bridge architecture:- Instead of starting a Rust daemon process, start
analyzeHeadlesswith Java bridge - Or: use
ghidra importwhich now auto-starts the bridge - Socket path environment variable replaced with port file path
- Instead of starting a Rust daemon process, start
- All existing E2E test files must pass
- CI workflow removes any Python setup steps
Acceptance Criteria:
cargo testpasses all existing tests (with Ghidra installed)cargo test --test daemon_testsvalidates bridge start/stop/restartcargo test --test query_testsvalidates all data query commandscargo test --test command_testsvalidates doctor/setup/version- CI workflow runs without Python dependencies
Tests:
- Test files: All existing test files in
tests/ - Test type: E2E integration
- Backing: Existing test suite is the validation gate
- Scenarios:
- Full regression: every existing test passes
- New: bridge restart recovery, stale port file cleanup
Code Intent:
- Modify
tests/common/mod.rsDaemonTestHarness:new(): instead of spawningghidra daemon start --foreground, useghidra importto start bridge, or spawnanalyzeHeadlessdirectly- Replace
socket_pathfield withportfield (read from port file) - Update
GHIDRA_CLI_SOCKETenv var toGHIDRA_CLI_PORTor equivalent Dropimpl: send shutdown command via TCP, verify process exit, cleanup port/PID files
- Verify
tests/common/helpers.rsghidra()builder works with new bridge connection - Update
tests/daemon_tests.rs: adapt tests that reference daemon-specific concepts (daemon start/stop → bridge start/stop) - Update
.github/workflows/test.yml: remove anypip install pyghidraor Python venv setup steps
Code Changes: To be filled by Developer
Delegated to: @agent-technical-writer (mode: post-implementation)
Source: ## Invisible Knowledge section of this plan
Files:
CLAUDE.md(MODIFY — update navigation index for new architecture)AGENTS.md(MODIFY — update architecture description)src/daemon/README.md(REWRITE — document new bridge-based architecture)CHANGELOG.md(MODIFY — document breaking change)
Requirements:
- CLAUDE.md: update file references (remove Python script references, add Java bridge)
- AGENTS.md: update architecture section to reflect single-layer IPC
- src/daemon/README.md: document new bridge lifecycle, port/PID file management, command protocol
- CHANGELOG.md: document breaking change — Python bridge removed, Java bridge replaces it, setup no longer installs PyGhidra
Acceptance Criteria:
- CLAUDE.md is tabular index only
- src/daemon/README.md describes new architecture with ASCII diagram
- CHANGELOG.md has breaking change entry
- No references to Python bridge in documentation
M1 (Core Java Bridge) ──→ M2 (Extended Handlers) ──→ M3 (Rust Rewrite) ──→ M4 (Setup + Python Removal) ──→ M5 (E2E Tests)
│
v
M6 (Docs)
- M1 and M2 are sequential (M2 extends M1's file)
- M3 depends on M1+M2 (Rust side needs Java bridge to exist)
- M4 depends on M3 (can't delete Python until Rust no longer references it)
- M5 depends on M3+M4 (tests validate the complete migration)
- M6 depends on M5 (document after validation)
Parallelization note: M1 and early M3 exploration can overlap — Rust-side design can be planned while Java handlers are being written. But M3 implementation depends on M1+M2 being complete for integration testing.