mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
fix: avoid lsp client cancel scope leaks
This commit is contained in:
@@ -7,6 +7,7 @@ that provide language intelligence features (completions, diagnostics, etc.).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
@@ -35,8 +36,7 @@ class AstrbotLspClient(BaseAstrbotLspClient):
|
||||
self._pending_requests: dict[int, Any] = {}
|
||||
self._request_id = 0
|
||||
self._server_command: list[str] | None = None
|
||||
# anyio TaskGroup handle for background readers
|
||||
self._task_group: Any | None = None
|
||||
self._reader_task: asyncio.Task[None] | None = None
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
@@ -77,11 +77,8 @@ class AstrbotLspClient(BaseAstrbotLspClient):
|
||||
self._server_command = command
|
||||
self._connected = True
|
||||
|
||||
# Start reading responses in background using anyio TaskGroup
|
||||
# Create and enter a TaskGroup so the reader runs until we close it at shutdown.
|
||||
self._task_group = anyio.create_task_group()
|
||||
await self._task_group.__aenter__()
|
||||
self._task_group.start_soon(self._read_responses)
|
||||
# Start reading responses in the background.
|
||||
self._reader_task = asyncio.create_task(self._read_responses())
|
||||
|
||||
# Send initialize request
|
||||
await self.send_request(
|
||||
@@ -217,13 +214,13 @@ class AstrbotLspClient(BaseAstrbotLspClient):
|
||||
"""Shutdown the LSP client."""
|
||||
self._connected = False
|
||||
|
||||
if self._task_group:
|
||||
if self._reader_task:
|
||||
self._reader_task.cancel()
|
||||
try:
|
||||
# Exit the TaskGroup, which cancels background tasks started within it
|
||||
await self._task_group.__aexit__(None, None, None)
|
||||
except anyio.get_cancelled_exc_class():
|
||||
await self._reader_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._task_group = None
|
||||
self._reader_task = None
|
||||
|
||||
if self._server_process:
|
||||
try:
|
||||
|
||||
@@ -6,11 +6,17 @@ Tests the LSP client against a real LSP server fixture.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
|
||||
from astrbot._internal.protocols.lsp.client import AstrbotLspClient
|
||||
|
||||
TEST_DIR = Path(__file__).resolve().parent
|
||||
SERVER_PATH = TEST_DIR / "fixtures" / "echo_lsp_server.py"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_lsp_client_initialization():
|
||||
@@ -25,11 +31,8 @@ async def test_lsp_client_connect_to_echo_server():
|
||||
"""Test LSP client can connect to echo LSP server."""
|
||||
client = AstrbotLspClient()
|
||||
|
||||
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py")
|
||||
|
||||
await client.connect_to_server(
|
||||
command=["python", server_path],
|
||||
command=[sys.executable, str(SERVER_PATH)],
|
||||
workspace_uri="file:///tmp",
|
||||
)
|
||||
|
||||
@@ -44,11 +47,8 @@ async def test_lsp_client_send_request():
|
||||
"""Test LSP client can send a request and receive response."""
|
||||
client = AstrbotLspClient()
|
||||
|
||||
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py")
|
||||
|
||||
await client.connect_to_server(
|
||||
command=["python", server_path],
|
||||
command=[sys.executable, str(SERVER_PATH)],
|
||||
workspace_uri="file:///tmp",
|
||||
)
|
||||
|
||||
@@ -68,11 +68,8 @@ async def test_lsp_client_send_notification():
|
||||
"""Test LSP client can send a notification (no response)."""
|
||||
client = AstrbotLspClient()
|
||||
|
||||
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_path = os.path.join(test_dir, "fixtures", "echo_lsp_server.py")
|
||||
|
||||
await client.connect_to_server(
|
||||
command=["python", server_path],
|
||||
command=[sys.executable, str(SERVER_PATH)],
|
||||
workspace_uri="file:///tmp",
|
||||
)
|
||||
|
||||
@@ -99,4 +96,22 @@ async def test_lsp_client_send_notification_not_connected():
|
||||
"""Test LSP client raises RuntimeError when sending notification while not connected."""
|
||||
client = AstrbotLspClient()
|
||||
with pytest.raises(RuntimeError, match="LSP client not connected"):
|
||||
await client.send_notification("test", {})
|
||||
await client.send_notification("test", {})
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_lsp_client_connect_does_not_corrupt_anyio_cancel_scope():
|
||||
"""Test connect/shutdown can run inside fail_after scopes without scope corruption."""
|
||||
client = AstrbotLspClient()
|
||||
|
||||
with anyio.fail_after(5):
|
||||
await client.connect_to_server(
|
||||
command=[sys.executable, str(SERVER_PATH)],
|
||||
workspace_uri="file:///tmp",
|
||||
)
|
||||
|
||||
try:
|
||||
assert client.connected
|
||||
finally:
|
||||
with anyio.fail_after(5):
|
||||
await client.shutdown()
|
||||
|
||||
Reference in New Issue
Block a user