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
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ 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"] ENTRYPOINT ["/entrypoint.sh"]

View File

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

View File

@ -52,6 +52,7 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
private HttpServer server; private HttpServer server;
private int port; private int port;
private boolean isBaseInstance = false; private boolean isBaseInstance = false;
private long serverStartTimeMs;
/** /**
* Constructor for MCGhidra Plugin. * 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) // Register Root Endpoint (should be last to include links to all other endpoints)
registerRootEndpoint(server); registerRootEndpoint(server);
serverStartTimeMs = System.currentTimeMillis();
new Thread(() -> { new Thread(() -> {
server.start(); server.start();
Msg.info(this, "MCGhidra HTTP server started on port " + port); 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 // Info endpoint
server.createContext("/info", exchange -> { server.createContext("/info", exchange -> {
try { try {
@ -359,6 +391,7 @@ public class MCGhidraPlugin extends Plugin implements ApplicationLevelPlugin {
.success(true) .success(true)
.result(rootData) .result(rootData)
.addLink("self", "/") .addLink("self", "/")
.addLink("health", "/health")
.addLink("info", "/info") .addLink("info", "/info")
.addLink("plugin-version", "/plugin-version") .addLink("plugin-version", "/plugin-version")
.addLink("projects", "/projects") .addLink("projects", "/projects")

View File

@ -889,19 +889,25 @@ class DockerMixin(MCGhidraMixinBase):
import urllib.error import urllib.error
import urllib.request import urllib.request
url = f"http://localhost:{port}/" health_url = f"http://localhost:{port}/health"
root_url = f"http://localhost:{port}/"
try: 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: with urllib.request.urlopen(req, timeout=timeout) as response:
data = json_module.loads(response.read().decode()) data = json_module.loads(response.read().decode())
result = data.get("result", data)
return { return {
"healthy": True, "healthy": True,
"port": port, "port": port,
"api_version": data.get("api_version"), "api_version": result.get("api_version"),
"program": data.get("program"), "program": result.get("program"),
"file": data.get("file"), "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: except urllib.error.URLError as e:
return { return {
"healthy": False, "healthy": False,
@ -916,6 +922,24 @@ class DockerMixin(MCGhidraMixinBase):
"error": str(e), "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 {
"healthy": True,
"port": port,
"api_version": data.get("api_version"),
"program": data.get("program"),
}
except Exception as e:
return {
"healthy": False,
"port": port,
"error": str(e),
}
@mcp_tool( @mcp_tool(
name="docker_health", name="docker_health",
description="Check if a MCGhidra container's API is responding", description="Check if a MCGhidra container's API is responding",