fix: 修复docker模式下沙盒启动时端口分配问题
This commit is contained in:
parent
590001c130
commit
fe3d5b7f33
|
|
@ -105,7 +105,10 @@ class LocalContainerBackend(SandboxBackend):
|
||||||
RuntimeError: If the container fails to start.
|
RuntimeError: If the container fails to start.
|
||||||
"""
|
"""
|
||||||
container_name = f"{self._container_prefix}-{sandbox_id}"
|
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:
|
try:
|
||||||
container_id = self._start_container(container_name, port, extra_mounts)
|
container_id = self._start_container(container_name, port, extra_mounts)
|
||||||
self._ensure_user_data_permissions(container_name)
|
self._ensure_user_data_permissions(container_name)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
"""Thread-safe network utilities."""
|
"""Thread-safe network utilities."""
|
||||||
|
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
@ -32,7 +34,33 @@ class PortAllocator:
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._reserved_ports: set[int] = set()
|
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.
|
"""Check if a port is available for binding.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -44,6 +72,9 @@ class PortAllocator:
|
||||||
if port in self._reserved_ports:
|
if port in self._reserved_ports:
|
||||||
return False
|
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:
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||||
try:
|
try:
|
||||||
s.bind(("localhost", port))
|
s.bind(("localhost", port))
|
||||||
|
|
@ -51,7 +82,7 @@ class PortAllocator:
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
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.
|
"""Allocate an available port in a thread-safe manner.
|
||||||
|
|
||||||
This method is thread-safe. It finds an available port, marks it as reserved,
|
This method is thread-safe. It finds an available port, marks it as reserved,
|
||||||
|
|
@ -69,7 +100,7 @@ class PortAllocator:
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for port in range(start_port, start_port + max_range):
|
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)
|
self._reserved_ports.add(port)
|
||||||
return port
|
return port
|
||||||
|
|
||||||
|
|
@ -85,7 +116,7 @@ class PortAllocator:
|
||||||
self._reserved_ports.discard(port)
|
self._reserved_ports.discard(port)
|
||||||
|
|
||||||
@contextmanager
|
@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.
|
"""Context manager for port allocation with automatic release.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -95,7 +126,7 @@ class PortAllocator:
|
||||||
Yields:
|
Yields:
|
||||||
An available port number.
|
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:
|
try:
|
||||||
yield port
|
yield port
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -106,7 +137,7 @@ class PortAllocator:
|
||||||
_global_port_allocator = 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.
|
"""Get a free port in a thread-safe manner.
|
||||||
|
|
||||||
This function uses a global port allocator to ensure that concurrent calls
|
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:
|
Raises:
|
||||||
RuntimeError: If no available port is found in the specified range.
|
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:
|
def release_port(port: int) -> None:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue