Skip to content

Commit c3b0cff

Browse files
committed
feat: add SocketServer for TCP and Unix socket communication
1 parent 36b560e commit c3b0cff

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

src/lsp_client/server/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .container import ContainerServer
77
from .exception import ServerError, ServerInstallationError, ServerRuntimeError
88
from .local import LocalServer
9+
from .socket import SocketServer
910

1011

1112
@frozen
@@ -22,4 +23,5 @@ class DefaultServers:
2223
"ServerError",
2324
"ServerInstallationError",
2425
"ServerRuntimeError",
26+
"SocketServer",
2527
]

src/lsp_client/server/socket.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import AsyncGenerator
4+
from contextlib import asynccontextmanager
5+
from pathlib import Path
6+
from typing import final, override
7+
8+
import anyio
9+
from anyio.abc import ByteStream
10+
from anyio.streams.buffered import BufferedByteReceiveStream
11+
from attrs import define, field
12+
from loguru import logger
13+
from tenacity import AsyncRetrying, stop_after_delay, wait_exponential
14+
15+
from lsp_client.jsonrpc.parse import read_raw_package, write_raw_package
16+
from lsp_client.jsonrpc.types import RawPackage
17+
from lsp_client.utils.workspace import Workspace
18+
19+
from .abc import Server
20+
21+
22+
@final
23+
@define
24+
class SocketServer(Server):
25+
"""Runtime for socket backend, e.g. connecting to a remote LSP server via TCP or Unix socket."""
26+
27+
host: str | None = None
28+
"""The host to connect to (TCP only)."""
29+
port: int | None = None
30+
"""The port to connect to (TCP only)."""
31+
path: Path | str | None = None
32+
"""The path to the Unix socket (Unix only)."""
33+
timeout: float = 10.0
34+
"""Timeout for connecting to the socket."""
35+
36+
_stream: ByteStream | None = field(init=False, default=None)
37+
_buffered: BufferedByteReceiveStream | None = field(init=False, default=None)
38+
39+
@override
40+
async def check_availability(self) -> None:
41+
if self.host is None and self.port is None and self.path is None:
42+
raise ValueError(
43+
"Either host and port (for TCP), or path (for Unix socket) must be provided"
44+
)
45+
46+
@override
47+
async def send(self, package: RawPackage) -> None:
48+
if self._stream is None:
49+
raise RuntimeError(
50+
"SocketServer is not running. Use 'async with server.run(...)'"
51+
)
52+
await write_raw_package(self._stream, package)
53+
54+
@override
55+
async def receive(self) -> RawPackage | None:
56+
if self._buffered is None:
57+
raise RuntimeError(
58+
"SocketServer is not running. Use 'async with server.run(...)'"
59+
)
60+
try:
61+
return await read_raw_package(self._buffered)
62+
except (anyio.EndOfStream, anyio.IncompleteRead, anyio.ClosedResourceError):
63+
logger.debug("Socket closed")
64+
return None
65+
66+
@override
67+
async def kill(self) -> None:
68+
if self._stream:
69+
await self._stream.aclose()
70+
71+
@override
72+
@asynccontextmanager
73+
async def run_process(self, workspace: Workspace) -> AsyncGenerator[None]:
74+
await self.check_availability()
75+
76+
async def connect() -> ByteStream:
77+
if self.host is not None and self.port is not None:
78+
logger.debug("Connecting to {}:{}", self.host, self.port)
79+
return await anyio.connect_tcp(self.host, self.port)
80+
if self.path is not None:
81+
if not hasattr(anyio, "connect_unix"):
82+
raise RuntimeError(
83+
"Unix sockets are not supported on this platform"
84+
)
85+
logger.debug("Connecting to {}", self.path)
86+
return await anyio.connect_unix(str(self.path))
87+
raise ValueError("Either host and port, or path must be provided")
88+
89+
stream: ByteStream | None = None
90+
async for attempt in AsyncRetrying(
91+
stop=stop_after_delay(self.timeout),
92+
wait=wait_exponential(multiplier=0.1, max=1),
93+
reraise=True,
94+
):
95+
with attempt:
96+
stream = await connect()
97+
98+
if stream is None:
99+
raise RuntimeError("Failed to connect to socket")
100+
101+
async with stream:
102+
self._stream = stream
103+
self._buffered = BufferedByteReceiveStream(stream)
104+
try:
105+
yield
106+
finally:
107+
self._stream = None
108+
self._buffered = None

0 commit comments

Comments
 (0)