Skip to content

Commit 99da868

Browse files
authored
Merge pull request #81 from bollwyvl/add-server-status-endpoint
Add server status endpoint
2 parents 21ff0cc + bf00df3 commit 99da868

File tree

8 files changed

+235
-31
lines changed

8 files changed

+235
-31
lines changed

py_src/jupyter_lsp/handlers.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
""" tornado handler for managing and communicating with language servers
22
"""
3+
from typing import Optional, Text
4+
35
from notebook.base.handlers import IPythonHandler
46
from notebook.base.zmqhandlers import WebSocketHandler, WebSocketMixin
57
from tornado.ioloop import IOLoop
68

9+
from .manager import LanguageServerManager
710

8-
class LanguageServerWebSocketHandler(WebSocketMixin, WebSocketHandler, IPythonHandler):
9-
""" Setup tornado websocket to route to language server sessions
10-
"""
1111

12-
language = None
13-
manager = None
12+
class BaseHandler(IPythonHandler):
13+
manager = None # type: LanguageServerManager
1414

15-
def initialize(self, manager):
15+
def initialize(self, manager: LanguageServerManager):
1616
self.manager = manager
1717

18+
19+
class LanguageServerWebSocketHandler(WebSocketMixin, WebSocketHandler, BaseHandler):
20+
""" Setup tornado websocket to route to language server sessions
21+
"""
22+
23+
language = None # type: Optional[Text]
24+
1825
def open(self, language):
1926
self.language = language
2027
self.manager.subscribe(self)
@@ -31,3 +38,23 @@ def send(message):
3138
def on_close(self):
3239
self.manager.unsubscribe(self)
3340
self.log.debug("[{0: >16}] Closed a handler".format(self.language))
41+
42+
43+
class LanguageServersHandler(BaseHandler):
44+
""" Reports the status of all current servers
45+
46+
Response should conform to schema in schema/servers.schema.json
47+
"""
48+
49+
def get(self):
50+
""" finish with the JSON representations of the sessions
51+
"""
52+
self.finish(
53+
{
54+
"version": 0,
55+
"sessions": sorted(
56+
[session.to_json() for session in self.manager.sessions.values()],
57+
key=lambda session: session["languages"],
58+
),
59+
}
60+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import json
2+
import pathlib
3+
4+
import jsonschema
5+
6+
HERE = pathlib.Path(__file__).parent
7+
8+
9+
def servers_schema() -> jsonschema.validators.Draft7Validator:
10+
""" return a JSON Schema Draft 7 validator for the server status API
11+
"""
12+
return jsonschema.validators.Draft7Validator(
13+
json.loads((HERE / "servers.schema.json").read_text())
14+
)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "#/definitions/servers-response",
4+
"title": "jupyter_lsp server status response",
5+
"description": "describes the current state of (potentially) running language servers",
6+
"definitions": {
7+
"servers-response": {
8+
"type": "object",
9+
"properties": {
10+
"sessions": {
11+
"type": "array",
12+
"description": "a list of servers that are, could be, or were running",
13+
"items": { "$ref": "#/definitions/session" }
14+
},
15+
"version": {
16+
"type": "integer",
17+
"description": "the version of the schema",
18+
"enum": [0]
19+
}
20+
},
21+
"required": ["sessions", "version"]
22+
},
23+
"nullable-date-time": {
24+
"description": "a date/time that might not have been recorded",
25+
"oneOf": [{ "type": "string", "format": "date-time" }, { "type": "null" }]
26+
},
27+
"session": {
28+
"title": "Language Server Session",
29+
"description": "a language server session",
30+
"additionalProperties": false,
31+
"required": [
32+
"languages",
33+
"handler_count",
34+
"status",
35+
"last_server_message_at",
36+
"last_handler_message_at"
37+
],
38+
"properties": {
39+
"languages": {
40+
"description": "languages supported by this Language Server",
41+
"type": "array",
42+
"items": { "type": "string" },
43+
"uniqueItems": true,
44+
"minItems": 1
45+
},
46+
"handler_count": {
47+
"title": "handler count",
48+
"description": "the count of currently-connected WebSocket handlers",
49+
"type": "integer",
50+
"minValue": 0
51+
},
52+
"status": {
53+
"description": "a string describing the current state of the server",
54+
"type": "string",
55+
"enum": ["not_started", "starting", "started", "stopping", "stopped"]
56+
},
57+
"last_server_message_at": {
58+
"description": "date-time of last seen message from the language server",
59+
"$ref": "#/definitions/nullable-date-time"
60+
},
61+
"last_handler_message_at": {
62+
"description": "date-time of last seen message from a WebSocket handler",
63+
"$ref": "#/definitions/nullable-date-time"
64+
}
65+
}
66+
}
67+
}
68+
}

py_src/jupyter_lsp/serverextension.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import traitlets
66
from notebook.utils import url_path_join as ujoin
77

8-
from .handlers import LanguageServerWebSocketHandler
8+
from .handlers import LanguageServersHandler, LanguageServerWebSocketHandler
99
from .manager import LanguageServerManager
1010

1111

