From c753e2b38293c321805f9001db48a2a72e1a95ba Mon Sep 17 00:00:00 2001 From: John Miller Date: Fri, 24 Apr 2020 11:01:36 -0600 Subject: [PATCH] Initial support for control channel and kernel interrupt messages. Control (no, not the video game) is a new messaging channel for communicating with jupyter kernels. Introduced in version 5.3 of the messaging protocol, see https://jupyter-client.readthedocs.io/en/stable/messaging.html#messages-on-the-control-router-dealer-channel for more information. It doesn't do much at the moment since, as far as I know, only the xeus-python kernel actually responds to messages on this channel. As a test see the new `ein:kernel-interrupt-command' function. With ipykernel nothing happens other than to raise an error in the server log, but in xeus-python the kernel will correctly be interrupted. For more information on what the Jupyter foundation plans to do with this new channel, see https://blog.jupyter.org/a-visual-debugger-for-jupyter-914e61716559. --- lisp/ein-classes.el | 1 + lisp/ein-kernel.el | 38 ++++++++++++++++++++++++++++++++++++++ lisp/ein-websocket.el | 14 ++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/lisp/ein-classes.el b/lisp/ein-classes.el index 9196d1857..984271e73 100644 --- a/lisp/ein-classes.el +++ b/lisp/ein-classes.el @@ -213,6 +213,7 @@ kernelspec events api-version + message-protocol-version session-id kernel-id shell-channel diff --git a/lisp/ein-kernel.el b/lisp/ein-kernel.el index 0b982e802..2ef1658e6 100644 --- a/lisp/ein-kernel.el +++ b/lisp/ein-kernel.el @@ -270,12 +270,16 @@ See https://github.com/ipython/ipython/pull/3307" (defun ein:kernel--handle-websocket-reply (kernel ws frame) (ein:and-let* ((packet (websocket-frame-payload frame)) (channel (plist-get (ein:json-read-from-string packet) :channel))) + (unless (ein:$kernel-message-protocol-version kernel) + (setf (ein:$kernel-message-protocol-version kernel) (ein:websocket--message-protocol-version packet))) (cond ((string-equal channel "iopub") (ein:kernel--handle-iopub-reply kernel packet)) ((string-equal channel "shell") (ein:kernel--handle-shell-reply kernel packet)) ((string-equal channel "stdin") (ein:kernel--handle-stdin-reply kernel packet)) + ((string-equal channel "control") + (ein:kernel--handle-control-reply kernel packet)) (t (ein:log 'warn "Received reply from unforeseen channel %s" channel))))) (defun ein:start-single-websocket (kernel open-callback) @@ -536,6 +540,17 @@ Example:: :success (lambda (&rest ignore) (ein:log 'info "Sent interruption command."))))) +(defun ein:kernel-interrupt-command (kernel) + "Send an interrupt request to the kernel via the control channel (see +https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-interrupt). +This message was recently introduced and only works with kernels that support messaging +protocols version 5.3 and greater. As of April 2020 I believe only xeus-python will correctly +process interrupt messages." + (interactive (list (ein:get-kernel))) + (awhen kernel + (let* ((msg (ein:kernel--get-msg kernel "interrupt_request" (make-hash-table)))) + (ein:websocket-send-control-channel kernel msg)))) + (cl-defun ein:kernel-delete-session (&optional callback &key url-or-port path kernel &aux (session-id)) @@ -667,6 +682,29 @@ Example:: (aif (plist-get content :execution_count) (ein:events-trigger events 'execution_count.Kernel it)))))))) +(defun ein:kernel--handle-control-reply (kernel packet) + (cl-destructuring-bind + (&key header content metadata &allow-other-keys) + (ein:json-read-from-string packet) + (let* ((msg-type (plist-get header :msg_type)) + (msg-id (plist-get header :msg_id)) + (callbacks (ein:kernel-get-callbacks-for-msg kernel msg-id))) + (ein:log 'debug "ein:kernel--handle-control-reply: msg_type=%s msg_id=%s" + msg-type msg-id) + (aif (plist-get callbacks (intern-soft (format ":%s" msg-type))) + (ein:funcall-packed it content metadata) + (ein:log 'info "ein:kernel--handle-control-reply: No :%s callback for msg_id=%s" + msg-type msg-id)) + (let ((events (ein:$kernel-events kernel))) + (ein:case-equal msg-type + (("shutdown_reply") + (aif (plist-get content :restart) + (ein:log 'info "ein:kernel--handle-control-reply: Kernel restarting."))) + (("interrupt_reply") + (ein:log 'info "ein:kernel--handle-control-reply: Kernel interrupt request received.")) + (("debug_reply") + (ein:log 'info "ein:kernel--handle-control-reply: Kernel debug request received: %s" content))))))) + (defun ein:kernel--handle-iopub-reply (kernel packet) (if (ein:$kernel-stdin-activep kernel) (ein:ipdb--handle-iopub-reply kernel packet) diff --git a/lisp/ein-websocket.el b/lisp/ein-websocket.el index 5b32e3fec..cce02e236 100644 --- a/lisp/ein-websocket.el +++ b/lisp/ein-websocket.el @@ -125,6 +125,13 @@ (setf (ein:$websocket-closed-by-client websocket) t) (websocket-close (ein:$websocket-ws websocket))) +(defun ein:websocket--message-protocol-version (msg) + (condition-case err + (progn + (if (stringp msg) + (setf msg (ein:json-read-from-string msg))) + (string-to-number (plist-get (plist-get msg :header) :version))) + (error 3))) ;; Assume a protocol version if it is not specified in the header. (defun ein:websocket-send-shell-channel (kernel msg) (cond ((= (ein:$kernel-api-version kernel) 2) @@ -144,6 +151,13 @@ (ein:$kernel-websocket kernel) (json-encode (plist-put msg :channel "stdin")))))) +(defun ein:websocket-send-control-channel (kernel msg) + (cond ((< (ein:$kernel-message-protocol-version kernel) 5.3) + (ein:log 'warn "Control channel only supported in Jupyter message protocol version 5.3 and later.")) + (t (ein:websocket-send + (ein:$kernel-websocket kernel) + (json-encode (plist-put msg :channel "control")))))) + (provide 'ein-websocket) ;;; ein-websocket.el ends here