From b89143d85fbe1df030e41f340e550eb2a1ae4495 Mon Sep 17 00:00:00 2001 From: Grotto Assistant Date: Wed, 4 Mar 2026 17:00:55 +0000 Subject: [PATCH 1/5] fix: close websocket cleanly on session id mismatch When a session ID mismatch occurs during kernel reuse, the code was sending a plain text error message via websocket.send_text(). However, the JupyterLab kernel protocol expects all WebSocket messages to be JSON. This caused a SyntaxError in the frontend when JSON.parse() tried to parse the plain text. The fix replaces send_text() with close() to cleanly close the WebSocket connection without sending a malformed message. The error is already logged on the server side, and the client will handle the WebSocket close gracefully. --- solara/server/kernel_context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solara/server/kernel_context.py b/solara/server/kernel_context.py index a5a7a90b2..bb8ac9121 100644 --- a/solara/server/kernel_context.py +++ b/solara/server/kernel_context.py @@ -475,7 +475,10 @@ def initialize_virtual_kernel(session_id: str, kernel_id: str, websocket: websoc context = contexts[kernel_id] if context.session_id != session_id: logger.critical("Session id mismatch when reusing kernel (hack attempt?): %s != %s", context.session_id, session_id) - websocket.send_text("Session id mismatch when reusing kernel (hack attempt?)") + # Close the websocket cleanly instead of sending plain text. + # The JupyterLab kernel protocol expects all messages to be JSON, + # so sending plain text causes a parse error on the client. + websocket.close() # to avoid very fast reconnects (we are in a thread anyway) time.sleep(0.5) raise ValueError("Session id mismatch") From 83c9eb3923e1df7ae6908db732ca4e7afc1e302f Mon Sep 17 00:00:00 2001 From: Grotto Assistant Date: Tue, 21 Apr 2026 09:14:06 +0000 Subject: [PATCH 2/5] fix: send solara kernel termination message --- packages/solara-widget-manager/src/kernel.ts | 17 +++ solara/server/kernel.py | 107 ++++++++++++++----- solara/server/kernel_context.py | 14 ++- tests/unit/lifecycle_test.py | 24 +++++ 4 files changed, 133 insertions(+), 29 deletions(-) diff --git a/packages/solara-widget-manager/src/kernel.ts b/packages/solara-widget-manager/src/kernel.ts index 49f5977a4..0839bd412 100644 --- a/packages/solara-widget-manager/src/kernel.ts +++ b/packages/solara-widget-manager/src/kernel.ts @@ -12,7 +12,23 @@ import { Kernel, ServerConnection } from '@jupyterlab/services'; import { KernelConnection } from '@jupyterlab/services/lib/kernel/default'; import * as KernelMessage from '@jupyterlab/services/lib/kernel/messages'; +const SOLARA_KERNEL_TERMINATED_MSG_TYPE = 'solara_kernel_terminated'; +function installSolaraKernelMessageLogging(kernel: KernelConnection) { + kernel.iopubMessage.connect((_, msg) => { + if (msg.header.msg_type !== SOLARA_KERNEL_TERMINATED_MSG_TYPE) { + return; + } + console.error('Solara kernel terminated:', msg.content); + if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') { + window.dispatchEvent( + new CustomEvent('solara.kernelTerminated', { + detail: msg.content, + }) + ); + } + }); +} export async function connectKernel( baseUrl?: string, @@ -29,6 +45,7 @@ export async function connectKernel( // } const model = { 'id': kernelId, 'name': 'solara-name' } const kernel = new KernelConnection({ model, serverSettings }); + installSolaraKernelMessageLogging(kernel); return kernel; } diff --git a/solara/server/kernel.py b/solara/server/kernel.py index 09420a9cc..76f047b80 100644 --- a/solara/server/kernel.py +++ b/solara/server/kernel.py @@ -250,7 +250,7 @@ def close(self): except: # noqa pass - def send( + def _wire_message( self, stream, msg_or_type, @@ -263,32 +263,87 @@ def send( metadata=None, ): if stream is None: - return # can happen when the kernel is closed but someone was still trying to send a message + return None # can happen when the kernel is closed but someone was still trying to send a message + if isinstance(msg_or_type, dict): + msg = msg_or_type + else: + msg = self.msg( + msg_or_type, + content=content, + parent=parent, + header=header, + metadata=metadata, + ) + _fix_msg(msg) + msg["channel"] = stream.channel try: - if isinstance(msg_or_type, dict): - msg = msg_or_type - else: - msg = self.msg( - msg_or_type, - content=content, - parent=parent, - header=header, - metadata=metadata, - ) - _fix_msg(msg) - msg["channel"] = stream.channel - # not using pdb guard for performance reasons - try: - if buffers: - msg["buffers"] = [memoryview(k).cast("b") for k in buffers] - wire_message = serialize_binary_message(msg) - else: - wire_message = json_dumps(msg) - except Exception: - logger.exception("Could not serialize message: %r", msg) - if settings.main.use_pdb: - pdb.post_mortem() - raise + if buffers: + msg["buffers"] = [memoryview(k).cast("b") for k in buffers] + return serialize_binary_message(msg) + return json_dumps(msg) + except Exception: + logger.exception("Could not serialize message: %r", msg) + if settings.main.use_pdb: + pdb.post_mortem() + raise + + def send_to( + self, + websocket_target: websocket.WebsocketWrapper, + stream, + msg_or_type, + content=None, + parent=None, + ident=None, + buffers=None, + track=False, + header=None, + metadata=None, + ): + try: + wire_message = self._wire_message( + stream, + msg_or_type, + content=content, + parent=parent, + ident=ident, + buffers=buffers, + track=track, + header=header, + metadata=metadata, + ) + if wire_message is None: + return + send_websockets({websocket_target}, wire_message) + except Exception as e: + logger.exception("Error sending message: %s", e) + + def send( + self, + stream, + msg_or_type, + content=None, + parent=None, + ident=None, + buffers=None, + track=False, + header=None, + metadata=None, + ): + try: + wire_message = self._wire_message( + stream, + msg_or_type, + content=content, + parent=parent, + ident=ident, + buffers=buffers, + track=track, + header=header, + metadata=metadata, + ) + if wire_message is None: + return send_websockets(self.websockets, wire_message) except Exception as e: logger.exception("Error sending message: %s", e) diff --git a/solara/server/kernel_context.py b/solara/server/kernel_context.py index bb8ac9121..98d0dedbd 100644 --- a/solara/server/kernel_context.py +++ b/solara/server/kernel_context.py @@ -475,9 +475,17 @@ def initialize_virtual_kernel(session_id: str, kernel_id: str, websocket: websoc context = contexts[kernel_id] if context.session_id != session_id: logger.critical("Session id mismatch when reusing kernel (hack attempt?): %s != %s", context.session_id, session_id) - # Close the websocket cleanly instead of sending plain text. - # The JupyterLab kernel protocol expects all messages to be JSON, - # so sending plain text causes a parse error on the client. + context.kernel.session.send_to( + websocket, + context.kernel.iopub_socket, + "solara_kernel_terminated", + { + "reason": "session_id_mismatch", + "message": "Rejected websocket connection for existing virtual kernel because the session id did not match.", + "kernel_id": kernel_id, + "retryable": False, + }, + ) websocket.close() # to avoid very fast reconnects (we are in a thread anyway) time.sleep(0.5) diff --git a/tests/unit/lifecycle_test.py b/tests/unit/lifecycle_test.py index 4a31e88ea..9f1e2e54d 100644 --- a/tests/unit/lifecycle_test.py +++ b/tests/unit/lifecycle_test.py @@ -1,4 +1,5 @@ import asyncio +import json import sys import time from unittest.mock import Mock @@ -114,3 +115,26 @@ async def test_kernel_lifecycle_close_while_disconnected(close_first, short_cull assert not context.closed_event.is_set() await cull_task_2 assert context.closed_event.is_set() + + +def test_kernel_lifecycle_session_id_mismatch_sends_solara_message(): + websocket = Mock() + kernel_context.initialize_virtual_kernel("session-id-1", "kernel-id-1", websocket) + + mismatched_websocket = Mock() + with pytest.raises(ValueError, match="Session id mismatch"): + kernel_context.initialize_virtual_kernel("session-id-2", "kernel-id-1", mismatched_websocket) + + mismatched_websocket.send.assert_called_once() + raw_message = mismatched_websocket.send.call_args.args[0] + msg = json.loads(raw_message) + + assert msg["channel"] == "iopub" + assert msg["header"]["msg_type"] == "solara_kernel_terminated" + assert msg["content"] == { + "reason": "session_id_mismatch", + "message": "Rejected websocket connection for existing virtual kernel because the session id did not match.", + "kernel_id": "kernel-id-1", + "retryable": False, + } + mismatched_websocket.close.assert_called_once() From 7ce47a746087f8d34348a0de667d2ed6da0458f4 Mon Sep 17 00:00:00 2001 From: Grotto Assistant Date: Tue, 21 Apr 2026 10:39:14 +0000 Subject: [PATCH 3/5] build: recompile solara widget manager kernel --- .../solara-widget-manager/lib/kernel.d.ts | 11 +++ packages/solara-widget-manager/lib/kernel.js | 98 +++++++++++++++++++ .../solara-widget-manager/lib/kernel.js.map | 1 + packages/solara-widget-manager/src/kernel.ts | 2 +- 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/solara-widget-manager/lib/kernel.d.ts create mode 100644 packages/solara-widget-manager/lib/kernel.js create mode 100644 packages/solara-widget-manager/lib/kernel.js.map diff --git a/packages/solara-widget-manager/lib/kernel.d.ts b/packages/solara-widget-manager/lib/kernel.d.ts new file mode 100644 index 000000000..4fe2bfc13 --- /dev/null +++ b/packages/solara-widget-manager/lib/kernel.d.ts @@ -0,0 +1,11 @@ +/*************************************************************************** + * Copyright (c) 2018, Voilà contributors * + * Copyright (c) 2018, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ****************************************************************************/ +import { Kernel, ServerConnection } from '@jupyterlab/services'; +export declare function connectKernel(baseUrl?: string, kernelId?: string, options?: Partial): Promise; +export declare function shutdownKernel(kernel: any): Promise; diff --git a/packages/solara-widget-manager/lib/kernel.js b/packages/solara-widget-manager/lib/kernel.js new file mode 100644 index 000000000..de4e7282a --- /dev/null +++ b/packages/solara-widget-manager/lib/kernel.js @@ -0,0 +1,98 @@ +"use strict"; +/*************************************************************************** + * Copyright (c) 2018, Voilà contributors * + * Copyright (c) 2018, QuantStack * + * * + * Distributed under the terms of the BSD 3-Clause License. * + * * + * The full license is in the file LICENSE, distributed with this software. * + ****************************************************************************/ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.shutdownKernel = exports.connectKernel = void 0; +const coreutils_1 = require("@jupyterlab/coreutils"); +const services_1 = require("@jupyterlab/services"); +const default_1 = require("@jupyterlab/services/lib/kernel/default"); +const KernelMessage = __importStar(require("@jupyterlab/services/lib/kernel/messages")); +const SOLARA_KERNEL_TERMINATED_MSG_TYPE = 'solara_kernel_terminated'; +function installSolaraKernelMessageLogging(kernel) { + kernel.iopubMessage.connect((_, msg) => { + if (msg.header.msg_type !== SOLARA_KERNEL_TERMINATED_MSG_TYPE) { + return; + } + console.error('Solara kernel terminated:', msg.content); + if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') { + window.dispatchEvent(new CustomEvent('solara.kernelTerminated', { + detail: msg.content, + })); + } + }); +} +function connectKernel(baseUrl, kernelId, options) { + return __awaiter(this, void 0, void 0, function* () { + baseUrl = baseUrl !== null && baseUrl !== void 0 ? baseUrl : coreutils_1.PageConfig.getBaseUrl(); + kernelId = kernelId !== null && kernelId !== void 0 ? kernelId : coreutils_1.PageConfig.getOption('kernelId'); + const serverSettings = services_1.ServerConnection.makeSettings(Object.assign({ baseUrl }, options)); + // const model = await KernelAPI.getKernelModel(kernelId, serverSettings); + // if (!model) { + // return; + // } + const model = { 'id': kernelId, 'name': 'solara-name' }; + const kernel = new default_1.KernelConnection({ model, serverSettings }); + installSolaraKernelMessageLogging(kernel); + return kernel; + }); +} +exports.connectKernel = connectKernel; +function shutdownKernel(kernel) { + return __awaiter(this, void 0, void 0, function* () { + // we cannot use this API since it uses the REST api + // while we want to use the current websocket since this makes + // sure the message gets delivered to the node/process where the + // kernel is running + // await kernel.shutdown(); + // so we use https://jupyter-protocol.readthedocs.io/en/latest/messaging.html#kernel-shutdown + // @ts-ignore + const msg = KernelMessage.createMessage({ + // @ts-ignore + msgType: 'shutdown_request', channel: 'control', username: kernel._username, session: kernel._clientId, content: { + // @ts-ignore + 'restart': false, // final shutdown, not a restart + } + }); + return yield kernel.sendControlMessage(msg); + }); +} +exports.shutdownKernel = shutdownKernel; +//# sourceMappingURL=kernel.js.map \ No newline at end of file diff --git a/packages/solara-widget-manager/lib/kernel.js.map b/packages/solara-widget-manager/lib/kernel.js.map new file mode 100644 index 000000000..ce19eb4da --- /dev/null +++ b/packages/solara-widget-manager/lib/kernel.js.map @@ -0,0 +1 @@ +{"version":3,"file":"kernel.js","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":";AAAA;;;;;;;8EAO8E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9E,qDAAmD;AACnD,mDAAgE;AAChE,qEAA2E;AAC3E,wFAA0E;AAE1E,MAAM,iCAAiC,GAAG,0BAA0B,CAAC;AAErE,SAAS,iCAAiC,CAAC,MAAwB;IACjE,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;QACrC,IAAK,GAAG,CAAC,MAAM,CAAC,QAAmB,KAAK,iCAAiC,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;YAChF,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,yBAAyB,EAAE;gBACzC,MAAM,EAAE,GAAG,CAAC,OAAO;aACpB,CAAC,CACH,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAsB,aAAa,CACjC,OAAgB,EAChB,QAAiB,EACjB,OAA6C;;QAE7C,OAAO,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,sBAAU,CAAC,UAAU,EAAE,CAAC;QAC7C,QAAQ,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,sBAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,2BAAgB,CAAC,YAAY,iBAAG,OAAO,IAAK,OAAO,EAAG,CAAC;QAE9E,0EAA0E;QAC1E,gBAAgB;QAChB,YAAY;QACZ,IAAI;QACJ,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,CAAA;QACvD,MAAM,MAAM,GAAG,IAAI,0BAAgB,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAC/D,iCAAiC,CAAC,MAAM,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;CAAA;AAjBD,sCAiBC;AAGD,SAAsB,cAAc,CAAC,MAAM;;QACzC,oDAAoD;QACpD,8DAA8D;QAC9D,gEAAgE;QAChE,oBAAoB;QACpB,2BAA2B;QAC3B,6FAA6F;QAC7F,aAAa;QACb,MAAM,GAAG,GAAG,aAAa,CAAC,aAAa,CAAC;YACtC,aAAa;YACb,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE;gBAC/G,aAAa;gBACb,SAAS,EAAE,KAAK,EAAE,gCAAgC;aACnD;SACF,CAAC,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;CAAA;AAhBD,wCAgBC"} \ No newline at end of file diff --git a/packages/solara-widget-manager/src/kernel.ts b/packages/solara-widget-manager/src/kernel.ts index 0839bd412..6759a90cd 100644 --- a/packages/solara-widget-manager/src/kernel.ts +++ b/packages/solara-widget-manager/src/kernel.ts @@ -16,7 +16,7 @@ const SOLARA_KERNEL_TERMINATED_MSG_TYPE = 'solara_kernel_terminated'; function installSolaraKernelMessageLogging(kernel: KernelConnection) { kernel.iopubMessage.connect((_, msg) => { - if (msg.header.msg_type !== SOLARA_KERNEL_TERMINATED_MSG_TYPE) { + if ((msg.header.msg_type as string) !== SOLARA_KERNEL_TERMINATED_MSG_TYPE) { return; } console.error('Solara kernel terminated:', msg.content); From 1c446d94b4766ec0a24ca2876c0dfa90c43e151f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:40:05 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- packages/solara-widget-manager/lib/kernel.js | 2 +- packages/solara-widget-manager/lib/kernel.js.map | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/solara-widget-manager/lib/kernel.js b/packages/solara-widget-manager/lib/kernel.js index de4e7282a..a300a97c4 100644 --- a/packages/solara-widget-manager/lib/kernel.js +++ b/packages/solara-widget-manager/lib/kernel.js @@ -95,4 +95,4 @@ function shutdownKernel(kernel) { }); } exports.shutdownKernel = shutdownKernel; -//# sourceMappingURL=kernel.js.map \ No newline at end of file +//# sourceMappingURL=kernel.js.map diff --git a/packages/solara-widget-manager/lib/kernel.js.map b/packages/solara-widget-manager/lib/kernel.js.map index ce19eb4da..1b51ffe60 100644 --- a/packages/solara-widget-manager/lib/kernel.js.map +++ b/packages/solara-widget-manager/lib/kernel.js.map @@ -1 +1 @@ -{"version":3,"file":"kernel.js","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":";AAAA;;;;;;;8EAO8E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9E,qDAAmD;AACnD,mDAAgE;AAChE,qEAA2E;AAC3E,wFAA0E;AAE1E,MAAM,iCAAiC,GAAG,0BAA0B,CAAC;AAErE,SAAS,iCAAiC,CAAC,MAAwB;IACjE,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;QACrC,IAAK,GAAG,CAAC,MAAM,CAAC,QAAmB,KAAK,iCAAiC,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;YAChF,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,yBAAyB,EAAE;gBACzC,MAAM,EAAE,GAAG,CAAC,OAAO;aACpB,CAAC,CACH,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAsB,aAAa,CACjC,OAAgB,EAChB,QAAiB,EACjB,OAA6C;;QAE7C,OAAO,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,sBAAU,CAAC,UAAU,EAAE,CAAC;QAC7C,QAAQ,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,sBAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,2BAAgB,CAAC,YAAY,iBAAG,OAAO,IAAK,OAAO,EAAG,CAAC;QAE9E,0EAA0E;QAC1E,gBAAgB;QAChB,YAAY;QACZ,IAAI;QACJ,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,CAAA;QACvD,MAAM,MAAM,GAAG,IAAI,0BAAgB,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAC/D,iCAAiC,CAAC,MAAM,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;CAAA;AAjBD,sCAiBC;AAGD,SAAsB,cAAc,CAAC,MAAM;;QACzC,oDAAoD;QACpD,8DAA8D;QAC9D,gEAAgE;QAChE,oBAAoB;QACpB,2BAA2B;QAC3B,6FAA6F;QAC7F,aAAa;QACb,MAAM,GAAG,GAAG,aAAa,CAAC,aAAa,CAAC;YACtC,aAAa;YACb,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE;gBAC/G,aAAa;gBACb,SAAS,EAAE,KAAK,EAAE,gCAAgC;aACnD;SACF,CAAC,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;CAAA;AAhBD,wCAgBC"} \ No newline at end of file +{"version":3,"file":"kernel.js","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":";AAAA;;;;;;;8EAO8E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9E,qDAAmD;AACnD,mDAAgE;AAChE,qEAA2E;AAC3E,wFAA0E;AAE1E,MAAM,iCAAiC,GAAG,0BAA0B,CAAC;AAErE,SAAS,iCAAiC,CAAC,MAAwB;IACjE,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;QACrC,IAAK,GAAG,CAAC,MAAM,CAAC,QAAmB,KAAK,iCAAiC,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;YAChF,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,yBAAyB,EAAE;gBACzC,MAAM,EAAE,GAAG,CAAC,OAAO;aACpB,CAAC,CACH,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAsB,aAAa,CACjC,OAAgB,EAChB,QAAiB,EACjB,OAA6C;;QAE7C,OAAO,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,sBAAU,CAAC,UAAU,EAAE,CAAC;QAC7C,QAAQ,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,sBAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,2BAAgB,CAAC,YAAY,iBAAG,OAAO,IAAK,OAAO,EAAG,CAAC;QAE9E,0EAA0E;QAC1E,gBAAgB;QAChB,YAAY;QACZ,IAAI;QACJ,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,CAAA;QACvD,MAAM,MAAM,GAAG,IAAI,0BAAgB,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAC/D,iCAAiC,CAAC,MAAM,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;CAAA;AAjBD,sCAiBC;AAGD,SAAsB,cAAc,CAAC,MAAM;;QACzC,oDAAoD;QACpD,8DAA8D;QAC9D,gEAAgE;QAChE,oBAAoB;QACpB,2BAA2B;QAC3B,6FAA6F;QAC7F,aAAa;QACb,MAAM,GAAG,GAAG,aAAa,CAAC,aAAa,CAAC;YACtC,aAAa;YACb,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE;gBAC/G,aAAa;gBACb,SAAS,EAAE,KAAK,EAAE,gCAAgC;aACnD;SACF,CAAC,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;CAAA;AAhBD,wCAgBC"} From 440503748ebeed22e3e87caa9161d949f289f3ff Mon Sep 17 00:00:00 2001 From: Grotto Assistant Date: Tue, 21 Apr 2026 10:42:32 +0000 Subject: [PATCH 5/5] fix: handle kernel termination in main app --- .../solara-widget-manager/lib/kernel.d.ts | 11 --- packages/solara-widget-manager/lib/kernel.js | 98 ------------------- .../solara-widget-manager/lib/kernel.js.map | 1 - packages/solara-widget-manager/src/kernel.ts | 17 ---- solara/server/static/main-vuetify.js | 9 ++ 5 files changed, 9 insertions(+), 127 deletions(-) delete mode 100644 packages/solara-widget-manager/lib/kernel.d.ts delete mode 100644 packages/solara-widget-manager/lib/kernel.js delete mode 100644 packages/solara-widget-manager/lib/kernel.js.map diff --git a/packages/solara-widget-manager/lib/kernel.d.ts b/packages/solara-widget-manager/lib/kernel.d.ts deleted file mode 100644 index 4fe2bfc13..000000000 --- a/packages/solara-widget-manager/lib/kernel.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*************************************************************************** - * Copyright (c) 2018, Voilà contributors * - * Copyright (c) 2018, QuantStack * - * * - * Distributed under the terms of the BSD 3-Clause License. * - * * - * The full license is in the file LICENSE, distributed with this software. * - ****************************************************************************/ -import { Kernel, ServerConnection } from '@jupyterlab/services'; -export declare function connectKernel(baseUrl?: string, kernelId?: string, options?: Partial): Promise; -export declare function shutdownKernel(kernel: any): Promise; diff --git a/packages/solara-widget-manager/lib/kernel.js b/packages/solara-widget-manager/lib/kernel.js deleted file mode 100644 index a300a97c4..000000000 --- a/packages/solara-widget-manager/lib/kernel.js +++ /dev/null @@ -1,98 +0,0 @@ -"use strict"; -/*************************************************************************** - * Copyright (c) 2018, Voilà contributors * - * Copyright (c) 2018, QuantStack * - * * - * Distributed under the terms of the BSD 3-Clause License. * - * * - * The full license is in the file LICENSE, distributed with this software. * - ****************************************************************************/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.shutdownKernel = exports.connectKernel = void 0; -const coreutils_1 = require("@jupyterlab/coreutils"); -const services_1 = require("@jupyterlab/services"); -const default_1 = require("@jupyterlab/services/lib/kernel/default"); -const KernelMessage = __importStar(require("@jupyterlab/services/lib/kernel/messages")); -const SOLARA_KERNEL_TERMINATED_MSG_TYPE = 'solara_kernel_terminated'; -function installSolaraKernelMessageLogging(kernel) { - kernel.iopubMessage.connect((_, msg) => { - if (msg.header.msg_type !== SOLARA_KERNEL_TERMINATED_MSG_TYPE) { - return; - } - console.error('Solara kernel terminated:', msg.content); - if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') { - window.dispatchEvent(new CustomEvent('solara.kernelTerminated', { - detail: msg.content, - })); - } - }); -} -function connectKernel(baseUrl, kernelId, options) { - return __awaiter(this, void 0, void 0, function* () { - baseUrl = baseUrl !== null && baseUrl !== void 0 ? baseUrl : coreutils_1.PageConfig.getBaseUrl(); - kernelId = kernelId !== null && kernelId !== void 0 ? kernelId : coreutils_1.PageConfig.getOption('kernelId'); - const serverSettings = services_1.ServerConnection.makeSettings(Object.assign({ baseUrl }, options)); - // const model = await KernelAPI.getKernelModel(kernelId, serverSettings); - // if (!model) { - // return; - // } - const model = { 'id': kernelId, 'name': 'solara-name' }; - const kernel = new default_1.KernelConnection({ model, serverSettings }); - installSolaraKernelMessageLogging(kernel); - return kernel; - }); -} -exports.connectKernel = connectKernel; -function shutdownKernel(kernel) { - return __awaiter(this, void 0, void 0, function* () { - // we cannot use this API since it uses the REST api - // while we want to use the current websocket since this makes - // sure the message gets delivered to the node/process where the - // kernel is running - // await kernel.shutdown(); - // so we use https://jupyter-protocol.readthedocs.io/en/latest/messaging.html#kernel-shutdown - // @ts-ignore - const msg = KernelMessage.createMessage({ - // @ts-ignore - msgType: 'shutdown_request', channel: 'control', username: kernel._username, session: kernel._clientId, content: { - // @ts-ignore - 'restart': false, // final shutdown, not a restart - } - }); - return yield kernel.sendControlMessage(msg); - }); -} -exports.shutdownKernel = shutdownKernel; -//# sourceMappingURL=kernel.js.map diff --git a/packages/solara-widget-manager/lib/kernel.js.map b/packages/solara-widget-manager/lib/kernel.js.map deleted file mode 100644 index 1b51ffe60..000000000 --- a/packages/solara-widget-manager/lib/kernel.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"kernel.js","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":";AAAA;;;;;;;8EAO8E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE9E,qDAAmD;AACnD,mDAAgE;AAChE,qEAA2E;AAC3E,wFAA0E;AAE1E,MAAM,iCAAiC,GAAG,0BAA0B,CAAC;AAErE,SAAS,iCAAiC,CAAC,MAAwB;IACjE,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE;QACrC,IAAK,GAAG,CAAC,MAAM,CAAC,QAAmB,KAAK,iCAAiC,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;YAChF,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,yBAAyB,EAAE;gBACzC,MAAM,EAAE,GAAG,CAAC,OAAO;aACpB,CAAC,CACH,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAsB,aAAa,CACjC,OAAgB,EAChB,QAAiB,EACjB,OAA6C;;QAE7C,OAAO,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,sBAAU,CAAC,UAAU,EAAE,CAAC;QAC7C,QAAQ,GAAG,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,sBAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACxD,MAAM,cAAc,GAAG,2BAAgB,CAAC,YAAY,iBAAG,OAAO,IAAK,OAAO,EAAG,CAAC;QAE9E,0EAA0E;QAC1E,gBAAgB;QAChB,YAAY;QACZ,IAAI;QACJ,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,CAAA;QACvD,MAAM,MAAM,GAAG,IAAI,0BAAgB,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QAC/D,iCAAiC,CAAC,MAAM,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;CAAA;AAjBD,sCAiBC;AAGD,SAAsB,cAAc,CAAC,MAAM;;QACzC,oDAAoD;QACpD,8DAA8D;QAC9D,gEAAgE;QAChE,oBAAoB;QACpB,2BAA2B;QAC3B,6FAA6F;QAC7F,aAAa;QACb,MAAM,GAAG,GAAG,aAAa,CAAC,aAAa,CAAC;YACtC,aAAa;YACb,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE;gBAC/G,aAAa;gBACb,SAAS,EAAE,KAAK,EAAE,gCAAgC;aACnD;SACF,CAAC,CAAC;QACH,OAAO,MAAM,MAAM,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;CAAA;AAhBD,wCAgBC"} diff --git a/packages/solara-widget-manager/src/kernel.ts b/packages/solara-widget-manager/src/kernel.ts index 6759a90cd..49f5977a4 100644 --- a/packages/solara-widget-manager/src/kernel.ts +++ b/packages/solara-widget-manager/src/kernel.ts @@ -12,23 +12,7 @@ import { Kernel, ServerConnection } from '@jupyterlab/services'; import { KernelConnection } from '@jupyterlab/services/lib/kernel/default'; import * as KernelMessage from '@jupyterlab/services/lib/kernel/messages'; -const SOLARA_KERNEL_TERMINATED_MSG_TYPE = 'solara_kernel_terminated'; -function installSolaraKernelMessageLogging(kernel: KernelConnection) { - kernel.iopubMessage.connect((_, msg) => { - if ((msg.header.msg_type as string) !== SOLARA_KERNEL_TERMINATED_MSG_TYPE) { - return; - } - console.error('Solara kernel terminated:', msg.content); - if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') { - window.dispatchEvent( - new CustomEvent('solara.kernelTerminated', { - detail: msg.content, - }) - ); - } - }); -} export async function connectKernel( baseUrl?: string, @@ -45,7 +29,6 @@ export async function connectKernel( // } const model = { 'id': kernelId, 'name': 'solara-name' } const kernel = new KernelConnection({ model, serverSettings }); - installSolaraKernelMessageLogging(kernel); return kernel; } diff --git a/solara/server/static/main-vuetify.js b/solara/server/static/main-vuetify.js index d4bce7148..f0ce6c86f 100644 --- a/solara/server/static/main-vuetify.js +++ b/solara/server/static/main-vuetify.js @@ -133,6 +133,15 @@ async function solaraInit(mountId, appName) { } const close_url = `${solara.rootPath}/_solara/api/close/${kernelId}?session_id=${kernel.clientId}`; let skipReconnectedCheck = true; + kernel.iopubMessage.connect((_, msg) => { + if (msg.header.msg_type !== 'solara_kernel_terminated') { + return; + } + console.error('Solara kernel terminated:', msg.content); + window.dispatchEvent(new CustomEvent('solara.kernelTerminated', { + detail: msg.content, + })); + }); kernel.statusChanged.connect(() => { app.$data.kernelBusy = kernel.status == 'busy'; });