mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-28 11:25:35 +08:00
fix(plugin): honor display_plugin_debug_url port for managed Runtime (#2782)
Stdio and Windows launches always bound the SDK default debug port 5401, so changing plugin.display_plugin_debug_url only updated the console URL and a second Core instance hit Address already in use. Pass the configured debug (and Windows control) ports into the Runtime CLI, and document the behavior. Fixes #2713 Co-authored-by: Li-john1021 <[email protected]>
This commit is contained in:
co-authored by
Li-john1021
parent
12debe2747
commit
bd4e408d7e
@@ -111,7 +111,7 @@ You need to first deploy and start LangBot, ensuring that Plugin Runtime is runn
|
||||
|
||||
<Info title="Runtime has two startup modes:">
|
||||
|
||||
- `Stdio` mode: When you start LangBot using source code without the startup parameter `--standalone-runtime`, LangBot will automatically start Plugin Runtime as a subprocess and communicate with Plugin Runtime through `stdio` (standard input/output streams). In this case, Runtime will load plugins from the `data/plugins` directory in the LangBot root directory and listen on port `5401` of the LangBot host as a debug port.
|
||||
- `Stdio` mode: When you start LangBot using source code without the startup parameter `--standalone-runtime`, LangBot will automatically start Plugin Runtime as a subprocess and communicate with Plugin Runtime through `stdio` (standard input/output streams). In this case, Runtime will load plugins from the `data/plugins` directory in the LangBot root directory and listen on the LangBot host debug port (default `5401`, overridable via the port in `plugin.display_plugin_debug_url`).
|
||||
|
||||
- `WebSocket` mode:
|
||||
- Production environment: When you start LangBot using the official `docker-compose.yaml`, Plugin Runtime will run in a separate container, and LangBot will communicate with Plugin Runtime in WebSocket mode due to the startup parameter `--standalone-runtime`. The 5401 port of the Runtime container will be mapped to the 5401 port of the host as default.
|
||||
|
||||
@@ -111,7 +111,7 @@ cd HelloPlugin
|
||||
|
||||
<Info title="Runtime 有两种启动方式:">
|
||||
|
||||
- `Stdio` 模式:当您使用源代码启动 LangBot 并未携带启动参数`--standalone-runtime`时,LangBot 会自动以子进程的形式启动 Plugin Runtime,并通过 `stdio`(标准输入输出流)模式与 Plugin Runtime 通信。此时 Runtime 会加载 LangBot 根目录的`data/plugins`目录下的插件。并监听 LangBot 运行主机的`5401`端口作为调试端口。
|
||||
- `Stdio` 模式:当您使用源代码启动 LangBot 并未携带启动参数`--standalone-runtime`时,LangBot 会自动以子进程的形式启动 Plugin Runtime,并通过 `stdio`(标准输入输出流)模式与 Plugin Runtime 通信。此时 Runtime 会加载 LangBot 根目录的`data/plugins`目录下的插件。并监听 LangBot 运行主机上的调试端口(默认`5401`,可由配置项`plugin.display_plugin_debug_url`中的端口覆盖)。
|
||||
|
||||
- `WebSocket` 模式:
|
||||
- 生产环境:当您使用官方提供的`docker-compose.yaml`启动 LangBot 时,会在独立的容器运行 Plugin Runtime,LangBot 也会因为携带启动参数`--standalone-runtime`而以 WebSocket 模式与 Plugin Runtime 通信。默认情况下,Runtime 容器的 5401 端口会被映射到宿主机的 5401 端口。
|
||||
|
||||
@@ -1121,13 +1121,18 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
# a managed runtime and authenticate its WebSocket controller.
|
||||
self.ap.logger.info('(windows) use cmd to launch plugin runtime and communicate via ws')
|
||||
control_headers = self._control_headers(allow_generate=True)
|
||||
control_port, debug_port = self._managed_runtime_ports()
|
||||
await self._start_runtime_subprocess(
|
||||
'-m',
|
||||
'langbot_plugin.cli.__init__',
|
||||
'rt',
|
||||
'--ws-control-port',
|
||||
str(control_port),
|
||||
'--ws-debug-port',
|
||||
str(debug_port),
|
||||
env_overrides={PLUGIN_RUNTIME_CONTROL_TOKEN_ENV: self._control_token},
|
||||
)
|
||||
ws_url = 'ws://localhost:5400/control/ws'
|
||||
ws_url = f'ws://localhost:{control_port}/control/ws'
|
||||
|
||||
async def connection_failed(
|
||||
ctrl: ws_client_controller.WebSocketClientController,
|
||||
@@ -1145,9 +1150,17 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
task_coro = self.ctrl.run(new_connection_callback)
|
||||
else:
|
||||
self.ap.logger.info('use stdio to connect to plugin runtime')
|
||||
_, debug_port = self._managed_runtime_ports()
|
||||
self.ctrl = stdio_client_controller.StdioClientController(
|
||||
command=sys.executable,
|
||||
args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'],
|
||||
args=[
|
||||
'-m',
|
||||
'langbot_plugin.cli.__init__',
|
||||
'rt',
|
||||
'-s',
|
||||
'--ws-debug-port',
|
||||
str(debug_port),
|
||||
],
|
||||
env=os.environ.copy(),
|
||||
capture_stderr=False,
|
||||
)
|
||||
@@ -1235,14 +1248,43 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
await self.runtime_ops_reporter.stop()
|
||||
await self._close_managed_subprocess()
|
||||
|
||||
@staticmethod
|
||||
def _runtime_port_from_url(url: str, *, default: int) -> int:
|
||||
"""Extract a TCP port from a WebSocket/HTTP URL, falling back to *default*."""
|
||||
try:
|
||||
parsed = urlparse(url if '://' in url else f'//{url}')
|
||||
return parsed.port or default
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def _runtime_debug_port_from_url(debug_url: str) -> int:
|
||||
"""Extract the local plugin runtime debug port from its display URL."""
|
||||
try:
|
||||
parsed = urlparse(debug_url if '://' in debug_url else f'//{debug_url}')
|
||||
return parsed.port or 5401
|
||||
except (TypeError, ValueError):
|
||||
return 5401
|
||||
return PluginRuntimeConnector._runtime_port_from_url(debug_url, default=5401)
|
||||
|
||||
@staticmethod
|
||||
def _runtime_control_port_from_url(control_url: str) -> int:
|
||||
"""Extract the local plugin runtime control port from its WebSocket URL."""
|
||||
return PluginRuntimeConnector._runtime_port_from_url(control_url, default=5400)
|
||||
|
||||
def _managed_runtime_ports(self) -> tuple[int, int]:
|
||||
"""Resolve control/debug ports for a Core-managed Runtime subprocess.
|
||||
|
||||
Docker/standalone deployments keep listening on their own configured
|
||||
ports; this helper only affects the stdio and Windows launch paths,
|
||||
where LangBot starts Runtime itself. The debug port follows
|
||||
``plugin.display_plugin_debug_url`` so a second Core instance can avoid
|
||||
``Address already in use`` on the default 5401 without editing CLI
|
||||
flags by hand. The control port follows ``plugin.runtime_ws_url`` for
|
||||
the Windows WebSocket path.
|
||||
"""
|
||||
plugin_config = self.ap.instance_config.data.get('plugin') or {}
|
||||
control_url = plugin_config.get('runtime_ws_url') or 'ws://localhost:5400/control/ws'
|
||||
debug_url = plugin_config.get('display_plugin_debug_url') or 'ws://localhost:5401/plugin/debug/ws'
|
||||
return (
|
||||
self._runtime_control_port_from_url(str(control_url)),
|
||||
self._runtime_debug_port_from_url(str(debug_url)),
|
||||
)
|
||||
|
||||
async def initialize_plugins(self):
|
||||
pass
|
||||
|
||||
@@ -289,6 +289,9 @@ plugin:
|
||||
connect_timeout_seconds: 180.0
|
||||
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
|
||||
enable_marketplace: true
|
||||
# Console debug endpoint; its port is also passed to managed Runtime
|
||||
# subprocesses (stdio / Windows) as --ws-debug-port so a second Core
|
||||
# instance can listen elsewhere without Address already in use.
|
||||
display_plugin_debug_url: 'ws://localhost:5401/plugin/debug/ws'
|
||||
certification:
|
||||
# Non-secret Ed25519 issuer key ring used to verify the SDK ZIP-comment
|
||||
|
||||
@@ -460,3 +460,171 @@ async def test_explicit_cloud_binding_is_revalidated_against_projection():
|
||||
expected_generation=7,
|
||||
)
|
||||
connector._synchronize_workspace.assert_awaited_once_with(configured)
|
||||
|
||||
|
||||
def test_runtime_debug_port_from_url_defaults_and_parses():
|
||||
assert PluginRuntimeConnector._runtime_debug_port_from_url('') == 5401
|
||||
assert PluginRuntimeConnector._runtime_debug_port_from_url('ws://localhost:5411/plugin/debug/ws') == 5411
|
||||
assert PluginRuntimeConnector._runtime_debug_port_from_url('localhost:5412/plugin/debug/ws') == 5412
|
||||
assert PluginRuntimeConnector._runtime_control_port_from_url('ws://langbot_plugin_runtime:5500/control/ws') == 5500
|
||||
assert PluginRuntimeConnector._runtime_control_port_from_url('') == 5400
|
||||
|
||||
|
||||
def test_managed_runtime_ports_follow_plugin_config_urls():
|
||||
connector = make_connector()
|
||||
connector.ap.instance_config.data['plugin']['runtime_ws_url'] = 'ws://127.0.0.1:5500/control/ws'
|
||||
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = 'ws://127.0.0.1:5411/plugin/debug/ws'
|
||||
|
||||
assert connector._managed_runtime_ports() == (5500, 5411)
|
||||
|
||||
|
||||
def test_managed_runtime_ports_default_when_urls_empty():
|
||||
connector = make_connector()
|
||||
connector.ap.instance_config.data['plugin']['runtime_ws_url'] = ''
|
||||
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = ''
|
||||
|
||||
assert connector._managed_runtime_ports() == (5400, 5401)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stdio_runtime_passes_debug_port_from_display_url(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
connector = make_connector()
|
||||
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = 'ws://localhost:5411/plugin/debug/ws'
|
||||
connector._prepare_connected_runtime = AsyncMock()
|
||||
created = {}
|
||||
monkeypatch.setattr(connector_module.constants, 'instance_id', 'instance-a')
|
||||
|
||||
class FakeRuntimeHandler:
|
||||
def __init__(self, connection, disconnect_callback, ap):
|
||||
self.release = asyncio.Event()
|
||||
|
||||
async def ping(self):
|
||||
return None
|
||||
|
||||
async def set_runtime_config(self, **kwargs):
|
||||
return None
|
||||
|
||||
async def run(self):
|
||||
await self.release.wait()
|
||||
|
||||
async def close(self):
|
||||
self.release.set()
|
||||
|
||||
class FakeController:
|
||||
def __init__(self, **kwargs):
|
||||
created.update(kwargs)
|
||||
|
||||
async def run(self, callback):
|
||||
await callback(object())
|
||||
|
||||
async def close(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(connector_module.platform, 'get_platform', lambda: 'linux')
|
||||
monkeypatch.setattr(
|
||||
connector_module.platform,
|
||||
'use_websocket_to_connect_plugin_runtime',
|
||||
lambda: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
connector_module.handler,
|
||||
'RuntimeConnectionHandler',
|
||||
FakeRuntimeHandler,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
connector_module.stdio_client_controller,
|
||||
'StdioClientController',
|
||||
FakeController,
|
||||
)
|
||||
|
||||
await connector.initialize()
|
||||
|
||||
assert created['args'] == [
|
||||
'-m',
|
||||
'langbot_plugin.cli.__init__',
|
||||
'rt',
|
||||
'-s',
|
||||
'--ws-debug-port',
|
||||
'5411',
|
||||
]
|
||||
assert connector._connected.is_set()
|
||||
await connector.aclose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_windows_runtime_passes_control_and_debug_ports(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
connector = make_connector()
|
||||
connector.ap.instance_config.data['plugin']['runtime_ws_url'] = 'ws://localhost:5500/control/ws'
|
||||
connector.ap.instance_config.data['plugin']['display_plugin_debug_url'] = 'ws://localhost:5411/plugin/debug/ws'
|
||||
connector._prepare_connected_runtime = AsyncMock()
|
||||
start_args: list[object] = []
|
||||
created = {}
|
||||
monkeypatch.setattr(connector_module.constants, 'instance_id', 'instance-a')
|
||||
|
||||
class FakeRuntimeHandler:
|
||||
def __init__(self, connection, disconnect_callback, ap):
|
||||
self.release = asyncio.Event()
|
||||
|
||||
async def ping(self):
|
||||
return None
|
||||
|
||||
async def set_runtime_config(self, **kwargs):
|
||||
return None
|
||||
|
||||
async def run(self):
|
||||
await self.release.wait()
|
||||
|
||||
async def close(self):
|
||||
self.release.set()
|
||||
|
||||
class FakeController:
|
||||
def __init__(self, **kwargs):
|
||||
created.update(kwargs)
|
||||
|
||||
async def run(self, callback):
|
||||
await callback(object())
|
||||
|
||||
async def close(self):
|
||||
return None
|
||||
|
||||
async def fake_start(*args, env_overrides=None):
|
||||
start_args.extend(args)
|
||||
start_args.append(env_overrides)
|
||||
|
||||
monkeypatch.setattr(connector_module.platform, 'get_platform', lambda: 'win32')
|
||||
monkeypatch.setattr(
|
||||
connector_module.platform,
|
||||
'use_websocket_to_connect_plugin_runtime',
|
||||
lambda: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
connector_module.handler,
|
||||
'RuntimeConnectionHandler',
|
||||
FakeRuntimeHandler,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
connector_module.ws_client_controller,
|
||||
'WebSocketClientController',
|
||||
FakeController,
|
||||
)
|
||||
monkeypatch.setattr(connector, '_start_runtime_subprocess', fake_start)
|
||||
|
||||
await connector.initialize()
|
||||
|
||||
assert start_args[:-1] == [
|
||||
'-m',
|
||||
'langbot_plugin.cli.__init__',
|
||||
'rt',
|
||||
'--ws-control-port',
|
||||
'5500',
|
||||
'--ws-debug-port',
|
||||
'5411',
|
||||
]
|
||||
assert start_args[-1] == {PLUGIN_RUNTIME_CONTROL_TOKEN_ENV: connector._control_token}
|
||||
assert created['ws_url'] == 'ws://localhost:5500/control/ws'
|
||||
assert connector._connected.is_set()
|
||||
await connector.aclose()
|
||||
|
||||
Reference in New Issue
Block a user