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.
		
		
		
		
		
			
		
			
				
	
	
		
			184 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			184 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
""" A session for managing a language server process
 | 
						|
"""
 | 
						|
 | 
						|
import asyncio
 | 
						|
import atexit
 | 
						|
import os
 | 
						|
import string
 | 
						|
import subprocess
 | 
						|
from datetime import datetime, timezone
 | 
						|
 | 
						|
from tornado.ioloop import IOLoop
 | 
						|
from tornado.queues import Queue
 | 
						|
from tornado.websocket import WebSocketHandler
 | 
						|
from traitlets import Bunch, Instance, Set, Unicode, UseEnum, observe
 | 
						|
from traitlets.config import LoggingConfigurable
 | 
						|
 | 
						|
from . import stdio
 | 
						|
from .schema import LANGUAGE_SERVER_SPEC
 | 
						|
from .specs.utils import censored_spec
 | 
						|
from .trait_types import Schema
 | 
						|
from .types import SessionStatus
 | 
						|
 | 
						|
 | 
						|
class LanguageServerSession(LoggingConfigurable):
 | 
						|
    """Manage a session for a connection to a language server"""
 | 
						|
 | 
						|
    language_server = Unicode(help="the language server implementation name")
 | 
						|
    spec = Schema(LANGUAGE_SERVER_SPEC)
 | 
						|
 | 
						|
    # run-time specifics
 | 
						|
    process = Instance(
 | 
						|
        subprocess.Popen, help="the language server subprocess", allow_none=True
 | 
						|
    )
 | 
						|
    writer = Instance(stdio.LspStdIoWriter, help="the JSON-RPC writer", allow_none=True)
 | 
						|
    reader = Instance(stdio.LspStdIoReader, help="the JSON-RPC reader", allow_none=True)
 | 
						|
    from_lsp = Instance(
 | 
						|
        Queue, help="a queue for string messages from the server", allow_none=True
 | 
						|
    )
 | 
						|
    to_lsp = Instance(
 | 
						|
        Queue, help="a queue for string message to the server", allow_none=True
 | 
						|
    )
 | 
						|
    handlers = Set(
 | 
						|
        trait=Instance(WebSocketHandler),
 | 
						|
        default_value=[],
 | 
						|
        help="the currently subscribed websockets",
 | 
						|
    )
 | 
						|
    status = UseEnum(SessionStatus, default_value=SessionStatus.NOT_STARTED)
 | 
						|
    last_handler_message_at = Instance(datetime, allow_none=True)
 | 
						|
    last_server_message_at = Instance(datetime, allow_none=True)
 | 
						|
 | 
						|
    _tasks = None
 | 
						|
 | 
						|
    _skip_serialize = ["argv", "debug_argv"]
 | 
						|
 | 
						|
    def __init__(self, *args, **kwargs):
 | 
						|
        """set up the required traitlets and exit behavior for a session"""
 | 
						|
        super().__init__(*args, **kwargs)
 | 
						|
        atexit.register(self.stop)
 | 
						|
 | 
						|
    def __repr__(self):  # pragma: no cover
 | 
						|
        return (
 | 
						|
            "<LanguageServerSession(" "language_server={language_server}, argv={argv})>"
 | 
						|
        ).format(language_server=self.language_server, **self.spec)
 | 
						|
 | 
						|
    def to_json(self):
 | 
						|
        return dict(
 | 
						|
            handler_count=len(self.handlers),
 | 
						|
            status=self.status.value,
 | 
						|
            last_server_message_at=(
 | 
						|
                self.last_server_message_at.isoformat()
 | 
						|
                if self.last_server_message_at
 | 
						|
                else None
 | 
						|
            ),
 | 
						|
            last_handler_message_at=(
 | 
						|
                self.last_handler_message_at.isoformat()
 | 
						|
                if self.last_handler_message_at
 | 
						|
                else None
 | 
						|
            ),
 | 
						|
            spec=censored_spec(self.spec),
 | 
						|
        )
 | 
						|
 | 
						|
    def initialize(self):
 | 
						|
        """(re)initialize a language server session"""
 | 
						|
        self.stop()
 | 
						|
        self.status = SessionStatus.STARTING
 | 
						|
        self.init_queues()
 | 
						|
        self.init_process()
 | 
						|
        self.init_writer()
 | 
						|
        self.init_reader()
 | 
						|
 | 
						|
        loop = asyncio.get_event_loop()
 | 
						|
        self._tasks = [
 | 
						|
            loop.create_task(coro())
 | 
						|
            for coro in [self._read_lsp, self._write_lsp, self._broadcast_from_lsp]
 | 
						|
        ]
 | 
						|
 | 
						|
        self.status = SessionStatus.STARTED
 | 
						|
 | 
						|
    def stop(self):
 | 
						|
        """clean up all of the state of the session"""
 | 
						|
 | 
						|
        self.status = SessionStatus.STOPPING
 | 
						|
 | 
						|
        if self.process:
 | 
						|
            self.process.terminate()
 | 
						|
            self.process = None
 | 
						|
        if self.reader:
 | 
						|
            self.reader.close()
 | 
						|
            self.reader = None
 | 
						|
        if self.writer:
 | 
						|
            self.writer.close()
 | 
						|
            self.writer = None
 | 
						|
 | 
						|
        if self._tasks:
 | 
						|
            [task.cancel() for task in self._tasks]
 | 
						|
 | 
						|
        self.status = SessionStatus.STOPPED
 | 
						|
 | 
						|
    @observe("handlers")
 | 
						|
    def _on_handlers(self, change: Bunch):
 | 
						|
        """re-initialize if someone starts listening, or stop if nobody is"""
 | 
						|
        if change["new"] and not self.process:
 | 
						|
            self.initialize()
 | 
						|
        elif not change["new"] and self.process:
 | 
						|
            self.stop()
 | 
						|
 | 
						|
    def write(self, message):
 | 
						|
        """wrapper around the write queue to keep it mostly internal"""
 | 
						|
        self.last_handler_message_at = self.now()
 | 
						|
        IOLoop.current().add_callback(self.to_lsp.put_nowait, message)
 | 
						|
 | 
						|
    def now(self):
 | 
						|
        return datetime.now(timezone.utc)
 | 
						|
 | 
						|
    def init_process(self):
 | 
						|
        """start the language server subprocess"""
 | 
						|
        self.process = subprocess.Popen(
 | 
						|
            self.spec["argv"],
 | 
						|
            stdin=subprocess.PIPE,
 | 
						|
            stdout=subprocess.PIPE,
 | 
						|
            env=self.substitute_env(self.spec.get("env", {}), os.environ),
 | 
						|
            bufsize=0,
 | 
						|
        )
 | 
						|
 | 
						|
    def init_queues(self):
 | 
						|
        """create the queues"""
 | 
						|
        self.from_lsp = Queue()
 | 
						|
        self.to_lsp = Queue()
 | 
						|
 | 
						|
    def init_reader(self):
 | 
						|
        """create the stdout reader (from the language server)"""
 | 
						|
        self.reader = stdio.LspStdIoReader(
 | 
						|
            stream=self.process.stdout, queue=self.from_lsp, parent=self
 | 
						|
        )
 | 
						|
 | 
						|
    def init_writer(self):
 | 
						|
        """create the stdin writer (to the language server)"""
 | 
						|
        self.writer = stdio.LspStdIoWriter(
 | 
						|
            stream=self.process.stdin, queue=self.to_lsp, parent=self
 | 
						|
        )
 | 
						|
 | 
						|
    def substitute_env(self, env, base):
 | 
						|
        final_env = base.copy()
 | 
						|
 | 
						|
        for key, value in env.items():
 | 
						|
            final_env.update({key: string.Template(value).safe_substitute(base)})
 | 
						|
 | 
						|
        return final_env
 | 
						|
 | 
						|
    async def _read_lsp(self):
 | 
						|
        await self.reader.read()
 | 
						|
 | 
						|
    async def _write_lsp(self):
 | 
						|
        await self.writer.write()
 | 
						|
 | 
						|
    async def _broadcast_from_lsp(self):
 | 
						|
        """loop for reading messages from the queue of messages from the language
 | 
						|
        server
 | 
						|
        """
 | 
						|
        async for message in self.from_lsp:
 | 
						|
            self.last_server_message_at = self.now()
 | 
						|
            await self.parent.on_server_message(message, self)
 | 
						|
            self.from_lsp.task_done()
 |