From 83949683ae352a31d2a5a273e68b18dbfba39ba2 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Fri, 6 Mar 2026 14:40:23 -0700 Subject: [PATCH] Add /health endpoint to Java plugin and update health checks 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 /. --- docker/Dockerfile | 2 +- pyproject.toml | 2 +- .../eu/starsong/ghidra/MCGhidraPlugin.java | 33 ++++++++++++++++++ src/mcghidra/mixins/docker.py | 34 ++++++++++++++++--- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2b69ef0..37eb289 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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"] diff --git a/pyproject.toml b/pyproject.toml index e50f55c..3dd893f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/main/java/eu/starsong/ghidra/MCGhidraPlugin.java b/src/main/java/eu/starsong/ghidra/MCGhidraPlugin.java index 5eceda8..19b191c 100644 --- a/src/main/java/eu/starsong/ghidra/MCGhidraPlugin.java +++ b/src/main/java/eu/starsong/ghidra/MCGhidraPlugin.java @@ -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 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") diff --git a/src/mcghidra/mixins/docker.py b/src/mcghidra/mixins/docker.py index f21e310..d6c01ad 100644 --- a/src/mcghidra/mixins/docker.py +++ b/src/mcghidra/mixins/docker.py @@ -889,19 +889,25 @@ 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": data.get("api_version"), - "program": data.get("program"), - "file": data.get("file"), + "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, @@ -916,6 +922,24 @@ class DockerMixin(MCGhidraMixinBase): "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( name="docker_health", description="Check if a MCGhidra container's API is responding",