Skip to content

Commit 453f363

Browse files
seeMwesm
authored andcommitted
Merged PR posit-dev/positron-python#109: improve kernel and language server shutdown behaviour
Merge pull request #109 from posit-dev/improve-shutdown-behaviour improve kernel and language server shutdown behaviour -------------------- Commit message for posit-dev/positron-python@7750de1: black formatting -------------------- Commit message for posit-dev/positron-python@1efb00d: improve kernel and language server shutdown behaviour Authored-by: Wasim Lorgat <[email protected]> Signed-off-by: Wasim Lorgat <[email protected]>
1 parent 9e90676 commit 453f363

File tree

3 files changed

+96
-42
lines changed

3 files changed

+96
-42
lines changed

extensions/positron-python/pythonFiles/positron/positron_ipkernel.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@
3131
POSITRON_PLOT_COMM = "positron.plot"
3232
"""The comm channel target_name for Positron's Plots View"""
3333

34-
POSITON_NS_HIDDEN = {"_exit_code": {}, "__pydevd_ret_val_dict": {}, "__warningregistry__": {}}
34+
POSITON_NS_HIDDEN = {
35+
"_exit_code": {},
36+
"__pydevd_ret_val_dict": {},
37+
"__warningregistry__": {},
38+
}
3539
"""Additional variables to hide from the user's namespace."""
3640

3741
# Key used to store the user's environment snapshot in the hidden namespace
3842
__POSITRON_CACHE_KEY__ = "__positron_cache__"
3943

44+
logger = logging.getLogger(__name__)
45+
4046

4147
class PositronIPyKernel(IPythonKernel):
4248
"""
@@ -75,12 +81,16 @@ def do_shutdown(self, restart) -> dict:
7581
"""
7682
Handle kernel shutdown.
7783
"""
78-
result = super().do_shutdown(restart)
84+
logger.info("Shutting down the kernel")
7985
self.display_pub_hook.shutdown()
8086
self.env_service.shutdown()
8187
self.lsp_service.shutdown()
8288
self.dataviewer_service.shutdown()
83-
return result
89+
90+
# We don't call super().do_shutdown since it sets shell.exit_now = True which tries to
91+
# stop the event loop at the same time as self.shutdown_request (since self.shell_stream.io_loop
92+
# points to the same underlying asyncio loop).
93+
return dict(status="ok", restart=restart)
8494

8595
def handle_pre_execute(self) -> None:
8696
"""

extensions/positron-python/pythonFiles/positron/positron_jedilsp.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import os
66
import sys
77

