fix: 修复docker模式下沙盒启动时端口分配问题

This commit is contained in:
Titan 2026-03-17 15:39:00 +08:00
parent 590001c130
commit fe3d5b7f33
2 changed files with 46 additions and 8 deletions

View File

@ -105,7 +105,10 @@ class LocalContainerBackend(SandboxBackend):
RuntimeError: If the container fails to start.
"""
container_name = f"{self._container_prefix}-{sandbox_id}"
port = get_free_port(start_port=self._base_port)
# Check host-side Docker published ports as well, because this code may
# run inside gateway/langgraph containers while creating sibling sandbox
# containers via host Docker socket.
port = get_free_port(start_port=self._base_port, check_docker_host_ports=True)
try:
container_id = self._start_container(container_name, port, extra_mounts)
self._ensure_user_data_permissions(container_name)

View File

@ -1,6 +1,8 @@
"""Thread-safe network utilities."""
import re
import socket
import subprocess
import threading
from contextlib import contextmanager
@ -32,7 +34,33 @@ class PortAllocator:
self._lock = threading.Lock()
self._reserved_ports: set[int] = set()
def _is_port_available(self, port: int) -> bool:
@staticmethod
def _is_docker_host_port_in_use(port: int) -> bool:
"""Check whether a host port is already published by Docker containers.
This is useful when running inside a container (e.g. gateway/langgraph)
while creating sibling containers through the host Docker daemon.
"""
try:
result = subprocess.run(
["docker", "ps", "--format", "{{.Ports}}"],
capture_output=True,
text=True,
check=True,
timeout=5,
)
except Exception:
# Fail-open to preserve previous behavior when docker CLI is unavailable.
return False
pattern = re.compile(r":(\d+)->")
for line in result.stdout.splitlines():
for match in pattern.finditer(line):
if int(match.group(1)) == port:
return True
return False
def _is_port_available(self, port: int, *, check_docker_host_ports: bool = False) -> bool:
"""Check if a port is available for binding.
Args:
@ -44,6 +72,9 @@ class PortAllocator:
if port in self._reserved_ports:
return False
if check_docker_host_ports and self._is_docker_host_port_in_use(port):
return False
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("localhost", port))
@ -51,7 +82,7 @@ class PortAllocator:
except OSError:
return False
def allocate(self, start_port: int = 8080, max_range: int = 100) -> int:
def allocate(self, start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False) -> int:
"""Allocate an available port in a thread-safe manner.
This method is thread-safe. It finds an available port, marks it as reserved,
@ -69,7 +100,7 @@ class PortAllocator:
"""
with self._lock:
for port in range(start_port, start_port + max_range):
if self._is_port_available(port):
if self._is_port_available(port, check_docker_host_ports=check_docker_host_ports):
self._reserved_ports.add(port)
return port
@ -85,7 +116,7 @@ class PortAllocator:
self._reserved_ports.discard(port)
@contextmanager
def allocate_context(self, start_port: int = 8080, max_range: int = 100):
def allocate_context(self, start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False):
"""Context manager for port allocation with automatic release.
Args:
@ -95,7 +126,7 @@ class PortAllocator:
Yields:
An available port number.
"""
port = self.allocate(start_port, max_range)
port = self.allocate(start_port, max_range, check_docker_host_ports=check_docker_host_ports)
try:
yield port
finally:
@ -106,7 +137,7 @@ class PortAllocator:
_global_port_allocator = PortAllocator()
def get_free_port(start_port: int = 8080, max_range: int = 100) -> int:
def get_free_port(start_port: int = 8080, max_range: int = 100, *, check_docker_host_ports: bool = False) -> int:
"""Get a free port in a thread-safe manner.
This function uses a global port allocator to ensure that concurrent calls
@ -123,7 +154,11 @@ def get_free_port(start_port: int = 8080, max_range: int = 100) -> int:
Raises:
RuntimeError: If no available port is found in the specified range.
"""
return _global_port_allocator.allocate(start_port, max_range)
return _global_port_allocator.allocate(
start_port,
max_range,
check_docker_host_ports=check_docker_host_ports,
)
def release_port(port: int) -> None: