mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-05 07:41:37 -05:00
Support Jupyter kernel servers
* jupyter-client.el (jupyter-initialize-connection): Elevate NOTE to FIXME. (jupyter-kernel-info): Extend timeout. * jupyter-kernel-manager.el (jupyter-shutdown-kernel): (jupyter-interrupt-kernel): Allow any number of arguments. * jupyter-kernelspec.el (jupyter-find-kernelspecs): Optionally allow specs. * jupyter-messages.el (jupyter-encode-raw-message): New function. * jupyter-rest-api.el (jupyter-api-auth-headers): New function. * jupyter-server-ioloop.el: New file. * jupyter-server.el: New file. * test/jupyter-test.el (jupyter-server): (jupyter-server-kernel-manager): (jupyter-server-start-new-kernel): New tests.
This commit is contained in:
parent
9df340bd0f
commit
67831c08fa
8 changed files with 1130 additions and 38 deletions
|
@ -275,7 +275,7 @@ See `jupyter-initialize-connection'."
|
|||
(list info-or-session
|
||||
'(or jupyter-session-p json-plist-p stringp))))))
|
||||
|
||||
;; NOTE: This requires that CLIENT is communicating with a kernel using a
|
||||
;; FIXME: This requires that CLIENT is communicating with a kernel using a
|
||||
;; `jupyter-channel-ioloop-comm' object.
|
||||
(cl-defmethod jupyter-initialize-connection ((client jupyter-kernel-client) info-or-session)
|
||||
"Initialize CLIENT with connection INFO-OR-SESSION.
|
||||
|
@ -1859,6 +1859,9 @@ dispatching based on the kernel language."
|
|||
(let* ((jupyter-inhibit-handlers t)
|
||||
(req (jupyter-send-kernel-info-request client))
|
||||
(msg (jupyter-wait-until-received :kernel-info-reply
|
||||
;; Go to great lengths to ensure we have waited long
|
||||
;; enough. When communicating with slow to start kernels
|
||||
;; behind a kernel server this is necessary.
|
||||
req (* 3 jupyter-long-timeout) "Requesting kernel info...")))
|
||||
(unless msg
|
||||
(error "Kernel did not respond to kernel-info request"))
|
||||
|
|
|
@ -380,7 +380,7 @@ connect to MANAGER's kernel."
|
|||
(jupyter-stop-channel channel)
|
||||
(oset manager control-channel nil)))
|
||||
|
||||
(cl-defgeneric jupyter-shutdown-kernel ((manager jupyter-kernel-manager) &optional restart timeout)
|
||||
(cl-defgeneric jupyter-shutdown-kernel ((manager jupyter-kernel-manager) &rest args)
|
||||
"Shutdown MANAGER's kernel or restart instead if RESTART is non-nil.
|
||||
Wait until TIMEOUT before forcibly shutting down the kernel.")
|
||||
|
||||
|
@ -418,7 +418,7 @@ channel is stopped unless RESTART is non-nil."
|
|||
(jupyter-start-kernel manager)
|
||||
(jupyter-stop-channels manager)))))
|
||||
|
||||
(cl-defgeneric jupyter-interrupt-kernel ((manager jupyter-kernel-manager) &optional timeout)
|
||||
(cl-defgeneric jupyter-interrupt-kernel ((manager jupyter-kernel-manager) &rest args)
|
||||
"Interrupt MANAGER's kernel.
|
||||
When the kernel has an interrupt mode of \"message\" send an
|
||||
interrupt request and wait until TIMEOUT for a reply.")
|
||||
|
|
|
@ -92,7 +92,7 @@ REFRESH has the same meaning as in
|
|||
`jupyter-available-kernelspecs'."
|
||||
(cdr (assoc name (jupyter-available-kernelspecs refresh))))
|
||||
|
||||
(defun jupyter-find-kernelspecs (re &optional refresh)
|
||||
(defun jupyter-find-kernelspecs (re &optional specs refresh)
|
||||
"Find all specs of kernels that have names matching matching RE.
|
||||
RE is a regular expression use to match the name of a kernel.
|
||||
Return an alist with elements of the form:
|
||||
|
@ -104,15 +104,22 @@ DIRECTORY is the kernel's resource directory, and PLIST is the
|
|||
kernelspec propery list read from the \"kernel.json\" file in the
|
||||
resource directory.
|
||||
|
||||
If SPECS is non-nil search SPECS, otherwise search the specs
|
||||
returned by `jupyter-available-kernelspecs'.
|
||||
|
||||
Optional argument REFRESH has the same meaning as in
|
||||
`jupyter-available-kernelspecs'."
|
||||
(delq nil (mapcar (lambda (s) (and (string-match-p re (car s)) s))
|
||||
(jupyter-available-kernelspecs refresh))))
|
||||
(cl-remove-if-not
|
||||
(lambda (s) (string-match-p re (car s)))
|
||||
(or specs (jupyter-available-kernelspecs refresh))))
|
||||
|
||||
(defun jupyter-guess-kernelspec (name)
|
||||
(defun jupyter-guess-kernelspec (name &optional specs refresh)
|
||||
"Return the first kernelspec matching NAME.
|
||||
Raise an error if no kernelspec could be found."
|
||||
(or (car (jupyter-find-kernelspecs name))
|
||||
Raise an error if no kernelspec could be found.
|
||||
|
||||
SPECS and REFRESH have the same meaning as in
|
||||
`jupyter-find-kernelspecs'."
|
||||
(or (car (jupyter-find-kernelspecs name specs refresh))
|
||||
(error "No valid kernelspec for kernel name (%s)" name)))
|
||||
|
||||
(defun jupyter-completing-read-kernelspec (&optional specs refresh)
|
||||
|
|
|
@ -239,6 +239,52 @@ The returned object has the same form as the object returned by
|
|||
"Encode TIME into an ISO 8601 time string."
|
||||
(format-time-string "%FT%T.%6N" time t))
|
||||
|
||||
(cl-defun jupyter-encode-raw-message (session
|
||||
type
|
||||
&rest plist
|
||||
&key
|
||||
content
|
||||
(msg-id (jupyter-new-uuid))
|
||||
parent-header
|
||||
metadata
|
||||
buffers
|
||||
&allow-other-keys)
|
||||
"Encode a message into a JSON string.
|
||||
Similar to `jupyter-encode-message', but returns the JSON encoded
|
||||
string instead of a list of the encoded parts.
|
||||
|
||||
PLIST is an extra property list added to the top level of the
|
||||
message before encoding."
|
||||
(declare (indent 2))
|
||||
(cl-check-type session jupyter-session)
|
||||
(cl-check-type metadata json-plist)
|
||||
(cl-check-type content json-plist)
|
||||
(cl-check-type parent-header json-plist)
|
||||
(cl-check-type buffers list)
|
||||
(or content (setq content jupyter--empty-dict))
|
||||
(or parent-header (setq parent-header jupyter--empty-dict))
|
||||
(or metadata (setq metadata jupyter--empty-dict))
|
||||
(or buffers (setq buffers []))
|
||||
(let (fplist)
|
||||
(while plist
|
||||
(cond
|
||||
((memq (car plist)
|
||||
'(:content :parent-header :metadata :buffers :msg-id))
|
||||
(pop plist)
|
||||
(pop plist))
|
||||
(t
|
||||
(push (prog1 (pop plist)
|
||||
(push (pop plist) fplist))
|
||||
fplist))))
|
||||
(jupyter--encode
|
||||
(cl-list*
|
||||
:parent_header parent-header
|
||||
:header (jupyter--message-header session type msg-id)
|
||||
:content content
|
||||
:metadata metadata
|
||||
:buffers buffers
|
||||
fplist))))
|
||||
|
||||
(cl-defun jupyter-encode-message (session
|
||||
type
|
||||
&key idents
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
(eval-when-compile (require 'subr-x))
|
||||
(require 'jupyter-base)
|
||||
(require 'websocket)
|
||||
(require 'url)
|
||||
|
||||
(declare-function jupyter-decode-time "jupyter-messages")
|
||||
|
||||
|
@ -71,7 +72,9 @@ operation of a `jupyter-rest-client'."
|
|||
`(progn
|
||||
(require 'url)
|
||||
(url-do-setup)
|
||||
(url-cookie-parse-file)
|
||||
;; Don't save any cookies or history in a subprocess
|
||||
(ignore-errors (cancel-timer url-history-timer))
|
||||
(ignore-errors (cancel-timer url-cookie-timer))
|
||||
(push ,(file-name-directory (locate-library "jupyter-base")) load-path)
|
||||
(push ,(file-name-directory (locate-library "websocket")) load-path)
|
||||
(require 'jupyter-rest-api)
|
||||
|
@ -391,19 +394,22 @@ Raise an error on failure."
|
|||
(cl-check-type client jupyter-rest-client)
|
||||
(unless jupyter-api--authentication-in-progress-p
|
||||
(let ((jupyter-api--authentication-in-progress-p t))
|
||||
(url-do-setup)
|
||||
(with-slots (auth url) client
|
||||
(unless (or (listp auth)
|
||||
(not (memq auth '(ask token password))))
|
||||
(when (eq auth 'ask)
|
||||
;; Check to see if the server is accessible first
|
||||
;; Check to see if the server is accessible first by trying to
|
||||
;; access the login page and checking if we can make an
|
||||
;; authenticated request afterwards.
|
||||
(let ((jupyter-api-max-authentication-attempts 1))
|
||||
(jupyter-api-password-authenticator client
|
||||
(lambda (_) "")))
|
||||
(if (jupyter-api-server-accessible-p client)
|
||||
;; Get the _xsrf cookie if we don't have it already.
|
||||
(progn
|
||||
(unless (jupyter-api-xsrf-header-from-cookies url)
|
||||
(let ((jupyter-api-max-authentication-attempts 1))
|
||||
(jupyter-api-password-authenticator client
|
||||
(lambda (_) ""))))
|
||||
(oset client auth t))
|
||||
(oset client auth t)
|
||||
(when noninteractive
|
||||
(signal 'jupyter-api-login-failed
|
||||
(list "Can't authenticate non-interactively")))
|
||||
(cond
|
||||
((y-or-n-p (format "Token authenticated [%s]? " url))
|
||||
(oset client auth 'token))
|
||||
|
@ -486,26 +492,24 @@ As a special case, if METHOD is \"WS\", a websocket will be
|
|||
opened using the REST api url and PLIST will be used in a call to
|
||||
`websocket-open'."
|
||||
(jupyter-api--ensure-authenticated client)
|
||||
(let* ((xsrf (jupyter-api-xsrf-header-from-cookies (oref client url)))
|
||||
(url-request-extra-headers url-request-extra-headers)
|
||||
(let* ((url-request-extra-headers
|
||||
(append (jupyter-api-auth-headers client)
|
||||
(jupyter-api-xsrf-header-from-cookies (oref client url))
|
||||
url-request-extra-headers))
|
||||
(jupyter-api-url (if (equal method "WS")
|
||||
(oref client ws-url)
|
||||
(oref client url))))
|
||||
(if (equal method "WS")
|
||||
(let ((head plist))
|
||||
(while (and head (not (keywordp (car head))))
|
||||
(pop head))
|
||||
(setq head (or (plist-member head :custom-header-alist)
|
||||
(setcdr (last plist)
|
||||
(list :custom-header-alist nil))))
|
||||
(let ((cur (plist-get head :custom-header-alist)))
|
||||
(plist-put head :custom-header-alist
|
||||
(append xsrf
|
||||
(jupyter-api-auth-headers client)
|
||||
cur))))
|
||||
(cl-callf2 append
|
||||
(append xsrf (jupyter-api-auth-headers client))
|
||||
url-request-extra-headers))
|
||||
(when (equal method "WS")
|
||||
(let ((head plist))
|
||||
(while (and head (not (keywordp (car head))))
|
||||
(pop head))
|
||||
(setq head (or (plist-member head :custom-header-alist)
|
||||
(setcdr (last plist)
|
||||
(list :custom-header-alist nil))))
|
||||
(plist-put head :custom-header-alist
|
||||
(append
|
||||
(plist-get head :custom-header-alist)
|
||||
url-request-extra-headers))))
|
||||
(apply #'jupyter-api--request method plist)))
|
||||
|
||||
;;; Endpoints
|
||||
|
|
225
jupyter-server-ioloop.el
Normal file
225
jupyter-server-ioloop.el
Normal file
|
@ -0,0 +1,225 @@
|
|||
;;; jupyter-server-ioloop.el --- Kernel server communication -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 03 Apr 2019
|
||||
;; Version: 0.7.3
|
||||
|
||||
;; 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 2, 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:
|
||||
|
||||
;; A `jupyter-server-ioloop' launches websocket connections in order to
|
||||
;; communicate with a kernel server via the Jupyter messaging protocol. You can
|
||||
;; tell the ioloop to establish a websocket connection to a particular kernel
|
||||
;; by sending a connect-channels event with the websocket URL and kernel ID.
|
||||
;;
|
||||
;; (jupyter-send ioloop 'connect-channels "id")
|
||||
;;
|
||||
;; A connect-channels event will be emitted back to the parent process with the
|
||||
;; ID of the kernel in response.
|
||||
;;
|
||||
;; To stop a websocket connection, a disconnect-channels event can be sent,
|
||||
;; passing the kernel ID.
|
||||
;;
|
||||
;; (jupyter-send ioloop 'disconnect-channels "id")
|
||||
;;
|
||||
;; A disconnect-channels event will also be emitted back to the parent process
|
||||
;; with the ID of the kernel.
|
||||
;;
|
||||
;; Finally, a `jupyter-server-ioloop' behaves as a `jupyter-channel-ioloop'
|
||||
;; when sent a `send' event. That is it will emit a `sent' event after every
|
||||
;; `send' and when a message is received from the kernel will emit a `message'
|
||||
;; event. When sending a `send' event, the format is the same as a
|
||||
;; `jupyter-channel-ioloop' except that the kernel ID must be first argument.
|
||||
;;
|
||||
;; (jupyter-send ioloop 'send "id" ...)
|
||||
;;
|
||||
;; Similarly, when the parent process receives a `message' or `sent' event, the
|
||||
;; first argument will be the kernel ID
|
||||
;;
|
||||
;; (message "id" ...) or (sent "id" ...)
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'jupyter-ioloop)
|
||||
(require 'jupyter-messages)
|
||||
(require 'jupyter-rest-api)
|
||||
(require 'websocket)
|
||||
|
||||
(defvar jupyter-server-recvd-messages nil)
|
||||
(defvar jupyter-server-connected-kernels nil)
|
||||
(defvar jupyter-server-rest-client nil)
|
||||
|
||||
(defclass jupyter-server-ioloop (jupyter-ioloop)
|
||||
;; TODO: Clean this up by removing the need for these and just setting these
|
||||
;; values in `jupyter-ioloop-start' similar to the `jupyter-channel-ioloop'.
|
||||
((url :type string :initarg :url)
|
||||
(ws-url
|
||||
:type string
|
||||
:initarg :ws-url
|
||||
:documentation "The URL to connect websockets to.")
|
||||
(ws-headers
|
||||
:type (list-of cons)
|
||||
:initform nil
|
||||
:initarg :ws-headers
|
||||
:documentation "Headers that will be passed to the websocket connections.
|
||||
Has the same format as `url-request-extra-headers'."))
|
||||
:documentation "A `jupyter-ioloop' configured for communication using websockets.
|
||||
|
||||
A websocket can be opened by sending the connect-channels event
|
||||
with the websocket url and the kernel-id of the kernel to connect
|
||||
to, e.g.
|
||||
|
||||
\(jupyter-send ioloop 'connect-channels \"kernel-id\")
|
||||
|
||||
Also implemented is the send event which takes the same arguments
|
||||
as the send event of a `jupyter-channel-ioloop' except the
|
||||
kernel-id must be the first element, e.g.
|
||||
|
||||
\(jupyter-send ioloop 'send \"kernel-id\" ...)
|
||||
|
||||
Events that are emitted to the parent process are the message
|
||||
event, also the same as the event in `jupyter-channel-ioloop'
|
||||
except with a kernel-id as the first element. And a
|
||||
disconnected-channels event that occurs whenever a websocket is
|
||||
closed, the event has the kernel-id of the associated with the
|
||||
websocket.")
|
||||
|
||||
(cl-defmethod initialize-instance ((ioloop jupyter-server-ioloop) &optional _slots)
|
||||
(cl-call-next-method)
|
||||
(cl-callf append (oref ioloop setup)
|
||||
`((jupyter-api-with-subprocess-setup
|
||||
(require 'jupyter-server-ioloop)
|
||||
(push 'jupyter-server-ioloop--recv-messages jupyter-ioloop-pre-hook)
|
||||
(setq jupyter-server-rest-client (jupyter-rest-client
|
||||
:url ,(oref ioloop url)
|
||||
:ws-url ,(oref ioloop ws-url)
|
||||
:auth (quote ,(oref ioloop ws-headers)))))))
|
||||
(jupyter-server-ioloop-add-send-event ioloop)
|
||||
(jupyter-server-ioloop-add-connect-channels-event ioloop)
|
||||
(jupyter-server-ioloop-add-disconnect-channels-event ioloop))
|
||||
|
||||
;;; Receiving messages on a websocket
|
||||
|
||||
;; Added to `jupyter-ioloop-pre-hook'
|
||||
(defun jupyter-server-ioloop--recv-messages ()
|
||||
;; A negative value of seconds means to return immediately if there was
|
||||
;; nothing that could be read from subprocesses. See `Faccept_process_output'
|
||||
;; and `wait_reading_process_output'.
|
||||
(accept-process-output nil -1)
|
||||
(when jupyter-server-recvd-messages
|
||||
(mapc (lambda (msg) (prin1 (cons 'message msg)))
|
||||
(nreverse jupyter-server-recvd-messages))
|
||||
(setq jupyter-server-recvd-messages nil)
|
||||
(zmq-flush 'stdout)))
|
||||
|
||||
(defun jupyter-server-ioloop--on-message (ws frame)
|
||||
(cl-case (websocket-frame-opcode frame)
|
||||
((text binary)
|
||||
(condition-case err
|
||||
(let* ((msg (jupyter-read-plist-from-string
|
||||
(websocket-frame-payload frame)))
|
||||
(channel (intern (concat ":" (plist-get msg :channel))))
|
||||
(msg-type (jupyter-message-type-as-keyword
|
||||
(jupyter-message-type msg)))
|
||||
(parent-header (plist-get msg :parent_header)))
|
||||
;; Convert into keyword since that is what is expected
|
||||
(plist-put msg :msg_type msg-type)
|
||||
(plist-put parent-header :msg_type msg-type)
|
||||
;; websocket-client-data = kernel-id
|
||||
(push (cons (websocket-client-data ws)
|
||||
;; NOTE: The nil is the identity field expected by a
|
||||
;; `jupyter-channel-ioloop', it is mimicked here.
|
||||
(cons channel (cons nil msg)))
|
||||
jupyter-server-recvd-messages))
|
||||
(error
|
||||
(zmq-prin1 (cons 'error (list (car err)
|
||||
(format "%S" (cdr err))))))))
|
||||
(t (zmq-prin1 (cons 'error (format "Unhandled websocket frame %s"
|
||||
(websocket-frame-opcode frame)))))))
|
||||
|
||||
(defun jupyter-server-ioloop--on-error (_ws type error)
|
||||
(zmq-prin1 (cons 'error (list 'websocket-error type
|
||||
(format "%S" (cdr error))))))
|
||||
|
||||
(defun jupyter-server-ioloop--disconnect (ws)
|
||||
(websocket-close ws)
|
||||
(cl-callf2 delq ws jupyter-server-connected-kernels))
|
||||
|
||||
(defun jupyter-server-ioloop--connect (kernel-id)
|
||||
(let ((ws (jupyter-api-get-kernel-ws
|
||||
jupyter-server-rest-client kernel-id
|
||||
:on-error #'jupyter-server-ioloop--on-error
|
||||
:on-message #'jupyter-server-ioloop--on-message)))
|
||||
(push ws jupyter-server-connected-kernels)))
|
||||
|
||||
(defun jupyter-server-ioloop--kernel-ws (kernel-id)
|
||||
(cl-find-if
|
||||
(lambda (ws) (equal kernel-id (websocket-client-data ws)))
|
||||
jupyter-server-connected-kernels))
|
||||
|
||||
;;; IOLoop events
|
||||
|
||||
(defvar jupyter-server--dummy-session (jupyter-session :id ""))
|
||||
|
||||
(defun jupyter-server-ioloop-add-send-event (ioloop)
|
||||
(jupyter-ioloop-add-event
|
||||
ioloop send (kernel-id channel msg-type msg msg-id)
|
||||
(let ((ws (jupyter-server-ioloop--kernel-ws kernel-id)))
|
||||
(unless ws
|
||||
(error "Kernel with ID (%s) not connected" kernel-id))
|
||||
(websocket-send-text
|
||||
ws (jupyter-encode-raw-message
|
||||
jupyter-server--dummy-session msg-type
|
||||
:channel (substring (symbol-name channel) 1)
|
||||
:msg-id msg-id
|
||||
:content msg))
|
||||
(jupyter-server-ioloop--recv-messages)
|
||||
(list 'sent kernel-id channel msg-id))))
|
||||
|
||||
(defun jupyter-server-ioloop-add-connect-channels-event (ioloop)
|
||||
(jupyter-ioloop-add-event ioloop connect-channels (kernel-id)
|
||||
(let ((ws (jupyter-server-ioloop--kernel-ws kernel-id)))
|
||||
(unless ws
|
||||
;; NOTE: Authentication of the client happens in the parent process or
|
||||
;; through the Authorization header set in the :auth slot of the client.
|
||||
;; In the case of the parent process doing the authentication, cookies
|
||||
;; are written to `url-cookie-file' and read from this subprocess by the
|
||||
;; websocket code.
|
||||
(url-cookie-parse-file)
|
||||
(jupyter-server-ioloop--connect kernel-id)))
|
||||
;; Ensure any pending messages are handled, since usually we synchronize on
|
||||
;; connect-channels events, we want this event to be the
|
||||
;; `jupyter-ioloop-last-event' so the waiting loop in the parent process
|
||||
;; can capture it.
|
||||
(jupyter-server-ioloop--recv-messages)
|
||||
(list 'connect-channels kernel-id)))
|
||||
|
||||
(defun jupyter-server-ioloop-add-disconnect-channels-event (ioloop)
|
||||
(jupyter-ioloop-add-event ioloop disconnect-channels (kernel-id)
|
||||
(let ((ws (jupyter-server-ioloop--kernel-ws kernel-id)))
|
||||
;; See the note at the end of
|
||||
;; `jupyter-server-ioloop-add-connect-channels-event'
|
||||
(jupyter-server-ioloop--recv-messages)
|
||||
(when ws
|
||||
(jupyter-server-ioloop--disconnect ws))
|
||||
(list 'disconnect-channels kernel-id))))
|
||||
|
||||
(provide 'jupyter-server-ioloop)
|
||||
|
||||
;;; jupyter-server-ioloop.el ends here
|
701
jupyter-server.el
Normal file
701
jupyter-server.el
Normal file
|
@ -0,0 +1,701 @@
|
|||
;;; jupyter-server.el --- Support for the Jupyter kernel servers -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2019 Nathaniel Nicandro
|
||||
|
||||
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
||||
;; Created: 02 Apr 2019
|
||||
;; Version: 0.8.0
|
||||
|
||||
;; 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.
|
||||
;;
|
||||
;; TODO: Naming kernels in `jupyter-server-list-kernels' instead of using their
|
||||
;; ID. The kernel ID is not very useful to quickly identify which kernel does
|
||||
;; what, it would be more useful to be able to associate a name with a kernel
|
||||
;; ID.
|
||||
|
||||
;;; 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)
|
||||
|
||||
(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)
|
||||
|
||||
;;; Plumbing
|
||||
|
||||
(defvar jupyter--servers nil)
|
||||
|
||||
(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'.")))
|
||||
|
||||
;; TODO: When to `delete-instance'? Or define a function so that the user can
|
||||
;; do so.
|
||||
(defun jupyter-servers ()
|
||||
"Return a list of all `jupyter-server's."
|
||||
jupyter--servers)
|
||||
|
||||
;; TODO: Add the server as a slot
|
||||
(defclass jupyter-server-kernel (jupyter-meta-kernel)
|
||||
((id
|
||||
:type string
|
||||
:initarg :id
|
||||
:documentation "The kernel ID.")))
|
||||
|
||||
(cl-defmethod jupyter-kernel-alive-p ((kernel jupyter-server-kernel))
|
||||
(slot-boundp kernel 'id))
|
||||
|
||||
(cl-defmethod jupyter-start-kernel ((kernel jupyter-server-kernel) server &rest _ignore)
|
||||
(cl-check-type server jupyter-server)
|
||||
(with-slots (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))
|
||||
(cl-call-next-method)
|
||||
(slot-makeunbound kernel 'id))
|
||||
|
||||
(defclass jupyter-server-kernel-comm (jupyter-comm-layer)
|
||||
((server :type jupyter-server :initarg :server)
|
||||
(kernel :type jupyter-server-kernel :initarg :kernel)))
|
||||
|
||||
(cl-defmethod jupyter-comm-id ((comm jupyter-server-kernel-comm))
|
||||
(format "kid=%s" (truncate-string-to-width
|
||||
(thread-first comm
|
||||
(oref kernel)
|
||||
(oref id))
|
||||
9 nil nil "…")))
|
||||
|
||||
;;;; `jupyter-server' events
|
||||
|
||||
(cl-defmethod jupyter-event-handler ((comm jupyter-server)
|
||||
(event (head disconnect-channels)))
|
||||
(let ((kernel-id (cadr event)))
|
||||
(jupyter-comm-client-loop comm client
|
||||
(when (equal kernel-id (oref (oref client kernel) id))
|
||||
(jupyter-comm-stop client)))
|
||||
(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-client-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. I wonder if there
|
||||
;; is any issue with having an empty session ID in the messages sent by
|
||||
;; the `jupyter-server-ioloop', see `jupyter-server--dummy-session'.
|
||||
(jupyter-event-handler client event)))))
|
||||
|
||||
;;;; `jupyter-server' methods
|
||||
|
||||
(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-connect-client ((comm jupyter-server)
|
||||
(kcomm jupyter-server-kernel-comm))
|
||||
(with-slots (id) (oref kcomm kernel)
|
||||
(cl-call-next-method)
|
||||
(jupyter-send comm 'connect-channels id)
|
||||
(unless (jupyter-ioloop-wait-until (oref comm ioloop)
|
||||
'connect-channels #'identity)
|
||||
(error "Timeout when connecting websocket to kernel id %s" id))))
|
||||
|
||||
(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))
|
||||
|
||||
;;;; `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) comm
|
||||
(jupyter-comm-start server)
|
||||
(jupyter-connect-client server comm)))
|
||||
|
||||
(cl-defmethod jupyter-comm-stop ((comm jupyter-server-kernel-comm) &rest _ignore)
|
||||
"Disconnect COMM from receiving server events."
|
||||
(jupyter-disconnect-client (oref comm 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 (server kernel) comm
|
||||
(apply #'jupyter-send 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."
|
||||
(and (jupyter-server-kernel-connected-p
|
||||
(oref comm server)
|
||||
(oref (oref comm kernel) id))
|
||||
(catch 'member
|
||||
(jupyter-comm-client-loop (oref comm 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))
|
||||
|
||||
(cl-defmethod jupyter-channels-running-p ((comm jupyter-server-kernel-comm))
|
||||
(jupyter-comm-alive-p comm))
|
||||
|
||||
;;;; `jupyter-server-kernel-manager'
|
||||
|
||||
(defclass jupyter-server-kernel-manager (jupyter-kernel-manager-base)
|
||||
((server :type jupyter-server :initarg :server)
|
||||
(kernel :type jupyter-server-kernel :initarg :kernel)
|
||||
(comm :type jupyter-server-kernel-comm)))
|
||||
|
||||
(cl-defmethod jupyter-kernel-alive-p ((manager jupyter-server-kernel-manager))
|
||||
(with-slots (server kernel) manager
|
||||
(and (jupyter-kernel-alive-p kernel)
|
||||
(ignore-errors (jupyter-api-get-kernel server (oref kernel id))))))
|
||||
|
||||
(cl-defmethod jupyter-start-kernel ((manager jupyter-server-kernel-manager) &rest _ignore)
|
||||
"Ensure that the gateway can receive events from its kernel."
|
||||
(with-slots (server kernel) manager
|
||||
(jupyter-start-kernel kernel server)))
|
||||
|
||||
(cl-defmethod jupyter-interrupt-kernel ((manager jupyter-server-kernel-manager))
|
||||
(with-slots (server kernel) manager
|
||||
(jupyter-api-interrupt-kernel server (oref kernel id))))
|
||||
|
||||
(cl-defmethod jupyter-kill-kernel ((manager jupyter-server-kernel-manager))
|
||||
(jupyter-shutdown-kernel manager))
|
||||
|
||||
;; TODO: Figure out if restarting a kernel keeps the kernel ID
|
||||
(cl-defmethod jupyter-shutdown-kernel ((manager jupyter-server-kernel-manager) &optional restart _timeout)
|
||||
(with-slots (server kernel comm) manager
|
||||
(if restart (jupyter-api-restart-kernel server (oref kernel id))
|
||||
(when (jupyter-comm-alive-p server)
|
||||
;; Stop the communication of a `jupyter-server' with
|
||||
;; `jupyter-server-kernel-comm's that have the associated kernel ID.
|
||||
(jupyter-send server 'disconnect-channels (oref kernel id)))
|
||||
(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
|
||||
(unless (slot-boundp manager 'comm)
|
||||
(oset manager comm (jupyter-server-kernel-comm
|
||||
:kernel (oref manager kernel)
|
||||
:server (oref manager server)))
|
||||
(jupyter-comm-start (oref manager comm)))
|
||||
(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)
|
||||
(eq (oref manager server) server)
|
||||
(jupyter-kernel-alive-p manager)
|
||||
(equal (oref (oref manager 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))))
|
||||
|
||||
(defun jupyter-current-server (&optional ask)
|
||||
"Return an existing `jupyter-server' or a new one.
|
||||
If `jupyter-current-server' is non-nil, return its value.
|
||||
Otherwise, return the most recently used server.
|
||||
|
||||
With a prefix argument, ASK to select one and set the selected
|
||||
one as the most recently used.
|
||||
|
||||
If no servers exist, ask the user to create one and return its
|
||||
value."
|
||||
(interactive "P")
|
||||
(let ((read-url-make-server
|
||||
(lambda ()
|
||||
(let ((url (read-string "Server URL: " "http://localhost:8888"))
|
||||
(ws-url (read-string "Websocket URL: " "ws://localhost:8888")))
|
||||
(or (jupyter-find-server url ws-url)
|
||||
(jupyter-server :url url :ws-url ws-url))))))
|
||||
(if ask (let ((server (funcall read-url-make-server)))
|
||||
(prog1 server
|
||||
(setq jupyter--servers
|
||||
(cons server (delq server jupyter--servers)))))
|
||||
(or jupyter-current-server
|
||||
(and (file-remote-p default-directory)
|
||||
(jupyter-tramp-file-name-p default-directory)
|
||||
(jupyter-tramp-server-from-file-name default-directory))
|
||||
(if (> (length jupyter--servers) 1)
|
||||
(let ((server (cdr (completing-read
|
||||
"Jupyter Server: "
|
||||
(mapcar (lambda (x) (cons (oref x url) x))
|
||||
jupyter--servers)))))
|
||||
(prog1 server
|
||||
(setq jupyter--servers
|
||||
(cons server (delq server jupyter--servers)))))
|
||||
(or (car jupyter--servers)
|
||||
(funcall read-url-make-server)))))))
|
||||
|
||||
;;; 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
|
||||
:spec (jupyter-guess-kernelspec kernel-name specs)))
|
||||
(manager (jupyter-server-kernel-manager
|
||||
:server server
|
||||
: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-initialize-connection' 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))
|
||||
(kernel (jupyter-server-kernel
|
||||
:id kernel-id
|
||||
:spec (assoc (plist-get model :name) specs))))
|
||||
(jupyter-server-kernel-manager
|
||||
:server server
|
||||
:kernel kernel))))
|
||||
(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))
|
||||
|
||||
(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 "<return>") #'jupyter-server-kernel-list-new-repl)
|
||||
(define-key map (kbd "C-<return>") #'jupyter-server-kernel-list-launch-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"))))
|
||||
|
||||
(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
|
||||
(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)
|
||||
(propertize name 'face 'font-lock-constant-face)))
|
||||
(activity (propertize (format-time-string "%F %T" time)
|
||||
'face 'font-lock-doc-face))
|
||||
(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."
|
||||
(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)
|
||||
(jupyter-server-kernel-list-mode))
|
||||
(jupyter-display-current-buffer-reuse-window))))
|
||||
|
||||
(provide 'jupyter-server)
|
||||
|
||||
;;; jupyter-server.el ends here
|
|
@ -34,6 +34,7 @@
|
|||
(require 'jupyter-repl)
|
||||
(require 'jupyter-org-client)
|
||||
(require 'jupyter-kernel-manager)
|
||||
(require 'jupyter-server)
|
||||
(require 'cl-lib)
|
||||
(require 'ert)
|
||||
(require 'subr-x) ; string-trim
|
||||
|
@ -49,6 +50,13 @@
|
|||
|
||||
(message "system-configuration %s" system-configuration)
|
||||
|
||||
(when noninteractive
|
||||
(message "Starting up notebook process for tests")
|
||||
(start-process "jupyter-notebook" nil "jupyter" "notebook"
|
||||
"--no-browser"
|
||||
"--NotebookApp.token=''"
|
||||
"--NotebookApp.password=''"))
|
||||
|
||||
(declare-function org-babel-python-table-or-string "ob-python" (results))
|
||||
|
||||
;; TODO: Required tests
|
||||
|
@ -704,6 +712,22 @@
|
|||
(jupyter-runtime-directory)
|
||||
(should (equal jupyter-runtime-directory "foo")))))))
|
||||
|
||||
(ert-deftest jupyter-server-kernel ()
|
||||
:tags '(kernel server)
|
||||
(let ((kernel (jupyter-server-kernel)))
|
||||
(should-not (slot-boundp kernel 'id))
|
||||
(should-not (jupyter-kernel-alive-p kernel))
|
||||
;; TODO: How should this work? Pass the server as an argument?
|
||||
(should-error (jupyter-start-kernel kernel))
|
||||
(oset kernel id "foobar")
|
||||
;; FIXME: Since the kernel does not have access to the server and is just a
|
||||
;; container for data, we suppose the kernel is alive when it has an ID
|
||||
;; assigned to it.
|
||||
(should (jupyter-kernel-alive-p kernel))
|
||||
(jupyter-kill-kernel kernel)
|
||||
(should-not (slot-boundp kernel 'id))
|
||||
(should-not (jupyter-kernel-alive-p kernel))))
|
||||
|
||||
;;; Client
|
||||
|
||||
;; TODO: Different values of the session argument
|
||||
|
@ -783,15 +807,18 @@
|
|||
(jupyter-initialize-connection client conn-info)
|
||||
(cl-loop
|
||||
for channel in '(:hb :shell :iopub :stdin)
|
||||
do (should-not (jupyter-channel-alive-p client channel)))
|
||||
for alive-p = (jupyter-channel-alive-p client channel)
|
||||
do (should-not alive-p))
|
||||
(jupyter-start-channels client)
|
||||
(cl-loop
|
||||
for channel in '(:hb :shell :iopub :stdin)
|
||||
do (should (jupyter-channel-alive-p client channel)))
|
||||
for alive-p = (jupyter-channel-alive-p client channel)
|
||||
do (should alive-p))
|
||||
(jupyter-stop-channels client)
|
||||
(cl-loop
|
||||
for channel in '(:hb :shell :iopub :stdin)
|
||||
do (should-not (jupyter-channel-alive-p client channel))))))
|
||||
for alive-p = (jupyter-channel-alive-p client channel)
|
||||
do (should-not alive-p)))))
|
||||
|
||||
(ert-deftest jupyter-inhibited-handlers ()
|
||||
:tags '(client handlers)
|
||||
|
@ -1921,6 +1948,85 @@ next(x"))))))
|
|||
(websocket-close ws))
|
||||
(jupyter-api-shutdown-kernel client id)))))
|
||||
|
||||
;;; Server
|
||||
|
||||
;; And `jupyter-server-kernel-comm'
|
||||
(ert-deftest jupyter-server ()
|
||||
:tags '(server)
|
||||
(let* ((server (jupyter-server))
|
||||
(kernel (jupyter-server-kernel
|
||||
:spec (jupyter-guess-kernelspec
|
||||
"python" (jupyter-server-kernelspecs server)))))
|
||||
(should-not (slot-boundp kernel 'id))
|
||||
(let ((id (jupyter-start-kernel kernel server)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(should (slot-boundp kernel 'id))
|
||||
(should (equal id (oref kernel id)))
|
||||
(should (jupyter-api-get-kernel server id))
|
||||
(should-not (jupyter-server-kernel-connected-p server id))
|
||||
(should-not (jupyter-comm-alive-p server))
|
||||
(jupyter-comm-start server)
|
||||
(should (jupyter-comm-alive-p server))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ert-info ("Connecting kernel comm to server")
|
||||
(let ((kcomm (jupyter-server-kernel-comm
|
||||
:server server
|
||||
:kernel kernel)))
|
||||
(should-not (jupyter-server-kernel-connected-p server id))
|
||||
(jupyter-connect-client server kcomm)
|
||||
(should (jupyter-server-kernel-connected-p server id))
|
||||
(should (jupyter-comm-alive-p kcomm))
|
||||
(jupyter-comm-stop kcomm)
|
||||
(should-not (jupyter-comm-alive-p kcomm))
|
||||
(should (jupyter-comm-alive-p server))))
|
||||
(ert-info ("Connecting kernel comm starts server comm if necessary")
|
||||
(let ((kcomm (jupyter-server-kernel-comm
|
||||
:server server
|
||||
:kernel kernel)))
|
||||
(jupyter-comm-stop server)
|
||||
(should-not (jupyter-comm-alive-p server))
|
||||
(jupyter-comm-start kcomm)
|
||||
(should (jupyter-comm-alive-p server))
|
||||
(should (jupyter-server-kernel-connected-p server id))
|
||||
(should (jupyter-comm-alive-p kcomm))
|
||||
(jupyter-comm-stop kcomm))))
|
||||
(jupyter-comm-stop server)))
|
||||
(jupyter-api-shutdown-kernel server id)))))
|
||||
|
||||
(ert-deftest jupyter-server-kernel-manager ()
|
||||
:tags '(server)
|
||||
(let* ((server (jupyter-server))
|
||||
(kernel (jupyter-server-kernel
|
||||
:spec (jupyter-guess-kernelspec
|
||||
"python" (jupyter-server-kernelspecs server))))
|
||||
(manager (jupyter-server-kernel-manager
|
||||
:server server
|
||||
:kernel kernel)))
|
||||
(should-not (jupyter-kernel-alive-p manager))
|
||||
(let ((id (jupyter-start-kernel manager)))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(should (jupyter-kernel-alive-p manager))
|
||||
;; TODO: Does shutting down the kernel also send a
|
||||
;; disconnect-channels event?
|
||||
)
|
||||
(jupyter-api-shutdown-kernel server id)))))
|
||||
|
||||
(ert-deftest jupyter-server-start-new-kernel ()
|
||||
:tags '(server)
|
||||
(let ((server (jupyter-server)))
|
||||
(cl-destructuring-bind (manager client)
|
||||
(jupyter-server-start-new-kernel server "python")
|
||||
(unwind-protect
|
||||
(let ((jupyter-current-client client))
|
||||
;; TODO: Get rid of these sleep calls
|
||||
(sleep-for 2)
|
||||
(should (equal (jupyter-eval "1 + 1") "2")))
|
||||
(jupyter-shutdown-kernel manager)
|
||||
(jupyter-comm-stop server)))))
|
||||
|
||||
;;; `org-mode'
|
||||
|
||||
(defvar org-babel-jupyter-resource-directory nil)
|
||||
|
|
Loading…
Add table
Reference in a new issue