Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add server status endpoint #81

Merged
merged 3 commits into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions py_src/jupyter_lsp/handlers.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
""" tornado handler for managing and communicating with language servers
"""
from typing import Optional, Text

from notebook.base.handlers import IPythonHandler
from notebook.base.zmqhandlers import WebSocketHandler, WebSocketMixin
from tornado.ioloop import IOLoop

from .manager import LanguageServerManager

class LanguageServerWebSocketHandler(WebSocketMixin, WebSocketHandler, IPythonHandler):
""" Setup tornado websocket to route to language server sessions
"""

language = None
manager = None
class BaseHandler(IPythonHandler):
manager = None # type: LanguageServerManager

def initialize(self, manager):
def initialize(self, manager: LanguageServerManager):
self.manager = manager


class LanguageServerWebSocketHandler(WebSocketMixin, WebSocketHandler, BaseHandler):
""" Setup tornado websocket to route to language server sessions
"""

language = None # type: Optional[Text]

def open(self, language):
self.language = language
self.manager.subscribe(self)
Expand All @@ -31,3 +38,23 @@ def send(message):
def on_close(self):
self.manager.unsubscribe(self)
self.log.debug("[{0: >16}] Closed a handler".format(self.language))


class LanguageServersHandler(BaseHandler):
""" Reports the status of all current servers

Response should conform to schema in schema/servers.schema.json
"""

def get(self):
""" finish with the JSON representations of the sessions
"""
self.finish(
{
"version": 0,
"sessions": sorted(
[session.to_json() for session in self.manager.sessions.values()],
key=lambda session: session["languages"],
),
}
)
14 changes: 14 additions & 0 deletions py_src/jupyter_lsp/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import json
import pathlib

import jsonschema

HERE = pathlib.Path(__file__).parent


def servers_schema() -> jsonschema.validators.Draft7Validator:
""" return a JSON Schema Draft 7 validator for the server status API
"""
return jsonschema.validators.Draft7Validator(
json.loads((HERE / "servers.schema.json").read_text())
)
68 changes: 68 additions & 0 deletions py_src/jupyter_lsp/schema/servers.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am experimenting with augmenting the statusbar with the server extension status; I was thinking about generating typescript interfaces from this schema e.g. with https://www.npmjs.com/package/json-schema-to-typescript. Any experiences or thoughts on this @bollwyvl?

"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/servers-response",
"title": "jupyter_lsp server status response",
"description": "describes the current state of (potentially) running language servers",
"definitions": {
"servers-response": {
"type": "object",
"properties": {
"sessions": {
"type": "array",
"description": "a list of servers that are, could be, or were running",
"items": { "$ref": "#/definitions/session" }
},
"version": {
"type": "integer",
"description": "the version of the schema",
"enum": [0]
}
},
"required": ["sessions", "version"]
},
"nullable-date-time": {
"description": "a date/time that might not have been recorded",
"oneOf": [{ "type": "string", "format": "date-time" }, { "type": "null" }]
},
"session": {
"title": "Language Server Session",
"description": "a language server session",
"additionalProperties": false,
"required": [
"languages",
"handler_count",
"status",
"last_server_message_at",
"last_handler_message_at"
],
"properties": {
"languages": {
"description": "languages supported by this Language Server",
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"minItems": 1
},
"handler_count": {
"title": "handler count",
"description": "the count of currently-connected WebSocket handlers",
"type": "integer",
"minValue": 0
},
"status": {
"description": "a string describing the current state of the server",
"type": "string",
"enum": ["not_started", "starting", "started", "stopping", "stopped"]
},
"last_server_message_at": {
"description": "date-time of last seen message from the language server",
"$ref": "#/definitions/nullable-date-time"
},
"last_handler_message_at": {
"description": "date-time of last seen message from a WebSocket handler",
"$ref": "#/definitions/nullable-date-time"
}
}
}
}
}
15 changes: 9 additions & 6 deletions py_src/jupyter_lsp/serverextension.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import traitlets
from notebook.utils import url_path_join as ujoin

