mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-04 15:41:37 -05:00
906 lines
39 KiB
EmacsLisp
906 lines
39 KiB
EmacsLisp
;;; jupyter-server.el --- Support for the Jupyter kernel servers -*- lexical-binding: t -*-
|
||
|
||
;; Copyright (C) 2019-2020 Nathaniel Nicandro
|
||
|
||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||
;; 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-<return>") #'jupyter-server-kernel-list-launch-kernel)
|
||
(define-key map (kbd "<return>") #'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
|