Skip to content

Commit 5c3a0ff

Browse files
committed
feat: add check_availablity for server
1 parent 5595bde commit 5c3a0ff

File tree

5 files changed

+49
-24
lines changed

5 files changed

+49
-24
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ classifiers = [
1313
]
1414

1515
dependencies = [
16+
"aioshutil>=1.6",
1617
"anyio>=4.10.0",
1718
"asyncer>=0.0.10",
1819
"attrs>=25.3.0",

src/lsp_client/client/abc.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import os
44
from abc import ABC, abstractmethod
5-
from collections.abc import AsyncGenerator, Iterable
6-
from contextlib import asynccontextmanager
5+
from collections.abc import AsyncGenerator
6+
from contextlib import asynccontextmanager, suppress
77
from pathlib import Path
88
from typing import Any, Literal, Self, override
99

@@ -62,12 +62,13 @@ class Client(
6262
_workspace: Workspace = field(init=False)
6363
_buffer: LSPFileBuffer = field(factory=LSPFileBuffer, init=False)
6464

65-
def _iter_candidate_servers(self) -> Iterable[Server]:
65+
async def _iter_candidate_servers(self) -> AsyncGenerator[Server]:
6666
"""
6767
Server candidates in order of priority:
6868
1. User-provided server
69-
2. Containerized server
70-
3. Local server (maybe with auto-installation)
69+
2. Local server (if available)
70+
3. Containerized server
71+
4. Local server with auto-install (if enabled)
7172
"""
7273

7374
defaults = self.create_default_servers()
@@ -80,6 +81,10 @@ def _iter_candidate_servers(self) -> Iterable[Server]:
8081
case Server() as server:
8182
yield server
8283

84+
with suppress(ServerRuntimeError):
85+
await defaults.local.check_availability()
86+
yield defaults.local
87+
8388
yield defaults.container
8489
yield defaults.local
8590

@@ -230,13 +235,13 @@ async def _run_server(
230235
) -> AsyncGenerator[tuple[Server, Receiver[ServerRequest]]]:
231236
async with channel[ServerRequest].create() as (sender, receiver):
232237
errors: list[ServerRuntimeError] = []
233-
for server in self._iter_candidate_servers():
238+
async for candidate in self._iter_candidate_servers():
234239
try:
235-
async with server.run(self._workspace, sender=sender) as s: # ty: ignore[invalid-argument-type]
236-
yield s, receiver
240+
async with candidate.run(self._workspace, sender=sender) as server: # ty: ignore[invalid-argument-type]
241+
yield server, receiver
237242
return
238243
except ServerRuntimeError as e:
239-
logger.debug("Failed to start server {}: {}", server, e)
244+
logger.debug("Failed to start server {}: {}", candidate, e)
240245
errors.append(e)
241246

242247
raise ExceptionGroup(

src/lsp_client/server/abc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class Server(ABC):
2727

2828
_resp_table: ResponseTable = field(factory=ResponseTable, init=False)
2929

30+
@abstractmethod
31+
async def check_availability(self) -> None:
32+
"""Check if the server runtime is available."""
33+
3034
@abstractmethod
3135
async def send(self, package: RawPackage) -> None:
3236
"""Send a package to the runtime."""

src/lsp_client/server/container.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import Literal, final, override
77

8+
import anyio
89
from attrs import Factory, define, field
910
from loguru import logger
1011

@@ -191,6 +192,20 @@ async def receive(self) -> RawPackage | None:
191192
async def kill(self) -> None:
192193
await self._local.kill()
193194

195+
@override
196+
async def check_availability(self) -> None:
197+
try:
198+
await anyio.run_process(
199+
[self.backend, "pull", self.image],
200+
stdout=anyio.streams.devnull.DevnullStream(),
201+
stderr=anyio.streams.devnull.DevnullStream(),
202+
)
203+
except anyio.ProcessError as e:
204+
raise RuntimeError(
205+
f"Container backend '{self.backend}' is not available or image '{self.image}' "
206+
"could not be pulled."
207+
) from e
208+
194209
@override
195210
@asynccontextmanager
196211
async def run_process(self, workspace: Workspace) -> AsyncGenerator[None]:

src/lsp_client/server/local.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from __future__ import annotations
22

3-
import shutil
43
from collections.abc import AsyncGenerator, Sequence
54
from contextlib import asynccontextmanager
65
from functools import cached_property
76
from pathlib import Path
87
from typing import Protocol, final, override
98

9+
import aioshutil
1010
import anyio
1111
from anyio.abc import AnyByteSendStream, Process
1212
from anyio.streams.buffered import BufferedByteReceiveStream
@@ -80,27 +80,27 @@ async def kill(self) -> None:
8080
self._process.kill()
8181
await self._process.aclose()
8282

83+
@override
84+
async def check_availability(self) -> None:
85+
if not await aioshutil.which(self.program):
86+
raise ServerRuntimeError(
87+
self, f"Program '{self.program}' not found in PATH."
88+
)
89+
8390
@override
8491
@asynccontextmanager
8592
async def run_process(self, workspace: Workspace) -> AsyncGenerator[None]:
86-
if not shutil.which(self.program):
93+
try:
94+
await self.check_availability()
95+
except ServerRuntimeError as e:
8796
if disable_auto_installation():
88-
raise ServerRuntimeError(
89-
self,
90-
f"Program '{self.program}' not found in PATH and auto-installation is disabled.",
91-
)
97+
raise ServerRuntimeError(self, "auto-installation is disabled.") from e
9298
elif self.ensure_installed:
93-
try:
94-
await self.ensure_installed()
95-
except (OSError, RuntimeError) as e:
96-
raise ServerRuntimeError(
97-
self, f"Failed to install '{self.program}'"
98-
) from e
99+
await self.ensure_installed()
99100
else:
100101
raise ServerRuntimeError(
101-
self,
102-
f"Program '{self.program}' not found in PATH and no installation method is provided.",
103-
)
102+
self, "no installation method is provided."
103+
) from e
104104

105105
command = [self.program, *self.args]
106106
logger.debug("Running with command: {}", command)

0 commit comments

Comments
 (0)