;;; jupyter-server.el --- Support for the Jupyter kernel servers -*- lexical-binding: t -*- ;; Copyright (C) 2019-2020 Nathaniel Nicandro ;; Author: Nathaniel Nicandro ;; Created: 02 Apr 2019 ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License as ;; published by the Free Software Foundation; either version 3, or (at ;; your option) any later version. ;; This program is distributed in the hope that it will be useful, but ;; WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;; General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, ;; Boston, MA 02111-1307, USA. ;;; Commentary: ;; Overview of implementation ;; ;; A `jupyter-server' communicates with a Jupyter kernel server (either the ;; notebook or a kernel gateway) via the Jupyter REST API. Given the URL and ;; Websocket URL for the server, the `jupyter-server' object can launch kernels ;; using the function `jupyter-server-start-new-kernel'. The kernelspecs ;; available on the server can be accessed by calling ;; `jupyter-server-kernelspecs'. ;; ;; `jupyter-server-start-new-kernel' returns a list (KM KC) where KM is a ;; `jupyter-server-kernel-manager' and KC is a kernel client that can ;; communicate with the kernel managed by KM. `jupyter-server-kernel-manager' ;; sends requests to the server using the `jupyter-server' object to manage the ;; lifetime of the kernel and ensures that a websocket is opened so that kernel ;; clients created using `jupyter-make-client' can communicate with the kernel. ;; ;; Communication with the channels of the kernels that are launched on the ;; `jupyter-server' is established via a `jupyter-server-ioloop' which ;; multiplexes the channels of all the kernel servers. The kernel ID the server ;; associated with a kernel can then be used to filter messages for a ;; particular kernel and to send messages to a kernel through the ;; `jupyter-server-ioloop'. ;; ;; `jupyter-server-kernel-comm' is a `jupyter-comm-layer' that handles the ;; communication of a client with a server kernel. The job of the ;; `jupyter-server-kernel-comm' is to connect to the `jupyter-server's event ;; stream and filter the messages to handle those of a particular kernel ;; identified by kernel ID. ;; ;; Starting REPLs ;; ;; You can launch kernels without connecting clients to them by using ;; `jupyter-server-launch-kernel'. To connect a REPL to a launched kernel use ;; `jupyter-connect-server-repl'. To both launch and connect a REPL use ;; `jupyter-run-server-repl'. All of the previous commands determine the server ;; to use by using the `jupyter-current-server' function, which see. ;; ;; Managing kernels on a server ;; ;; To get an overview of all live kernels on a server you can call ;; `jupyter-server-list-kernels'. From the buffer displayed there are a number ;; of keys bound that enable you to manage the kernels on the server. See ;; `jupyter-server-kernel-list-mode-map'. ;; ;; TODO: Find where it would be appropriate to call `delete-instance' on a ;;`jupyter-server' that does not have any websockets open, clients connected, ;; or HTTP connections open, or is not bound to `jupyter-current-server' in any ;; buffer. ;;; Code: (eval-when-compile (require 'subr-x)) (require 'jupyter-repl) (require 'jupyter-rest-api) (require 'jupyter-kernel-manager) (require 'jupyter-ioloop-comm) (require 'jupyter-server-ioloop) (declare-function jupyter-tramp-file-name-p "jupyter-tramp" (filename)) (declare-function jupyter-tramp-server-from-file-name "jupyter-tramp" (filename)) (declare-function jupyter-tramp-file-name-from-url "jupyter-tramp" (url)) (defgroup jupyter-server nil "Support for the Jupyter kernel gateway" :group 'jupyter) (defvar-local jupyter-current-server nil "The `jupyter-server' associated with the current buffer. Used in, e.g. a `jupyter-server-kernel-list-mode' buffer.") (put 'jupyter-current-server 'permanent-local t) ;;; `jupyter-server' (defvar jupyter--servers nil) ;; TODO: We should really rename `jupyter-server' to something like ;; `jupyter-server-client' since it isn't a representation of a server, but a ;; communication channel with one. (defclass jupyter-server (jupyter-rest-client jupyter-ioloop-comm eieio-instance-tracker) ((tracking-symbol :initform 'jupyter--servers) (kernelspecs :type json-plist :initform nil :documentation "Kernelspecs for the kernels available behind this gateway. Access should be done through `jupyter-available-kernelspecs'."))) (defun jupyter-servers () "Return a list of all `jupyter-server's." jupyter--servers) (defun jupyter-gc-servers () "Forget `jupyter-servers' that are no longer accessible at their hosts." (dolist (server (jupyter-servers)) (unless (jupyter-api-server-exists-p server) (when (jupyter-comm-alive-p server) (jupyter-comm-stop server)) (jupyter-api-delete-cookies (oref server url)) (delete-instance server)))) ;;; `jupyter-server-kernel' ;; TODO: Add the server as a slot (defclass jupyter-server-kernel (jupyter-kernel) ((server :type jupyter-server :initarg :server :documentation "The kernel server.") (id :type string :initarg :id :documentation "The kernel ID."))) (cl-defmethod jupyter-kernel-alive-p ((kernel jupyter-server-kernel)) (and (slot-boundp kernel 'id) (slot-boundp kernel 'server) ;; TODO: Cache this call (condition-case err (jupyter-api-get-kernel (oref kernel server) (oref kernel id)) (file-error nil) ; Non-existent server (jupyter-api-http-error (unless (= (nth 1 err) 404) ; Not Found (signal (car err) (cdr err))))))) (cl-defmethod jupyter-start-kernel ((kernel jupyter-server-kernel) &rest _ignore) (with-slots (server spec) kernel (jupyter-server--verify-kernelspec server spec) (cl-destructuring-bind (&key id &allow-other-keys) (jupyter-api-start-kernel server (car spec)) (oset kernel id id)))) (cl-defmethod jupyter-kill-kernel ((_kernel jupyter-server-kernel)) ;; The notebook server already takes care of forcing shutdown of a kernel. (ignore)) (defclass jupyter-server-kernel-comm (jupyter-comm-layer) ((kernel :type jupyter-server-kernel :initarg :kernel))) (cl-defmethod jupyter-comm-id ((comm jupyter-server-kernel-comm)) (let* ((kernel (oref comm kernel)) (id (oref kernel id))) (or (jupyter-server-kernel-name (oref kernel server) id) (format "kid=%s" (truncate-string-to-width id 9 nil nil "…"))))) ;;; Assigning names to kernel IDs (defvar jupyter-server-kernel-names nil "An alist mapping URLs to alists mapping kernel IDs to human friendly names. For example \((\"http://localhost:8888\" (\"72d92ded-1275-440a-852f-90f655197305\" . \"thermo\"))\) You can persist this alist across Emacs sessions using `desktop', `savehist', or any other session persistence package. For example, when using `savehist' you can add the following to your init file to persist the server names across Emacs sessions. \(savehist-mode\) \(add-to-list 'savehist-additional-variables 'jupyter-server-kernel-names\).") (defun jupyter-server-cull-kernel-names (&optional server) "Ensure all names in `jupyter-server-kernel-names' map to existing kernels. If SERVER is non-nil only check the kernels on SERVER, otherwise check all kernels on all existing servers." (let ((servers (if server (list server) (jupyter-gc-servers) (jupyter-servers)))) (unless server ;; Only remove non-existing servers when culling all kernels on all ;; servers. (let ((urls (mapcar (lambda (x) (oref x url)) servers))) (cl-callf2 cl-remove-if-not (lambda (x) (member (car x) urls)) jupyter-server-kernel-names))) (dolist (server servers) (when-let* ((names (assoc (oref server url) jupyter-server-kernel-names))) (setf (alist-get (oref server url) jupyter-server-kernel-names nil nil #'equal) (cl-loop for kernel across (jupyter-api-get-kernel server) for name = (assoc (plist-get kernel :id) names) when name collect name)))))) (defun jupyter-server-kernel-name (server id) "Return the associated name of the kernel with ID on SERVER. If there is no name associated, return nil. See `jupyter-server-kernel-names'." (cl-check-type server jupyter-server) (let ((kernel-names (assoc (oref server url) jupyter-server-kernel-names))) (cdr (assoc id kernel-names)))) (defun jupyter-server-kernel-id-from-name (server name) "Return the ID of the kernel that has NAME on SERVER. If NAME does not have a kernel associated, return nil. See `jupyter-server-kernel-names'." (cl-check-type server jupyter-server) (jupyter-server-cull-kernel-names server) (let ((kernel-names (assoc (oref server url) jupyter-server-kernel-names))) (car (rassoc name kernel-names)))) (defun jupyter-server-name-kernel (server id name) "NAME the kernel with ID on SERVER. See `jupyter-server-kernel-names'." (cl-check-type server jupyter-server) (setf (alist-get id (alist-get (oref server url) jupyter-server-kernel-names nil nil #'equal) nil nil #'equal) name)) (defun jupyter-server-name-client-kernel (client name) "For the kernel connected to CLIENT associate NAME. CLIENT must be communicating with a `jupyter-server-kernel', the ID of the kernel will be associated with NAME, see `jupyter-server-kernel-names'." (cl-check-type client jupyter-kernel-client) (cl-check-type (oref client kcomm) jupyter-server-kernel-comm) (let* ((kernel (thread-first client (oref kcomm) (oref kernel))) (id (oref kernel id))) (jupyter-server-name-kernel (oref kernel server) id name))) ;;; Plumbing ;;;; `jupyter-server' events (cl-defmethod jupyter-event-handler ((comm jupyter-server) (event (head disconnect-channels))) (let ((kernel-id (cadr event))) (with-slots (ioloop) comm (cl-callf2 remove kernel-id (process-get (oref ioloop process) :kernel-ids))))) (cl-defmethod jupyter-event-handler ((comm jupyter-server) (event (head connect-channels))) (let ((kernel-id (cadr event))) (with-slots (ioloop) comm (cl-callf append (process-get (oref ioloop process) :kernel-ids) (list kernel-id))))) (cl-defmethod jupyter-event-handler ((comm jupyter-server) event) "Send EVENT to all clients connected to COMM. Each client must have a KERNEL slot which, in turn, must have an ID slot. The second element of EVENT is expected to be a kernel ID. Send EVENT, with the kernel ID excluded, to a client whose kernel has a matching ID." (let ((kernel-id (cadr event))) (setq event (cons (car event) (cddr event))) (jupyter-comm-handler-loop comm client (when (equal kernel-id (oref (oref client kernel) id)) ;; TODO: Since the event handlers of CLIENT will eventually call the ;; `jupyter-handle-message' of a `jupyter-kernel-client' we really ;; don't need to do any filtering based off of a `jupyter-session-id', ;; but maybe should? The `jupyter-handle-message' method will only ;; handle messages that have a parent ID of a previous request so there ;; already is filtering at the kernel client level. (jupyter-event-handler client event))))) ;;;; `jupyter-server' methods (defun jupyter-server--connect-channels (server id) (jupyter-send server 'connect-channels id) (jupyter-with-timeout (nil jupyter-default-timeout (error "Timeout when connecting websocket to kernel id %s" id)) (jupyter-server-kernel-connected-p server id))) (defun jupyter-server--refresh-comm (server) "Stop and then start SERVER communication. Reconnect the previously connected kernels when starting." (when (jupyter-comm-alive-p server) (let ((connected (cl-remove-if-not (apply-partially #'jupyter-server-kernel-connected-p server) (mapcar (lambda (kernel) (plist-get kernel :id)) (jupyter-api-get-kernel server))))) (jupyter-comm-stop server) (jupyter-comm-start server) (while connected (jupyter-server--connect-channels server (pop connected)))))) (cl-defmethod jupyter-api-request :around ((server jupyter-server) _method &rest _plist) (condition-case nil (cl-call-next-method) (jupyter-api-unauthenticated (if (memq jupyter-api-authentication-method '(ask token password)) (oset server auth jupyter-api-authentication-method) (error "Unauthenticated request, can't attempt re-authentication \ with default `jupyter-api-authentication-method'")) (prog1 (cl-call-next-method) (jupyter-server--refresh-comm server))))) (cl-defmethod jupyter-comm-start ((comm jupyter-server)) (unless (and (slot-boundp comm 'ioloop) (jupyter-ioloop-alive-p (oref comm ioloop))) ;; TODO: Is a write to the cookie file and then a read of the cookie file ;; whenever connecting a websocket in a subprocess good enough? If, e.g. ;; the notebook is restarted and it clears the login information, there are ;; sometimes error due to `jupyter-api-request' trying to ask for login ;; information which look like "wrong type argument listp, [http://...]". ;; They don't seem to happens with the changes mentioned, but is it enough? (url-cookie-write-file) (oset comm ioloop (jupyter-server-ioloop :url (oref comm url) :ws-url (oref comm ws-url) :ws-headers (jupyter-api-auth-headers comm))) (cl-call-next-method))) (cl-defmethod jupyter-comm-add-handler ((comm jupyter-server) (kcomm jupyter-server-kernel-comm)) (cl-call-next-method) (with-slots (id) (oref kcomm kernel) (unless (jupyter-server-kernel-connected-p comm id) (jupyter-server--connect-channels comm id)))) (cl-defmethod jupyter-comm-remove-handler ((comm jupyter-server) (kcomm jupyter-server-kernel-comm)) (with-slots (id) (oref kcomm kernel) (when (jupyter-server-kernel-connected-p comm id) (jupyter-send comm 'disconnect-channels id) (unless (jupyter-ioloop-wait-until (oref comm ioloop) 'disconnect-channels #'identity) (error "Timeout when disconnecting websocket for kernel id %s" id)))) (cl-call-next-method)) (cl-defmethod jupyter-server-kernel-connected-p ((comm jupyter-server) id) "Return non-nil if COMM has a WebSocket connection to a kernel with ID." (and (jupyter-comm-alive-p comm) (member id (process-get (oref (oref comm ioloop) process) :kernel-ids)))) (defun jupyter-server--verify-kernelspec (server spec) (cl-destructuring-bind (name _ . kspec) spec (let ((server-spec (assoc name (jupyter-server-kernelspecs server)))) (unless server-spec (error "No kernelspec matching %s on server @ %s" name (oref server url))) (when (cl-loop with sspec = (cddr server-spec) for (k v) on sspec by #'cddr thereis (not (equal (plist-get kspec k) v))) (error "%s kernelspec doesn't match one on server @ %s" name (oref server url)))))) (cl-defmethod jupyter-server-kernelspecs ((server jupyter-server) &optional refresh) "Return the kernelspecs on SERVER. By default the available kernelspecs are cached. To force an update of the cached kernelspecs, give a non-nil value to REFRESH. The kernelspecs are returned in the same form as returned by `jupyter-available-kernelspecs'." (when (or refresh (null (oref server kernelspecs))) (let ((specs (jupyter-api-get-kernelspec server))) (unless specs (error "Can't retrieve kernelspecs from server @ %s" (oref server url))) (oset server kernelspecs specs) (plist-put (oref server kernelspecs) :kernelspecs (cl-loop with specs = (plist-get specs :kernelspecs) for (_ spec) on specs by #'cddr ;; Uses the same format as `jupyter-available-kernelspecs' ;; (name dir . spec) collect (cons (plist-get spec :name) (cons nil (plist-get spec :spec))))))) (plist-get (oref server kernelspecs) :kernelspecs)) (cl-defmethod jupyter-server-has-kernelspec-p ((server jupyter-server) name) "Return non-nil if SERVER can launch kernels with kernelspec NAME." (jupyter-guess-kernelspec name (jupyter-server-kernelspecs server))) ;;;; `jupyter-server-kernel-comm' methods (cl-defmethod jupyter-comm-start ((comm jupyter-server-kernel-comm) &rest _ignore) "Register COMM to receive server events. If SERVER receives events that have the same kernel ID as the kernel associated with COMM, then COMM's `jupyter-event-handler' will receive those events." (with-slots (server) (oref comm kernel) (jupyter-comm-start server) (jupyter-comm-add-handler server comm))) (cl-defmethod jupyter-comm-stop ((comm jupyter-server-kernel-comm) &rest _ignore) "Disconnect COMM from receiving server events." (jupyter-comm-remove-handler (oref (oref comm kernel) server) comm)) (cl-defmethod jupyter-send ((comm jupyter-server-kernel-comm) event-type &rest event) "Use COMM to send an EVENT to the server with type, EVENT-TYPE. SERVER will direct EVENT to the right kernel based on the kernel ID of the kernel associated with COMM." (with-slots (kernel) comm (unless (jupyter-comm-alive-p comm) (jupyter-comm-start comm)) (apply #'jupyter-send (oref kernel server) event-type (oref kernel id) event))) (cl-defmethod jupyter-comm-alive-p ((comm jupyter-server-kernel-comm)) "Return non-nil if COMM can receive server events for its associated kernel." (with-slots (kernel) comm (and (jupyter-server-kernel-connected-p (oref kernel server) (oref kernel id)) (catch 'member (jupyter-comm-handler-loop (oref kernel server) client (when (eq client comm) (throw 'member t))))))) ;; TODO: Remove the need for these methods, they are remnants from an older ;; implementation. They will need to be removed from `jupyter-kernel-client'. (cl-defmethod jupyter-channel-alive-p ((comm jupyter-server-kernel-comm) _channel) (jupyter-comm-alive-p comm)) ;;;; `jupyter-server-kernel-manager' (defclass jupyter-server-kernel-manager (jupyter-kernel-manager) ((comm :type jupyter-server-kernel-comm))) (cl-defmethod jupyter-comm-start ((manager jupyter-server-kernel-manager)) "Start a websocket connection to MANAGER's kernel. MANAGER's COMM slot will be set to the `jupyter-comm-layer' receiving events on the websocket when this method returns." (with-slots (kernel comm) manager (unless (slot-boundp manager 'comm) (oset manager comm (jupyter-server-kernel-comm :kernel kernel))) (unless (jupyter-comm-alive-p comm) (jupyter-comm-start comm)))) (cl-defmethod jupyter-comm-stop ((manager jupyter-server-kernel-manager)) "Stop a websocket connection to MANAGER's kernel." (when (slot-boundp manager 'comm) (with-slots (comm) manager (when (jupyter-comm-alive-p comm) (jupyter-comm-stop comm))))) (cl-defmethod jupyter-kernel-alive-p ((manager jupyter-server-kernel-manager)) (jupyter-kernel-alive-p (oref manager kernel))) (cl-defmethod jupyter-start-kernel ((manager jupyter-server-kernel-manager) &rest _ignore) "Ensure that the gateway can receive events from its kernel." (with-slots (kernel) manager (unless (jupyter-kernel-alive-p kernel) (jupyter-start-kernel kernel)) (jupyter-comm-start manager))) (cl-defmethod jupyter-interrupt-kernel ((manager jupyter-server-kernel-manager)) (with-slots (kernel) manager (jupyter-api-interrupt-kernel (oref kernel server) (oref kernel id)))) (cl-defmethod jupyter-shutdown-kernel ((manager jupyter-server-kernel-manager) &optional restart _timeout) (with-slots (kernel) manager (let ((server (oref kernel server))) (if restart (jupyter-api-restart-kernel server (oref kernel id)) (jupyter-comm-stop manager) (when (jupyter-kernel-alive-p manager) (jupyter-api-shutdown-kernel server (oref kernel id))))))) (cl-defmethod jupyter-make-client ((manager jupyter-server-kernel-manager) _class &rest _slots) (let ((client (cl-call-next-method))) (prog1 client (jupyter-comm-start manager) (oset client kcomm (oref manager comm))))) ;;; Finding exisisting kernel managers and servers (defun jupyter-server-find-manager (server id) "Return a kernel manager managing kernel with ID on SERVER. Return nil if none could be found." (cl-loop for manager in (jupyter-kernel-managers) thereis (and (cl-typep manager 'jupyter-server-kernel-manager) (with-slots (kernel) manager (and (eq (oref kernel server) server) (equal (oref kernel id) id))) manager))) (defun jupyter-find-server (url &optional ws-url) "Return a live `jupyter-server' that lives at URL. Finds a server that matches both URL and WS-URL. When WS-URL the default set by `jupyter-rest-client' is used. Return nil if no `jupyter-server' could be found." (with-slots (url ws-url) (apply #'make-instance 'jupyter-rest-client (append (list :url url) (when ws-url (list :ws-url ws-url)))) (cl-loop for server in (jupyter-servers) thereis (and (equal (oref server url) url) (equal (oref server ws-url) ws-url) server)))) ;;; Helpers for commands (defun jupyter-completing-read-server-kernel (server) "Use `completing-read' to select a kernel on SERVER. A model of the kernel is returned as a property list and has at least the following keys: - :id :: The ID used to identify the kernel on the server - :last_activity :: The last channel activity of the kernel - :name :: The kernelspec name used to start the kernel - :execution_state :: The status of the kernel - :connections :: The number of websocket connections for the kernel" (let* ((kernels (jupyter-api-get-kernel server)) (display-names (if (null kernels) (error "No kernels @ %s" (oref server url)) (mapcar (lambda (k) (cl-destructuring-bind (&key name id last_activity &allow-other-keys) k (concat name " (last activity: " last_activity ", id: " id ")"))) kernels))) (name (completing-read "kernel: " display-names nil t))) (when (equal name "") (error "No kernel selected")) (nth (- (length display-names) (length (member name display-names))) (append kernels nil)))) (define-error 'jupyter-server-non-existent "The server doesn't exist") (defun jupyter-current-server (&optional ask) "Return an existing `jupyter-server' or ASK for a new one. If ASK is non-nil, always ask for a URL and return the `jupyter-server' object corresponding to it. If no Jupyter server at URL exists, `signal' a `jupyter-server-non-existent' error with error data being URL. If the buffer local value of `jupyter-current-server' is non-nil, return its value. If `jupyter-current-server' is nil and the `jupyter-current-client' is communicating with a kernel behind a kernel server, return the `jupyter-server' managing the connection. If `jupyter-current-client' is nil or not communicating with a kernel behind a server and if `default-directory' is a Jupyter remote file name, return the `jupyter-server' object corresponding to that connection. If all of the above fails, either return the most recently used `jupyter-server' object if there is one or ask for one based off a URL." (interactive "P") (let ((read-url-make-server (lambda () ;; From the list of available server ;; (if (> (length jupyter--servers) 1) ;; (let ((server (cdr (completing-read ;; "Jupyter Server: " ;; (mapcar (lambda (x) (cons (oref x url) x)) ;; jupyter--servers))))) ;; ) (jupyter-gc-servers) (let* ((url (read-string "Server URL: " "http://localhost:8888")) (ws-url (read-string "Websocket URL: " (let ((u (url-generic-parse-url url))) (setf (url-type u) "ws") (url-recreate-url u))))) (or (jupyter-find-server url ws-url) (let ((server (jupyter-server :url url :ws-url ws-url))) (if (jupyter-api-server-exists-p server) server (delete-instance server) (signal 'jupyter-server-non-existent (list url))))))))) (let ((server (if ask (funcall read-url-make-server) (cond (jupyter-current-server) ;; Server of the current kernel client ((and jupyter-current-client (object-of-class-p (oref jupyter-current-client kcomm) 'jupyter-server-kernel-comm) (thread-first jupyter-current-client (oref kcomm) (oref kernel) (oref server)))) ;; Server of the current TRAMP remote context ((and (file-remote-p default-directory) (jupyter-tramp-file-name-p default-directory) (jupyter-tramp-server-from-file-name default-directory))) ;; Most recently accessed (t (or (car jupyter--servers) (funcall read-url-make-server))))))) (prog1 server (setq jupyter--servers (cons server (delq server jupyter--servers))))))) ;;; Commands ;;;###autoload (defun jupyter-server-launch-kernel (server) "Start a kernel on SERVER. With a prefix argument, ask to select a server if there are mutiple to choose from, otherwise the most recently used server is used as determined by `jupyter-current-server'." (interactive (list (jupyter-current-server current-prefix-arg))) (let* ((specs (jupyter-server-kernelspecs server)) (spec (jupyter-completing-read-kernelspec specs))) (jupyter-api-start-kernel server (car spec)))) ;;; REPL ;; TODO: When closing the REPL buffer and it is the last connected client as ;; shown by the :connections key of a `jupyter-api-get-kernel' call, ask to ;; also shutdown the kernel. ;; ;; TODO: When calling `jupyter-stop-channels' and there is only one client to a ;; `jupyter-server-kernel-comm', tell the `jupyter-server-ioloop' to disconnect ;; the channels. (defun jupyter-server-start-new-kernel (server kernel-name &optional client-class) "Start a managed Jupyter kernel on SERVER. KERNEL-NAME is the name of the kernel to start. It can also be the prefix of a valid kernel name, in which case the first kernel in ‘jupyter-server-kernelspecs’ that has KERNEL-NAME as a prefix will be used. Optional argument CLIENT-CLASS is a subclass of ‘jupyer-kernel-client’ and will be used to initialize a new client connected to the kernel. CLIENT-CLASS defaults to the symbol ‘jupyter-kernel-client’. Return a list (KM KC) where KM is the kernel manager managing the lifetime of the kernel on SERVER. KC is a new client connected to the kernel whose class is CLIENT-CLASS. Note that the client’s ‘manager’ slot will also be set to the kernel manager instance, see ‘jupyter-make-client’." (or client-class (setq client-class 'jupyter-kernel-client)) (let* ((specs (jupyter-server-kernelspecs server)) (kernel (jupyter-server-kernel :server server :spec (jupyter-guess-kernelspec kernel-name specs))) (manager (jupyter-server-kernel-manager :kernel kernel))) ;; Needs to be started before calling `jupyter-make-client' since that ;; method will send a request to start a websocket channel to the kernel. ;; FIXME: This should be done in a `jupyter-comm-initialize' method, ;; but first that method needs to be generalize in `jupyter-client.el' (unless (jupyter-kernel-alive-p manager) (jupyter-start-kernel manager)) (let ((client (jupyter-make-client manager client-class))) (jupyter-start-channels client) (list manager client)))) ;;;###autoload (defun jupyter-run-server-repl (server kernel-name &optional repl-name associate-buffer client-class display) "On SERVER start a kernel with KERNEL-NAME. With a prefix argument, ask to select a server if there are mutiple to choose from, otherwise the most recently used server is used as determined by `jupyter-current-server'. REPL-NAME, ASSOCIATE-BUFFER, CLIENT-CLASS, and DISPLAY all have the same meaning as in `jupyter-run-repl'." (interactive (let ((server (jupyter-current-server current-prefix-arg))) (list server (car (jupyter-completing-read-kernelspec (jupyter-server-kernelspecs server))) ;; FIXME: Ambiguity with `jupyter-current-server' and ;; `current-prefix-arg' (when (and current-prefix-arg (y-or-n-p "Name REPL? ")) (read-string "REPL Name: ")) t nil t))) (or client-class (setq client-class 'jupyter-repl-client)) (jupyter-error-if-not-client-class-p client-class 'jupyter-repl-client) (cl-destructuring-bind (_manager client) (jupyter-server-start-new-kernel server kernel-name client-class) (jupyter-bootstrap-repl client repl-name associate-buffer display))) ;;;###autoload (defun jupyter-connect-server-repl (server kernel-id &optional repl-name associate-buffer client-class display) "On SERVER, connect to the kernel with KERNEL-ID. With a prefix argument, ask to select a server if there are mutiple to choose from, otherwise the most recently used server is used as determined by `jupyter-current-server'. REPL-NAME, ASSOCIATE-BUFFER, CLIENT-CLASS, and DISPLAY all have the same meaning as in `jupyter-connect-repl'." (interactive (let ((server (jupyter-current-server current-prefix-arg))) (list server (plist-get (jupyter-completing-read-server-kernel server) :id) ;; FIXME: Ambiguity with `jupyter-current-server' and ;; `current-prefix-arg' (when (and current-prefix-arg (y-or-n-p "Name REPL? ")) (read-string "REPL Name: ")) t nil t))) (or client-class (setq client-class 'jupyter-repl-client)) (jupyter-error-if-not-client-class-p client-class 'jupyter-repl-client) (let* ((specs (jupyter-server-kernelspecs server)) (manager (or (jupyter-server-find-manager server kernel-id) (let ((model (jupyter-api-get-kernel server kernel-id))) (jupyter-server-kernel-manager :kernel (jupyter-server-kernel :id kernel-id :server server :spec (assoc (plist-get model :name) specs)))))) (client (jupyter-make-client manager client-class))) (jupyter-start-channels client) (jupyter-bootstrap-repl client repl-name associate-buffer display))) ;;; `jupyter-server-kernel-list' (defun jupyter-server-kernel-list-do-shutdown () "Shutdown the kernel corresponding to the current entry." (interactive) (when-let* ((id (tabulated-list-get-id)) (really (yes-or-no-p (format "Really shutdown %s kernel? " (aref (tabulated-list-get-entry) 0))))) (let ((manager (jupyter-server-find-manager jupyter-current-server id))) (if manager (jupyter-shutdown-kernel manager) (jupyter-api-shutdown-kernel jupyter-current-server id))) (tabulated-list-delete-entry))) (defun jupyter-server-kernel-list-do-restart () "Restart the kernel corresponding to the current entry." (interactive) (when-let* ((id (tabulated-list-get-id)) (really (yes-or-no-p "Really restart kernel? "))) (let ((manager (jupyter-server-find-manager jupyter-current-server id))) (if manager (jupyter-shutdown-kernel manager 'restart) (jupyter-api-restart-kernel jupyter-current-server id))) (revert-buffer))) (defun jupyter-server-kernel-list-do-interrupt () "Interrupt the kernel corresponding to the current entry." (interactive) (when-let* ((id (tabulated-list-get-id))) (jupyter-api-interrupt-kernel jupyter-current-server id) (revert-buffer))) (defun jupyter-server-kernel-list-new-repl () "Connect a REPL to the kernel corresponding to the current entry." (interactive) (when-let* ((id (tabulated-list-get-id))) (let ((jupyter-current-client (jupyter-connect-server-repl jupyter-current-server id))) (revert-buffer) (jupyter-repl-pop-to-buffer)))) (defun jupyter-server-kernel-list-launch-kernel () "Launch a new kernel on the server." (interactive) (jupyter-server-launch-kernel jupyter-current-server) (revert-buffer)) (defun jupyter-server-kernel-list-name-kernel () "Name the kernel under `point'." (interactive) (when-let* ((id (tabulated-list-get-id)) (name (read-string (let ((cname (jupyter-server-kernel-name jupyter-current-server id))) (if cname (format "Rename %s to: " cname) (format "Name kernel [%s]: " id)))))) (when (zerop (length name)) (jupyter-server-kernel-list-name-kernel)) (jupyter-server-name-kernel jupyter-current-server id name) (revert-buffer))) (defvar jupyter-server-kernel-list-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-i") #'jupyter-server-kernel-list-do-interrupt) (define-key map (kbd "d") #'jupyter-server-kernel-list-do-shutdown) (define-key map (kbd "C-c C-d") #'jupyter-server-kernel-list-do-shutdown) (define-key map (kbd "C-c C-r") #'jupyter-server-kernel-list-do-restart) (define-key map [follow-link] nil) ;; allows mouse-1 to be activated (define-key map [mouse-1] #'jupyter-server-kernel-list-new-repl) (define-key map (kbd "RET") #'jupyter-server-kernel-list-new-repl) (define-key map (kbd "C-RET") #'jupyter-server-kernel-list-launch-kernel) (define-key map (kbd "C-") #'jupyter-server-kernel-list-launch-kernel) (define-key map (kbd "") #'jupyter-server-kernel-list-new-repl) (define-key map "R" #'jupyter-server-kernel-list-name-kernel) (define-key map "r" #'revert-buffer) (define-key map "g" #'revert-buffer) map)) (define-derived-mode jupyter-server-kernel-list-mode tabulated-list-mode "Jupyter Server Kernels" "A list of live kernels on a Jupyter kernel server." (tabulated-list-init-header) (tabulated-list-print) (let ((inhibit-read-only t) (url (oref jupyter-current-server url))) (overlay-put (make-overlay 1 2) 'before-string (concat (propertize url 'face '(fixed-pitch default)) "\n"))) ;; So that `dired-jump' will visit the directory of the kernel server. (setq default-directory (jupyter-tramp-file-name-from-url (oref jupyter-current-server url)))) (defun jupyter-server--kernel-list-format () (let* ((get-time (lambda (a) (or (get-text-property 0 'jupyter-time a) (let ((time (jupyter-decode-time a))) (prog1 time (put-text-property 0 1 'jupyter-time time a)))))) (time-sort (lambda (a b) (time-less-p (funcall get-time (aref (nth 1 a) 2)) (funcall get-time (aref (nth 1 b) 2))))) (conn-sort (lambda (a b) (< (string-to-number (aref (nth 1 a) 4)) (string-to-number (aref (nth 1 b) 4)))))) `[("Name" 17 t) ("ID" 38 nil) ("Activity" 20 ,time-sort) ("State" 10 nil) ("Conns." 6 ,conn-sort)])) (defun jupyter-server--kernel-list-entries () (cl-loop with names = nil for kernel across (jupyter-api-get-kernel jupyter-current-server) collect (cl-destructuring-bind (&key name id last_activity execution_state connections &allow-other-keys) kernel (let* ((time (jupyter-decode-time last_activity)) (name (propertize (or (jupyter-server-kernel-name jupyter-current-server id) (let ((same (cl-remove-if-not (lambda (x) (string-prefix-p name x)) names))) (when same (setq name (format "%s<%d>" name (length same)))) (push name names) name)) 'face 'font-lock-constant-face)) (activity (propertize (jupyter-format-time-low-res time) 'face 'font-lock-doc-face 'jupyter-time time)) (conns (propertize (number-to-string connections) 'face 'shadow)) (state (propertize execution_state 'face (pcase execution_state ("busy" 'warning) ("idle" 'shadow) ("starting" 'success))))) (list id (vector name id activity state conns)))))) ;;;###autoload (defun jupyter-server-list-kernels (server) "Display a list of live kernels on SERVER. When called interactively, ask to select a SERVER when given a prefix argument otherwise the `jupyter-current-server' will be used." (interactive (list (jupyter-current-server current-prefix-arg))) (if (zerop (length (jupyter-api-get-kernel server))) (when (yes-or-no-p (format "No kernels at %s; launch one? " (oref server url))) (jupyter-server-launch-kernel server) (jupyter-server-list-kernels server)) (with-current-buffer (jupyter-get-buffer-create (format "kernels[%s]" (oref server url))) (setq jupyter-current-server server) (if (eq major-mode 'jupyter-server-kernel-list-mode) (revert-buffer) (setq tabulated-list-format (jupyter-server--kernel-list-format) tabulated-list-entries #'jupyter-server--kernel-list-entries tabulated-list-sort-key (cons "Activity" t)) (jupyter-server-kernel-list-mode) ;; So that `dired-jump' will visit the directory of the kernel server. (setq default-directory (jupyter-tramp-file-name-from-url (oref server url)))) (jupyter-display-current-buffer-reuse-window)))) (provide 'jupyter-server) ;;; jupyter-server.el ends here