diff --git a/docker/Dockerfile b/docker/Dockerfile index bfee5c2..2b69ef0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -25,8 +25,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # Download and extract Ghidra WORKDIR /opt -RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \ - -o ghidra.zip \ +# Download with retries and resume support for unreliable connections +RUN for i in 1 2 3 4 5; do \ + curl -fSL --http1.1 -C - \ + "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \ + -o ghidra.zip && break || sleep 30; \ + done \ && unzip -q ghidra.zip \ && rm ghidra.zip \ && mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra @@ -89,8 +93,12 @@ RUN groupadd -g 1001 ghidra && useradd -u 1001 -g ghidra -m -s /bin/bash ghidra # Download and extract Ghidra (in runtime stage for cleaner image) WORKDIR /opt -RUN curl -fsSL "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \ - -o ghidra.zip \ +# Download with retries and resume support for unreliable connections +RUN for i in 1 2 3 4 5; do \ + curl -fSL --http1.1 -C - \ + "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_DATE}.zip" \ + -o ghidra.zip && break || sleep 30; \ + done \ && unzip -q ghidra.zip \ && rm ghidra.zip \ && mv ghidra_${GHIDRA_VERSION}_PUBLIC ghidra \ diff --git a/pyproject.toml b/pyproject.toml index db8bad7..4f06f42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mcghidra" -version = "2025.12.3" +version = "2026.2.11" description = "AI-assisted reverse engineering bridge: a multi-instance Ghidra plugin exposed via a HATEOAS REST API plus an MCP Python bridge for decompilation, analysis & binary manipulation" readme = "README.md" requires-python = ">=3.11" @@ -22,7 +22,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/mcghidra"] +packages = ["mcghidra"] [tool.hatch.build] sources = ["src"] diff --git a/src/mcghidra/mixins/docker.py b/src/mcghidra/mixins/docker.py index df7d940..54cdec6 100644 --- a/src/mcghidra/mixins/docker.py +++ b/src/mcghidra/mixins/docker.py @@ -581,14 +581,6 @@ class DockerMixin(MCGhidraMixinBase): if not binary_file.exists(): return {"error": f"Binary not found: {binary_path}"} - # Always allocate from pool to prevent conflicts between sessions - port = self.port_pool.allocate(self.session_id) - if port is None: - return { - "error": "Port pool exhausted (8192-8223). Stop some containers first.", - "allocated_ports": self.port_pool.get_allocated_ports(), - } - # Generate container name if not specified if name is None: name = self._generate_container_name(binary_file.name) @@ -602,19 +594,38 @@ class DockerMixin(MCGhidraMixinBase): ["ps", "-a", "-q", "-f", f"name=^{name}$"], check=False ) if check_result.stdout.strip(): - self.port_pool.release(port) return { "error": f"Container '{name}' already exists. Stop it first with docker_stop." } - # Check if port is already in use by a non-pool container - port_check = await self._run_docker_cmd( - ["ps", "-q", "-f", f"publish={port}"], check=False - ) - if port_check.stdout.strip(): - self.port_pool.release(port) + # Allocate a port that's both lockable AND not in use by Docker + # This handles external containers (not managed by MCGhidra) using ports in our range + port = None + ports_tried = [] + for _ in range(PORT_POOL_END - PORT_POOL_START + 1): + candidate_port = self.port_pool.allocate(self.session_id) + if candidate_port is None: + break # Pool exhausted + + # Check if this port is already in use by a Docker container + port_check = await self._run_docker_cmd( + ["ps", "-q", "-f", f"publish={candidate_port}"], check=False + ) + if port_check.stdout.strip(): + # Port is in use by Docker - release and try next + ports_tried.append(candidate_port) + self.port_pool.release(candidate_port) + continue + + # Found a usable port! + port = candidate_port + break + + if port is None: return { - "error": f"Port {port} is already in use by another container" + "error": "Port pool exhausted (8192-8223). All ports are in use by Docker containers.", + "ports_checked": ports_tried if ports_tried else "all ports locked by other MCGhidra sessions", + "allocated_ports": self.port_pool.get_allocated_ports(), } # Build label arguments diff --git a/uv.lock b/uv.lock index 447a9a3..f2d2a32 100644 --- a/uv.lock +++ b/uv.lock @@ -572,7 +572,7 @@ wheels = [ [[package]] name = "mcghidra" -version = "2025.12.3" +version = "2026.2.11" source = { editable = "." } dependencies = [ { name = "fastmcp" },