You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
6.9 KiB
Python
204 lines
6.9 KiB
Python
""" Language Server stdio-mode readers
|
|
|
|
Parts of this code are derived from:
|
|
|
|
> https://github.com/palantir/python-jsonrpc-server/blob/0.2.0/pyls_jsonrpc/streams.py#L83 # noqa
|
|
> https://github.com/palantir/python-jsonrpc-server/blob/45ed1931e4b2e5100cc61b3992c16d6f68af2e80/pyls_jsonrpc/streams.py # noqa
|
|
> > MIT License https://github.com/palantir/python-jsonrpc-server/blob/0.2.0/LICENSE
|
|
> > Copyright 2018 Palantir Technologies, Inc.
|
|
"""
|
|
|
|
# pylint: disable=broad-except
|
|
import asyncio
|
|
import io
|
|
import os
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import List, Optional, Text
|
|
|
|
from tornado.concurrent import run_on_executor
|
|
from tornado.gen import convert_yielded
|
|
from tornado.httputil import HTTPHeaders
|
|
from tornado.ioloop import IOLoop
|
|
from tornado.queues import Queue
|
|
from traitlets import Float, Instance, default
|
|
from traitlets.config import LoggingConfigurable
|
|
|
|
from .non_blocking import make_non_blocking
|
|
|
|
|
|
class LspStdIoBase(LoggingConfigurable):
|
|
"""Non-blocking, queued base for communicating with stdio Language Servers"""
|
|
|
|
executor = None
|
|
|
|
stream = Instance( # type:ignore[assignment]
|
|
io.RawIOBase, help="the stream to read/write"
|
|
) # type: io.RawIOBase
|
|
queue = Instance(Queue, help="queue to get/put")
|
|
|
|
def __repr__(self): # pragma: no cover
|
|
return "<{}(parent={})>".format(self.__class__.__name__, self.parent)
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.log.debug("%s initialized", self)
|
|
self.executor = ThreadPoolExecutor(max_workers=1)
|
|
|
|
def close(self):
|
|
self.stream.close()
|
|
self.log.debug("%s closed", self)
|
|
|
|
|
|
class LspStdIoReader(LspStdIoBase):
|
|
"""Language Server stdio Reader
|
|
|
|
Because non-blocking (but still synchronous) IO is used, rudimentary
|
|
exponential backoff is used.
|
|
"""
|
|
|
|
max_wait = Float(help="maximum time to wait on idle stream").tag(config=True)
|
|
min_wait = Float(0.05, help="minimum time to wait on idle stream").tag(config=True)
|
|
next_wait = Float(0.05, help="next time to wait on idle stream").tag(config=True)
|
|
|
|
@default("max_wait")
|
|
def _default_max_wait(self):
|
|
return 0.1 if os.name == "nt" else self.min_wait * 2
|
|
|
|
async def sleep(self):
|
|
"""Simple exponential backoff for sleeping"""
|
|
if self.stream.closed: # pragma: no cover
|
|
return
|
|
self.next_wait = min(self.next_wait * 2, self.max_wait)
|
|
try:
|
|
await asyncio.sleep(self.next_wait)
|
|
except Exception: # pragma: no cover
|
|
pass
|
|
|
|
def wake(self):
|
|
"""Reset the wait time"""
|
|
self.wait = self.min_wait
|
|
|
|
async def read(self) -> None:
|
|
"""Read from a Language Server until it is closed"""
|
|
make_non_blocking(self.stream)
|
|
|
|
while not self.stream.closed:
|
|
message = None
|
|
try:
|
|
message = await self.read_one()
|
|
|
|
if not message:
|
|
await self.sleep()
|
|
continue
|
|
else:
|
|
self.wake()
|
|
|
|
IOLoop.current().add_callback(self.queue.put_nowait, message)
|
|
except Exception as e: # pragma: no cover
|
|
self.log.exception(
|
|
"%s couldn't enqueue message: %s (%s)", self, message, e
|
|
)
|
|
await self.sleep()
|
|
|
|
async def _read_content(
|
|
self, length: int, max_parts=1000, max_empties=200
|
|
) -> Optional[bytes]:
|
|
"""Read the full length of the message unless exceeding max_parts or
|
|
max_empties empty reads occur.
|
|
|
|
See https://github.com/jupyter-lsp/jupyterlab-lsp/issues/450
|
|
|
|
Crucial docs or read():
|
|
"If the argument is positive, and the underlying raw
|
|
stream is not interactive, multiple raw reads may be issued
|
|
to satisfy the byte count (unless EOF is reached first)"
|
|
|
|
Args:
|
|
- length: the content length
|
|
- max_parts: prevent absurdly long messages (1000 parts is several MBs):
|
|
1 part is usually sufficient but not enough for some long
|
|
messages 2 or 3 parts are often needed.
|
|
"""
|
|
raw = None
|
|
raw_parts: List[bytes] = []
|
|
received_size = 0
|
|
while received_size < length and len(raw_parts) < max_parts and max_empties > 0:
|
|
part = None
|
|
try:
|
|
part = self.stream.read(length - received_size)
|
|
except OSError: # pragma: no cover
|
|
pass
|
|
if part is None:
|
|
max_empties -= 1
|
|
await self.sleep()
|
|
continue
|
|
received_size += len(part)
|
|
raw_parts.append(part)
|
|
|
|
if raw_parts:
|
|
raw = b"".join(raw_parts)
|
|
if len(raw) != length: # pragma: no cover
|
|
self.log.warning(
|
|
f"Readout and content-length mismatch: {len(raw)} vs {length};"
|
|
f"remaining empties: {max_empties}; remaining parts: {max_parts}"
|
|
)
|
|
|
|
return raw
|
|
|
|
async def read_one(self) -> Text:
|
|
"""Read a single message"""
|
|
message = ""
|
|
headers = HTTPHeaders()
|
|
|
|
line = await convert_yielded(self._readline())
|
|
|
|
if line:
|
|
while line and line.strip():
|
|
headers.parse_line(line)
|
|
line = await convert_yielded(self._readline())
|
|
|
|
content_length = int(headers.get("content-length", "0"))
|
|
|
|
if content_length:
|
|
raw = await self._read_content(length=content_length)
|
|
if raw is not None:
|
|
message = raw.decode("utf-8").strip()
|
|
else: # pragma: no cover
|
|
self.log.warning(
|
|
"%s failed to read message of length %s",
|
|
self,
|
|
content_length,
|
|
)
|
|
|
|
return message
|
|
|
|
@run_on_executor
|
|
def _readline(self) -> Text:
|
|
"""Read a line (or immediately return None)"""
|
|
try:
|
|
return self.stream.readline().decode("utf-8").strip()
|
|
except OSError: # pragma: no cover
|
|
return ""
|
|
|
|
|
|
class LspStdIoWriter(LspStdIoBase):
|
|
"""Language Server stdio Writer"""
|
|
|
|
async def write(self) -> None:
|
|
"""Write to a Language Server until it closes"""
|
|
while not self.stream.closed:
|
|
message = await self.queue.get()
|
|
try:
|
|
body = message.encode("utf-8")
|
|
response = "Content-Length: {}\r\n\r\n{}".format(len(body), message)
|
|
await convert_yielded(self._write_one(response.encode("utf-8")))
|
|
except Exception: # pragma: no cover
|
|
self.log.exception("%s couldn't write message: %s", self, response)
|
|
finally:
|
|
self.queue.task_done()
|
|
|
|
@run_on_executor
|
|
def _write_one(self, message) -> None:
|
|
self.stream.write(message)
|
|
self.stream.flush()
|