The changes align the project with the 2025.0.0 lsprotocol release, removing the old backport and updating type hints in the protocol hooks to use Sequence where appropriate. The dist-info and packaging metadata for older lsprotocol versions are replaced with the new 2025.0.0 artifacts. - Remove exceptiongroup backport used on Python <3.11 - Use Sequence instead of List in LS protocol hooks - Replace old dist-info with 2025.0.0 metadata
284 lines
9.4 KiB
Python
284 lines
9.4 KiB
Python
############################################################################
|
|
# Copyright(c) Open Law Library. All rights reserved. #
|
|
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
|
# #
|
|
# Licensed under the Apache License, Version 2.0 (the "License") #
|
|
# you may not use this file except in compliance with the License. #
|
|
# You may obtain a copy of the License at #
|
|
# #
|
|
# http: // www.apache.org/licenses/LICENSE-2.0 #
|
|
# #
|
|
# Unless required by applicable law or agreed to in writing, software #
|
|
# distributed under the License is distributed on an "AS IS" BASIS, #
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
|
# See the License for the specific language governing permissions and #
|
|
# limitations under the License. #
|
|
############################################################################
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import sys
|
|
import typing
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from threading import Event
|
|
|
|
import cattrs
|
|
|
|
from pygls import IS_WASM
|
|
from pygls.exceptions import JsonRpcException, PyglsError
|
|
from pygls.io_ import StdinAsyncReader, StdoutWriter, run, run_async, run_websocket
|
|
from pygls.protocol import JsonRPCProtocol
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from typing import Any, BinaryIO, Callable, Optional, Type, TypeVar, Union
|
|
|
|
from websockets.asyncio.server import Server as WSServer
|
|
from websockets.asyncio.server import ServerConnection
|
|
|
|
F = TypeVar("F", bound=Callable)
|
|
ServerErrors = Union[type[PyglsError], type[JsonRpcException]]
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class JsonRPCServer:
|
|
"""Base server class
|
|
|
|
Parameters
|
|
----------
|
|
protocol_cls
|
|
Protocol implementation that should derive from
|
|
:class:`~pygls.protocol.JsonRPCProtocol`
|
|
|
|
converter_factory
|
|
Factory function to use when constructing a cattrs converter.
|
|
|
|
max_workers
|
|
Maximum number of workers for `ThreadPoolExecutor`
|
|
|
|
"""
|
|
|
|
protocol: JsonRPCProtocol
|
|
|
|
def __init__(
|
|
self,
|
|
protocol_cls: Type[JsonRPCProtocol],
|
|
converter_factory: Callable[[], cattrs.Converter],
|
|
max_workers: int | None = None,
|
|
):
|
|
self._max_workers = max_workers
|
|
self._server: asyncio.Server | WSServer | None = None
|
|
self._stop_event: Event | None = None
|
|
self._thread_pool: ThreadPoolExecutor | None = None
|
|
|
|
self.protocol = protocol_cls(self, converter_factory())
|
|
|
|
def shutdown(self):
|
|
"""Shutdown server."""
|
|
logger.info("Shutting down the server")
|
|
|
|
if self._stop_event is not None:
|
|
self._stop_event.set()
|
|
|
|
if self._thread_pool:
|
|
self._thread_pool.shutdown()
|
|
|
|
if self._server:
|
|
self._server.close()
|
|
|
|
def _report_server_error(
|
|
self,
|
|
error: Exception,
|
|
source: ServerErrors,
|
|
):
|
|
# Prevent recursive error reporting
|
|
try:
|
|
self.report_server_error(error, source)
|
|
except Exception:
|
|
logger.warning("Failed to report error")
|
|
|
|
def report_server_error(self, error: Exception, source: ServerErrors):
|
|
"""Default error reporter."""
|
|
logger.error("%s", error)
|
|
|
|
def start_io(
|
|
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
|
):
|
|
"""Starts an IO server."""
|
|
|
|
if IS_WASM:
|
|
self._start_io_sync(stdin, stdout)
|
|
else:
|
|
self._start_io_async(stdin, stdout)
|
|
|
|
def _start_io_async(
|
|
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
|
):
|
|
"""Starts an asynchronous IO server."""
|
|
logger.info("Starting async IO server")
|
|
|
|
self._stop_event = Event()
|
|
reader = StdinAsyncReader(stdin or sys.stdin.buffer, self.thread_pool)
|
|
writer = StdoutWriter(stdout or sys.stdout.buffer)
|
|
self.protocol.set_writer(writer)
|
|
|
|
try:
|
|
asyncio.run(
|
|
run_async(
|
|
stop_event=self._stop_event,
|
|
reader=reader,
|
|
protocol=self.protocol,
|
|
logger=logger,
|
|
error_handler=self.report_server_error,
|
|
)
|
|
)
|
|
except BrokenPipeError:
|
|
logger.error("Connection to the client is lost! Shutting down the server.")
|
|
except (KeyboardInterrupt, SystemExit):
|
|
pass
|
|
finally:
|
|
self.shutdown()
|
|
|
|
def _start_io_sync(
|
|
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
|
):
|
|
"""Starts an synchronous IO server."""
|
|
logger.info("Starting sync IO server")
|
|
|
|
self._stop_event = Event()
|
|
writer = StdoutWriter(stdout or sys.stdout.buffer)
|
|
self.protocol.set_writer(writer)
|
|
|
|
try:
|
|
asyncio.run(
|
|
run(
|
|
stop_event=self._stop_event,
|
|
reader=stdin or sys.stdin.buffer,
|
|
protocol=self.protocol,
|
|
logger=logger,
|
|
error_handler=self.report_server_error,
|
|
)
|
|
)
|
|
except BrokenPipeError:
|
|
logger.error("Connection to the client is lost! Shutting down the server.")
|
|
except (KeyboardInterrupt, SystemExit):
|
|
pass
|
|
finally:
|
|
self.shutdown()
|
|
|
|
def start_tcp(self, host: str, port: int) -> None:
|
|
"""Starts TCP server."""
|
|
logger.info("Starting TCP server on %s:%s", host, port)
|
|
|
|
self._stop_event = stop_event = Event()
|
|
|
|
async def lsp_connection(
|
|
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
):
|
|
logger.debug("Connected to client")
|
|
self.protocol.set_writer(writer) # type: ignore
|
|
await run_async(
|
|
stop_event=stop_event,
|
|
reader=reader,
|
|
protocol=self.protocol,
|
|
logger=logger,
|
|
error_handler=self.report_server_error,
|
|
)
|
|
logger.debug("Main loop finished")
|
|
self.shutdown()
|
|
|
|
async def tcp_server(h: str, p: int):
|
|
self._server = await asyncio.start_server(lsp_connection, h, p)
|
|
|
|
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
|
logger.info(f"Serving on {addrs}")
|
|
|
|
async with self._server:
|
|
await self._server.serve_forever()
|
|
|
|
try:
|
|
asyncio.run(tcp_server(host, port))
|
|
except asyncio.CancelledError:
|
|
logger.debug("Server was cancelled")
|
|
|
|
def start_ws(self, host: str, port: int) -> None:
|
|
"""Starts WebSocket server."""
|
|
try:
|
|
from websockets.asyncio.server import serve
|
|
except ImportError:
|
|
logger.error(
|
|
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
|
)
|
|
sys.exit(1)
|
|
|
|
logger.info("Starting WebSocket server on {}:{}".format(host, port))
|
|
self._stop_event = stop_event = Event()
|
|
|
|
async def lsp_connection(websocket: ServerConnection):
|
|
await run_websocket(
|
|
stop_event=stop_event,
|
|
websocket=websocket,
|
|
protocol=self.protocol,
|
|
logger=logger,
|
|
error_handler=self.report_server_error,
|
|
)
|
|
self.shutdown()
|
|
|
|
async def ws_server(h: str, p: int):
|
|
self._server = await serve(lsp_connection, host, port)
|
|
|
|
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
|
logger.info(f"Serving on {addrs}")
|
|
|
|
async with self._server:
|
|
await self._server.serve_forever()
|
|
|
|
try:
|
|
asyncio.run(ws_server(host, port))
|
|
except asyncio.CancelledError:
|
|
logger.debug("Server was cancelled")
|
|
|
|
def command(self, command_name: str) -> Callable[[F], F]:
|
|
"""Decorator used to register custom commands.
|
|
|
|
Example
|
|
-------
|
|
::
|
|
|
|
@ls.command('myCustomCommand')
|
|
def my_cmd(ls, a, b, c):
|
|
pass
|
|
"""
|
|
return self.protocol.fm.command(command_name)
|
|
|
|
def thread(self) -> Callable[[F], F]:
|
|
"""Decorator that mark function to execute it in a thread."""
|
|
return self.protocol.fm.thread()
|
|
|
|
def feature(
|
|
self,
|
|
feature_name: str,
|
|
options: Any | None = None,
|
|
) -> Callable[[F], F]:
|
|
"""Decorator used to register LSP features.
|
|
|
|
Example
|
|
-------
|
|
::
|
|
|
|
@ls.feature('textDocument/completion', CompletionOptions(trigger_characters=['.']))
|
|
def completions(ls, params: CompletionParams):
|
|
return CompletionList(is_incomplete=False, items=[CompletionItem("Completion 1")])
|
|
"""
|
|
return self.protocol.fm.feature(feature_name, options)
|
|
|
|
@property
|
|
def thread_pool(self) -> ThreadPoolExecutor:
|
|
"""Returns thread pool instance (lazy initialization)."""
|
|
if not self._thread_pool:
|
|
self._thread_pool = ThreadPoolExecutor(max_workers=self._max_workers)
|
|
|
|
return self._thread_pool
|