fix: avoid lsp client cancel scope leaks

This commit is contained in:
邹永赫
2026-03-26 01:50:18 +09:00
parent c529c716f4
commit 5fa18dd836
2 changed files with 38 additions and 26 deletions

View File

@@ -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:

View File

@@ -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()