@@ -20,13 +20,16 @@ def load_jupyter_server_extension(nbapp):
2020
json.dumps(manager.language_servers, indent=2, sort_keys=True)
2121
)
2222
)
23+
24+
lsp_url = ujoin(nbapp.base_url, "lsp")
25+
re_langs = "(?P<language>.*)"
26+
27+
opts = {"manager": nbapp.language_server_manager}
28+
2329
nbapp.web_app.add_handlers(
2430
".*",
2531
[
26-
(
27-
ujoin(nbapp.base_url, "lsp", "(?P<language>.*)"),
28-
LanguageServerWebSocketHandler,
29-
{"manager": nbapp.language_server_manager},
30-
)
32+
(lsp_url, LanguageServersHandler, opts),
33+
(ujoin(lsp_url, re_langs), LanguageServerWebSocketHandler, opts),
3134
],
3235
)

py_src/jupyter_lsp/session.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import asyncio
44
import atexit
55
import subprocess
6+
from datetime import datetime, timezone
67

78
from tornado.queues import Queue
89
from tornado.websocket import WebSocketHandler
9-
from traitlets import Bunch, Instance, List, Set, Unicode, observe
10+
from traitlets import Bunch, Instance, List, Set, Unicode, UseEnum, observe
1011
from traitlets.config import LoggingConfigurable
1112

1213
from . import stdio
14+
from .types import SessionStatus
1315

1416

1517
class LanguageServerSession(LoggingConfigurable):
@@ -42,6 +44,9 @@ class LanguageServerSession(LoggingConfigurable):
4244
default_value=[],
4345
help="the currently subscribed websockets",
4446
)
47+
status = UseEnum(SessionStatus, default_value=SessionStatus.NOT_STARTED)
48+
last_handler_message_at = Instance(datetime, allow_none=True)
49+
last_server_message_at = Instance(datetime, allow_none=True)
4550

4651
_tasks = None
4752

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

64+
def to_json(self):
65+
return dict(
66+
languages=self.languages,
67+
handler_count=len(self.handlers),
68+
status=self.status.value,
69+
last_server_message_at=self.last_server_message_at.isoformat()
70+
if self.last_server_message_at
71+
else None,
72+
last_handler_message_at=self.last_handler_message_at.isoformat()
73+
if self.last_handler_message_at
74+
else None,
75+
)
76+
5977
def initialize(self):
6078
""" (re)initialize a language server session
6179
"""
6280
self.stop()
81+
self.status = SessionStatus.STARTING
6382
self.init_queues()
6483
self.init_process()
6584
self.init_writer()
@@ -71,9 +90,14 @@ def initialize(self):
7190
for coro in [self._read_lsp, self._write_lsp, self._broadcast_from_lsp]
7291
]
7392

93+
self.status = SessionStatus.STARTED
94+
7495
def stop(self):
7596
""" clean up all of the state of the session
7697
"""
98+
99+
self.status = SessionStatus.STOPPING
100+
77101
if self.process:
78102
self.process.terminate()
79103
self.process = None
@@ -87,6 +111,8 @@ def stop(self):
87111
if self._tasks:
88112
[task.cancel() for task in self._tasks]
89113

114+
self.status = SessionStatus.STOPPED
115+
90116
@observe("handlers")
91117
def _on_handlers(self, change: Bunch):
92118
""" re-initialize if someone starts listening, or stop if nobody is
@@ -99,8 +125,12 @@ def _on_handlers(self, change: Bunch):
99125
def write(self, message):
100126
""" wrapper around the write queue to keep it mostly internal
101127
"""
128+
self.last_handler_message_at = self.now()
102129
self.to_lsp.put_nowait(message)
103130

131+
def now(self):
132+
return datetime.now(timezone.utc)
133+
104134
def init_process(self):
105135
""" start the language server subprocess
106136
"""
@@ -139,6 +169,7 @@ async def _broadcast_from_lsp(self):
139169
server
140170
"""
141171
async for msg in self.from_lsp:
172+
self.last_server_message_at = self.now()
142173
for handler in self.handlers:
143174
handler.write_message(msg)
144175
self.from_lsp.task_done()

py_src/jupyter_lsp/tests/conftest.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
# local imports
1111
from jupyter_lsp import LanguageServerManager
12-
from jupyter_lsp.handlers import LanguageServerWebSocketHandler
12+
from jupyter_lsp.handlers import LanguageServersHandler, LanguageServerWebSocketHandler
13+
from jupyter_lsp.schema import servers_schema
1314

1415
# these should always be available in a test environment ()
1516
KNOWN_LANGUAGES = [
@@ -38,6 +39,8 @@
3839

3940
KNOWN_UNKNOWN_LANGUAGES = ["cobol"]
4041

42+
SERVERS_SCHEMA = servers_schema()
43+
4144

4245
@fixture
4346
def manager() -> LanguageServerManager:
@@ -55,10 +58,12 @@ def known_unknown_language(request):
5558

5659

5760
@fixture
58-
def handler(manager):
59-
handler = MockWebsocketHandler()
61+
def handlers(manager):
62+
ws_handler = MockWebsocketHandler()
63+
ws_handler.initialize(manager)
64+
handler = MockHandler()
6065
handler.initialize(manager)
61-
return handler
66+
return handler, ws_handler
6267

6368

6469
@fixture
@@ -100,5 +105,15 @@ def write_message(self, message: Text) -> None:
100105
self._messages_wrote.put_nowait(message)
101106

102107

108+
class MockHandler(LanguageServersHandler):
109+
_payload = None
110+
111+
def __init__(self):
112+
pass
113+
114+
def finish(self, payload):
115+
self._payload = payload
116+
117+
103118
class MockNotebookApp(NotebookApp):
104119
pass

0 commit comments

Comments
 (0)