8+
from pygls.protocol import lsp_method
9+
810
# Add the lib path to our sys path so jedi_language_server can find its references
911
EXTENSION_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1012
sys.path.insert(0, os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "jedilsp"))
@@ -38,6 +40,7 @@
3840
)
3941
from lsprotocol.types import (
4042
COMPLETION_ITEM_RESOLVE,
43+
EXIT,
4144
TEXT_DOCUMENT_CODE_ACTION,
4245
TEXT_DOCUMENT_COMPLETION,
4346
TEXT_DOCUMENT_DEFINITION,
@@ -87,6 +90,34 @@
8790
from .positron_ipkernel import PositronIPyKernel
8891

8992

93+
logger = logging.getLogger(__name__)
94+
95+
96+
class PositronJediLanguageServerProtocol(JediLanguageServerProtocol):
97+
"""Positron extension to the Jedi language server protocol."""
98+
99+
@lsp_method(EXIT)
100+
def lsp_exit(self, *args) -> None:
101+
"""Override superclass to avoid system exit."""
102+
if self.transport is not None:
103+
self.transport.close()
104+
105+
# --- Start Positron
106+
# Instead of calling sys.exit (which pygls catches to call self.shutdown) call shutdown directly.
107+
asyncio.create_task(POSITRON.shutdown())
108+
# --- End Positron
109+
110+
def connection_lost(self, exc):
111+
"""Override superclass to avoid system exit."""
112+
logger.info("Connection lost")
113+
if exc is not None:
114+
logger.error("Connection lost with error:", exc_info=exc)
115+
116+
def eof_received(self):
117+
"""Override superclass to avoid closing on eof (e.g. when the Positron browser is refreshed)."""
118+
return True
119+
120+
90121
class PositronJediLanguageServer(JediLanguageServer):
91122
"""Positron extension to the Jedi language server."""
92123

@@ -121,11 +152,7 @@ def start(self, lsp_host: str, lsp_port: int, kernel: "PositronIPyKernel") -> No
121152
completions with awareness of live variables from user's namespace.
122153
"""
123154
self.kernel = kernel
124-
125-
try:
126-
asyncio.ensure_future(self._start_jedi(lsp_host, lsp_port))
127-
except KeyboardInterrupt:
128-
pass
155+
asyncio.create_task(self._start_jedi(lsp_host, lsp_port))
129156

130157
async def _start_jedi(self, lsp_host, lsp_port):
131158
"""
@@ -134,15 +161,36 @@ async def _start_jedi(self, lsp_host, lsp_port):
134161
Adapted from `pygls.server.Server.start_tcp` to use existing asyncio loop.
135162
"""
136163
self._stop_event = Event()
164+
self.lsp: PositronJediLanguageServerProtocol
137165
loop = asyncio.get_event_loop()
138-
self._server = await loop.create_server(self.lsp, lsp_host, lsp_port) # type: ignore
139-
await self._server.serve_forever()
166+
self._server = await loop.create_server(self.lsp, lsp_host, lsp_port)
167+
168+
async def shutdown(self):
169+
"""Override superclass to allow running async and avoid stopping the event loop."""
170+
logger.info("Shutting down the language server")
171+
172+
self._stop_event.set()
173+
174+
if self._thread_pool:
175+
self._thread_pool.terminate()
176+
self._thread_pool.join()
177+
178+
if self._thread_pool_executor:
179+
self._thread_pool_executor.shutdown()
180+
181+
if self._server:
182+
self._server.close()
183+
# --- Start Positron
184+
await self._server.wait_closed()
185+
# --- End Positron
186+
187+
logger.info("Language server is shut down")
140188

141189

142190
POSITRON = PositronJediLanguageServer(
143191
name="jedi-language-server",
144192
version="0.18.2",
145-
protocol_cls=JediLanguageServerProtocol,
193+
protocol_cls=PositronJediLanguageServerProtocol,
146194
)
147195

148196
# Server Features
@@ -177,7 +225,7 @@ def positron_completion(
177225
ns = server.kernel.get_user_ns()
178226
namespaces.append(ns)
179227

180-
# Use Interpreter() to include the kernel namespaces in completions
228+
# Use Interpreter instead of Script to include the kernel namespaces in completions
181229
jedi_script = Interpreter(
182230
document.source, namespaces, path=document.path, project=server.project
183231
)

extensions/positron-python/pythonFiles/positron_language_server.py

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import logging
99
import os
1010
import sys
11-
import traceback
1211

1312
from ipykernel import kernelapp
1413

@@ -19,6 +18,8 @@
1918

2019
from positron.positron_ipkernel import PositronIPyKernel
2120

21+
logger = logging.getLogger(__name__)
22+
2223

2324
def initialize_config() -> None:
2425
"""
@@ -85,39 +86,34 @@ def initialize_config() -> None:
8586
logging.warning(f"Unable to start debugpy: {error}", exc_info=True)
8687

8788

88-
async def start_ipykernel() -> None:
89-
"""Starts Positron's IPyKernel as the interpreter for our console."""
89+
if __name__ == "__main__":
90+
exit_status = 0
91+
92+
# Init the configuration args
93+
initialize_config()
94+
95+
# Start Positron's IPyKernel as the interpreter for our console.
9096
app = kernelapp.IPKernelApp.instance(kernel_class=PositronIPyKernel)
9197
app.initialize()
9298
app.kernel.start()
9399

100+
logger.info(f"Process ID {os.getpid()}")
94101

95-
if __name__ == "__main__":
96-
exitStatus = 0
102+
# IPyKernel uses Tornado which (as of version 5.0) shares the same event
103+
# loop as asyncio.
104+
loop = asyncio.get_event_loop()
97105

98-
try:
99-
# Init the configuration args
100-
initialize_config()
106+
# Enable asyncio debug mode.
107+
if logging.getLogger().level == logging.DEBUG:
108+
loop.set_debug(True)
101109

102-
# Start Positron's IPyKernel as the interpreter for our console.
103-
loop = asyncio.get_event_loop()
104-
try:
105-
asyncio.ensure_future(start_ipykernel())
106-
loop.run_forever()
107-
except KeyboardInterrupt:
108-
pass
109-
finally:
110-
loop.close()
111-
112-
except SystemExit as error:
113-
# TODO: Remove this workaround once we can improve Jedi
114-
# disconnection logic
115-
tb = "".join(traceback.format_tb(error.__traceback__))
116-
if tb.find("connection_lost") > 0:
117-
logging.warning("Positron Language Server client disconnected, exiting.")
118-
exitStatus = 0
119-
else:
120-
logging.error("Error in Positron Language Server: %s", error)
121-
exitStatus = 1
122-
123-
sys.exit(exitStatus)
110+
try:
111+
loop.run_forever()
112+
except (KeyboardInterrupt, SystemExit):
113+
logger.exception("Unexpected exception in event loop")
114+
exit_status = 1
115+
finally:
116+
loop.close()
117+
118+
logger.info(f"Exiting process with status {exit_status}")
119+
sys.exit(exit_status)

0 commit comments

Comments
 (0)