Add /health endpoint to Java plugin and update health checks
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run

New GET /health endpoint returns status, uptime, api_version, and
loaded program without depending on program state. Lightweight
enough for Docker HEALTHCHECK and monitoring probes.

Python docker_health tool tries /health first, falls back to root
endpoint for older plugin versions. Docker HEALTHCHECK updated to
use /health instead of /.
This commit is contained in:
Ryan Malloy 2026-03-06 14:40:23 -07:00
parent 14b2b575c8
commit 83949683ae
4 changed files with 64 additions and 7 deletions

View File

@ -147,6 +147,6 @@ ENV MCGHIDRA_MAXMEM=2G
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:${MCGHIDRA_PORT}/ || exit 1
CMD curl -f http://localhost:${MCGHIDRA_PORT}/health || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -1,6 +1,6 @@
[project]
name = "mcghidra"
version = "2026.3.2.1"
version = "2026.3.6"
description = "Reverse engineering bridge: multi-instance Ghidra plugin with HATEOAS REST API and MCP server for decompilation, analysis & binary manipulation"
readme = "README.md"
requires-python = ">=3.11"

View File

@ -52,6 +52,7 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
private HttpServer server;
private int port;
private boolean isBaseInstance = false;
private long serverStartTimeMs;
/**
* Constructor for MCGhidra Plugin.
@ -109,6 +110,8 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
// Register Root Endpoint (should be last to include links to all other endpoints)
registerRootEndpoint(server);
serverStartTimeMs = System.currentTimeMillis();
new Thread(() -> {
server.start();
Msg.info(this, "MCGhidra HTTP server started on port " + port);
@ -184,6 +187,35 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
}
});
// Health endpoint lightweight, no program dependency
server.createContext("/health", exchange -> {
try {
if ("GET".equals(exchange.getRequestMethod())) {
long uptimeMs = System.currentTimeMillis() - serverStartTimeMs;
Program program = getCurrentProgram();
Map<String, Object> healthData = new HashMap<>();
healthData.put("status", "up");
healthData.put("port", port);
healthData.put("api_version", ApiConstants.API_VERSION);
healthData.put("uptime_ms", uptimeMs);
healthData.put("program", program != null ? program.getName() : null);
ResponseBuilder builder = new ResponseBuilder(exchange, port)
.success(true)
.result(healthData)
.addLink("self", "/health")
.addLink("root", "/");
HttpUtil.sendJsonResponse(exchange, builder.build(), 200, port);
} else {
HttpUtil.sendErrorResponse(exchange, 405, "Method Not Allowed", "METHOD_NOT_ALLOWED", port);
}
} catch (IOException e) {
Msg.error(this, "Error handling /health", e);
}
});
// Info endpoint
server.createContext("/info", exchange -> {
try {
@ -359,6 +391,7 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
.success(true)
.result(rootData)
.addLink("self", "/")
.addLink("health", "/health")
.addLink("info", "/info")
.addLink("plugin-version", "/plugin-version")
.addLink("projects", "/projects")

View File

@ -889,10 +889,42 @@ class DockerMixin(MCGhidraMixinBase):
import urllib.error
import urllib.request
url = f"http://localhost:{port}/"
health_url = f"http://localhost:{port}/health"
root_url = f"http://localhost:{port}/"
try:
req = urllib.request.Request(url)
# Try /health first (available in plugin v2.2+)
req = urllib.request.Request(health_url)
with urllib.request.urlopen(req, timeout=timeout) as response:
data = json_module.loads(response.read().decode())
result = data.get("result", data)
return {
"healthy": True,
"port": port,
"api_version": result.get("api_version"),
"program": result.get("program"),
"uptime_ms": result.get("uptime_ms"),
}
except urllib.error.HTTPError:
# /health not available — fall back to root endpoint (older plugin)
pass
except urllib.error.URLError as e:
return {
"healthy": False,
"port": port,
"error": str(e.reason),
"message": "Container may still be starting or analyzing binary",
}
except Exception as e:
return {
"healthy": False,
"port": port,
"error": str(e),
}
# Fallback: try root endpoint for older plugin versions
try:
req = urllib.request.Request(root_url)
with urllib.request.urlopen(req, timeout=timeout) as response:
data = json_module.loads(response.read().decode())
return {
@ -900,14 +932,6 @@ class DockerMixin(MCGhidraMixinBase):
"port": port,
"api_version": data.get("api_version"),
"program": data.get("program"),
"file": data.get("file"),
}
except urllib.error.URLError as e:
return {
"healthy": False,
"port": port,
"error": str(e.reason),
"message": "Container may still be starting or analyzing binary",
}
except Exception as e:
return {