from .handlers import LanguageServerWebSocketHandler
from .handlers import LanguageServersHandler, LanguageServerWebSocketHandler
from .manager import LanguageServerManager


Expand All @@ -20,13 +20,16 @@ def load_jupyter_server_extension(nbapp):
json.dumps(manager.language_servers, indent=2, sort_keys=True)
)
)

lsp_url = ujoin(nbapp.base_url, "lsp")
re_langs = "(?P<language>.*)"

opts = {"manager": nbapp.language_server_manager}

nbapp.web_app.add_handlers(
".*",
[
(
ujoin(nbapp.base_url, "lsp", "(?P<language>.*)"),
LanguageServerWebSocketHandler,
{"manager": nbapp.language_server_manager},
)
(lsp_url, LanguageServersHandler, opts),
(ujoin(lsp_url, re_langs), LanguageServerWebSocketHandler, opts),
],
)
33 changes: 32 additions & 1 deletion py_src/jupyter_lsp/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import asyncio
import atexit
import subprocess
from datetime import datetime, timezone

from tornado.queues import Queue
from tornado.websocket import WebSocketHandler
from traitlets import Bunch, Instance, List, Set, Unicode, observe
from traitlets import Bunch, Instance, List, Set, Unicode, UseEnum, observe
from traitlets.config import LoggingConfigurable

from . import stdio
from .types import SessionStatus


class LanguageServerSession(LoggingConfigurable):
Expand Down Expand Up @@ -42,6 +44,9 @@ class LanguageServerSession(LoggingConfigurable):
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

Expand All @@ -56,10 +61,24 @@ def __repr__(self): # pragma: no cover
self.languages, self.argv
)

def to_json(self):
return dict(
languages=self.languages,
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,
)

def initialize(self):
""" (re)initialize a language server session
"""
self.stop()
self.status = SessionStatus.STARTING
self.init_queues()
self.init_process()
self.init_writer()
Expand All @@ -71,9 +90,14 @@ def initialize(self):
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
Expand All @@ -87,6 +111,8 @@ def stop(self):
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
Expand All @@ -99,8 +125,12 @@ def _on_handlers(self, change: Bunch):
def write(self, message):
""" wrapper around the write queue to keep it mostly internal
"""
self.last_handler_message_at = self.now()
self.to_lsp.put_nowait(message)

def now(self):
return datetime.now(timezone.utc)

def init_process(self):
""" start the language server subprocess
"""
Expand Down Expand Up @@ -139,6 +169,7 @@ async def _broadcast_from_lsp(self):
server
"""
async for msg in self.from_lsp:
self.last_server_message_at = self.now()
for handler in self.handlers:
handler.write_message(msg)
self.from_lsp.task_done()
23 changes: 19 additions & 4 deletions py_src/jupyter_lsp/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

# local imports
from jupyter_lsp import LanguageServerManager
from jupyter_lsp.handlers import LanguageServerWebSocketHandler
from jupyter_lsp.handlers import LanguageServersHandler, LanguageServerWebSocketHandler
from jupyter_lsp.schema import servers_schema

# these should always be available in a test environment ()
KNOWN_LANGUAGES = [
Expand Down Expand Up @@ -38,6 +39,8 @@

KNOWN_UNKNOWN_LANGUAGES = ["cobol"]

SERVERS_SCHEMA = servers_schema()


@fixture
def manager() -> LanguageServerManager:
Expand All @@ -55,10 +58,12 @@ def known_unknown_language(request):


@fixture
def handler(manager):
handler = MockWebsocketHandler()
def handlers(manager):
ws_handler = MockWebsocketHandler()
ws_handler.initialize(manager)
handler = MockHandler()
handler.initialize(manager)
return handler
return handler, ws_handler


@fixture
Expand Down Expand Up @@ -100,5 +105,15 @@ def write_message(self, message: Text) -> None:
self._messages_wrote.put_nowait(message)


class MockHandler(LanguageServersHandler):
_payload = None

def __init__(self):
pass

def finish(self, payload):
self._payload = payload


class MockNotebookApp(NotebookApp):
pass
Loading