2018-01-08 21:38:32 -06:00
|
|
|
;;; jupyter-kernel-manager.el --- Jupyter kernel manager -*- lexical-binding: t -*-
|
|
|
|
|
|
|
|
;; Copyright (C) 2018 Nathaniel Nicandro
|
|
|
|
|
|
|
|
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.edu>
|
|
|
|
;; Created: 08 Jan 2018
|
|
|
|
;; Version: 0.0.1
|
|
|
|
;; Keywords:
|
|
|
|
;; X-URL: https://github.com/nathan/jupyter-kernel-manager
|
|
|
|
|
|
|
|
;; 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:
|
|
|
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(require 'jupyter-base)
|
|
|
|
(require 'jupyter-connection)
|
2018-01-08 21:38:32 -06:00
|
|
|
(require 'jupyter-messages)
|
2018-01-04 23:03:18 -06:00
|
|
|
(require 'jupyter-client)
|
|
|
|
|
|
|
|
(declare-function string-trim-right "subr-x" (str))
|
|
|
|
|
2018-01-08 21:38:32 -06:00
|
|
|
(defgroup jupyter-kernel-manager nil
|
|
|
|
"Jupyter kernel manager"
|
|
|
|
:group 'communication)
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(defclass jupyter-kernel-manager (jupyter-connection)
|
|
|
|
((name
|
|
|
|
:initarg :name
|
|
|
|
:type string)
|
|
|
|
(conn-file
|
|
|
|
:type (or null string))
|
|
|
|
(kernel
|
|
|
|
:type (or null process)
|
|
|
|
:initform nil
|
|
|
|
:documentation "The local kernel process or nil if no local
|
|
|
|
kernel was started by this client.")
|
|
|
|
(control-channel
|
|
|
|
:type (or null jupyter-control-channel)
|
|
|
|
:initform nil)
|
|
|
|
:initform nil
|
2018-01-17 20:37:34 -06:00
|
|
|
(spec
|
2018-01-04 23:03:18 -06:00
|
|
|
:type (or null json-plist)
|
|
|
|
:initform nil)))
|
|
|
|
|
2018-01-08 22:31:33 -06:00
|
|
|
(cl-defmethod initialize-instance ((manager jupyter-kernel-manager) &rest _slots)
|
2018-01-04 23:03:18 -06:00
|
|
|
"Initialize MANAGER based on SLOTS.
|
|
|
|
If the `:name' slot is not found in SLOTS, it defaults to
|
|
|
|
\"python\". This means that without providing a kernel name, the
|
|
|
|
default kernel is a python kernel."
|
|
|
|
(cl-call-next-method)
|
|
|
|
(unless (slot-boundp manager 'name)
|
|
|
|
(oset manager name "python")))
|
|
|
|
|
2018-01-18 16:06:22 -06:00
|
|
|
(cl-defmethod destructor ((manager jupyter-kernel-manager) &rest _params)
|
|
|
|
"Kill the kernel of MANAGER and stop its channels."
|
|
|
|
;; See `jupyter--kernel-sentinel' for other cleanup
|
|
|
|
(when (processp (oref manager kernel))
|
|
|
|
(delete-process (oref manager kernel)))
|
|
|
|
(jupyter-stop-channels manager))
|
|
|
|
|
2018-01-08 13:26:09 -06:00
|
|
|
(cl-defmethod slot-unbound ((manager jupyter-kernel-manager) _class slot-name _fn)
|
|
|
|
"Set default values for the SESSION and CONN-INFO slots of MANAGER.
|
|
|
|
When a MANAGER's `jupyter-connection' slots are missing set them
|
|
|
|
to their default values. For the `session' slot, set it to a new
|
|
|
|
`jupyter-session' with `:key' set to a new UUID. For the
|
|
|
|
`conn-info' slot, set it to the plist returned by a call to
|
|
|
|
`jupyter-create-connection-info' with `:kernel-name' being the
|
|
|
|
MANAGER's name slot and `:key' being the key of MANAGER's
|
|
|
|
session."
|
|
|
|
(cond
|
|
|
|
((eq slot-name 'session)
|
|
|
|
(oset manager session (jupyter-session :key (jupyter-new-uuid))))
|
|
|
|
((eq slot-name 'conn-info)
|
|
|
|
(oset manager conn-info (jupyter-create-connection-info
|
|
|
|
:kernel-name (oref manager name)
|
|
|
|
:key (jupyter-session-key (oref manager session)))))
|
|
|
|
(t (cl-call-next-method))))
|
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(cl-defmethod jupyter-make-client ((manager jupyter-kernel-manager) class &rest slots)
|
|
|
|
"Make a new client from CLASS connected to MANAGER's kernel.
|
|
|
|
CLASS should be a subclass of `jupyter-kernel-client', a new
|
|
|
|
instance of CLASS initialized with SLOTS is returned configured
|
|
|
|
to connect to MANAGER's kernel."
|
|
|
|
(unless (child-of-class-p class 'jupyter-kernel-client)
|
2018-01-06 16:42:55 -06:00
|
|
|
(signal 'wrong-type-argument (list '(subclass jupyter-kernel-client) class)))
|
2018-01-04 23:03:18 -06:00
|
|
|
(let ((client (apply #'make-instance class slots)))
|
2018-01-07 14:48:11 -06:00
|
|
|
(oset client parent-instance manager)
|
2018-01-06 16:42:55 -06:00
|
|
|
(jupyter-initialize-connection client)
|
2018-01-04 23:03:18 -06:00
|
|
|
client))
|
|
|
|
|
|
|
|
(defun jupyter--kernel-sentinel (manager kernel event)
|
|
|
|
(cond
|
|
|
|
((cl-loop for type in '("exited" "failed" "finished" "killed" "deleted")
|
|
|
|
thereis (string-prefix-p type event))
|
2018-01-16 11:51:16 -06:00
|
|
|
(and (buffer-live-p (process-buffer kernel))
|
|
|
|
(kill-buffer (process-buffer kernel)))
|
2018-01-14 13:54:02 -06:00
|
|
|
(when (file-exists-p (oref manager conn-file))
|
|
|
|
(delete-file (oref manager conn-file)))
|
2018-01-04 23:03:18 -06:00
|
|
|
(oset manager kernel nil)
|
2018-01-13 22:06:41 -06:00
|
|
|
(oset manager conn-file nil))))
|
2018-01-04 23:03:18 -06:00
|
|
|
|
2018-01-13 22:04:06 -06:00
|
|
|
(defun jupyter--start-kernel (manager kernel-name env args)
|
2018-01-04 23:03:18 -06:00
|
|
|
"Start a kernel.
|
2018-01-06 16:42:55 -06:00
|
|
|
A kernel named KERNEL-NAME is started using ARGS. The name of the
|
|
|
|
command used to start the kernel subprocess should be the first
|
|
|
|
element of ARGS and the rest of the elements of ARGS are the
|
|
|
|
command line parameters passed to the command. If ENV is non-nil,
|
|
|
|
then it should be a plist containing environment variable names
|
|
|
|
as keywords along with their corresponding values. These will be
|
|
|
|
set as the process environment before starting the kernel.
|
|
|
|
|
|
|
|
Return the newly created kernel process."
|
|
|
|
(let* ((process-environment
|
2018-01-04 23:03:18 -06:00
|
|
|
(append
|
|
|
|
;; The first entry takes precedence when duplicated variables
|
|
|
|
;; are found in `process-environment'
|
|
|
|
(cl-loop
|
|
|
|
for e on env by #'cddr
|
|
|
|
for k = (car e)
|
|
|
|
for v = (cadr e)
|
|
|
|
collect (format "%s=%s" (cl-subseq (symbol-name k) 1) v))
|
2018-01-13 22:04:06 -06:00
|
|
|
process-environment))
|
|
|
|
(proc (apply #'start-process
|
|
|
|
(format "jupyter-kernel-%s" kernel-name)
|
2018-01-16 11:51:16 -06:00
|
|
|
(generate-new-buffer
|
|
|
|
(format " *jupyter-kernel[%s]*" kernel-name))
|
|
|
|
(car args) (cdr args))))
|
2018-01-13 22:04:06 -06:00
|
|
|
(prog1 proc
|
|
|
|
(set-process-sentinel
|
|
|
|
proc (apply-partially #'jupyter--kernel-sentinel manager)))))
|
2018-01-04 23:03:18 -06:00
|
|
|
|
|
|
|
;; TODO: Allow passing arguments like a different kernel file name or different
|
|
|
|
;; ports and arguments to the kernel
|
2018-01-12 18:09:30 -06:00
|
|
|
(cl-defmethod jupyter-start-kernel ((manager jupyter-kernel-manager) &optional timeout)
|
2018-01-04 23:03:18 -06:00
|
|
|
"Start a kernel and associate it with MANAGER.
|
|
|
|
|
|
|
|
The MANAGER's `name' property is passed to
|
|
|
|
`jupyter-find-kernelspec' in order to find the kernel to start,
|
|
|
|
this means that `name' can be a prefix of a kernel name as well
|
|
|
|
as a full kernel name. For example, if `name' is \"julia\" it
|
|
|
|
will match the full kernel names \"julia-0.6\", \"julia-0.4\",
|
|
|
|
etc. The kernel used will be the first one matched from the list
|
|
|
|
of kernels returned by:
|
|
|
|
|
|
|
|
jupyter kernelspec list
|
|
|
|
|
|
|
|
If a valid kernel is found, its kernelspec is used to start a new
|
|
|
|
kernel. Starting a kernel involves the following steps:
|
|
|
|
|
2018-01-13 13:40:32 -06:00
|
|
|
1. Generate a new connection info plist with random ports for the
|
2018-01-04 23:03:18 -06:00
|
|
|
channels. See `jupyter-create-connection-info'.
|
|
|
|
|
2018-01-13 13:40:32 -06:00
|
|
|
2. Assign a new `jupyter-session' to the MANAGER using the
|
|
|
|
generated key from the connection info.
|
2018-01-04 23:03:18 -06:00
|
|
|
|
2018-01-13 13:40:32 -06:00
|
|
|
3. Write the connection info to file in the Jupyter runtime
|
|
|
|
directory (found using \"jupyter --runtime-dir\")
|
2018-01-04 23:03:18 -06:00
|
|
|
|
2018-01-13 13:40:32 -06:00
|
|
|
4. Start a kernel subprocess passing the connection info file as
|
|
|
|
the {connection_file} argument in the kernelspec argument
|
|
|
|
vector of the kernel."
|
2018-01-04 23:03:18 -06:00
|
|
|
(let ((kname-spec (jupyter-find-kernelspec (oref manager name))))
|
|
|
|
(unless kname-spec
|
|
|
|
(error "No kernel found that starts with name (%s)" (oref manager name)))
|
2018-01-17 20:37:34 -06:00
|
|
|
(cl-destructuring-bind (kernel-name . (_dir . spec)) kname-spec
|
2018-01-13 22:10:18 -06:00
|
|
|
;; Ensure we use the full name of the kernel since
|
|
|
|
;; `jupyter-find-kernelspec' accepts a prefix of a kernel
|
2018-01-04 23:03:18 -06:00
|
|
|
(oset manager name kernel-name)
|
2018-01-17 20:37:34 -06:00
|
|
|
(oset manager spec spec)
|
2018-01-13 13:40:32 -06:00
|
|
|
;; NOTE: `jupyter-connection' fields are shared between other
|
|
|
|
;; `jupyter-connection' objects. The `jupyter-kernel-manager' sets
|
|
|
|
;; defaults for these when their slots are unbound, see `slot-unbound'.
|
2018-01-06 16:42:55 -06:00
|
|
|
(let* ((key (jupyter-session-key (oref manager session)))
|
2018-01-08 22:26:19 -06:00
|
|
|
(resource-dir (string-trim-right
|
|
|
|
(shell-command-to-string
|
|
|
|
"jupyter --runtime-dir")))
|
2018-01-04 23:03:18 -06:00
|
|
|
(conn-file (expand-file-name
|
2018-01-06 16:42:55 -06:00
|
|
|
(concat "kernel-" key ".json")
|
2018-01-08 22:26:19 -06:00
|
|
|
resource-dir)))
|
2018-01-13 22:10:18 -06:00
|
|
|
;; Write the connection info file
|
|
|
|
(with-temp-file (oset manager conn-file conn-file)
|
2018-01-04 23:03:18 -06:00
|
|
|
(let ((json-encoding-pretty-print t))
|
2018-01-08 13:26:09 -06:00
|
|
|
(insert (json-encode-plist (oref manager conn-info)))))
|
2018-01-06 16:42:55 -06:00
|
|
|
;; Start the process
|
|
|
|
(let ((atime (nth 4 (file-attributes conn-file)))
|
|
|
|
(proc (jupyter--start-kernel
|
2018-01-13 22:04:06 -06:00
|
|
|
manager kernel-name (plist-get spec :env)
|
2018-01-06 16:42:55 -06:00
|
|
|
(cl-loop
|
|
|
|
for arg in (plist-get spec :argv)
|
|
|
|
if (equal arg "{connection_file}") collect conn-file
|
2018-01-08 22:26:19 -06:00
|
|
|
else if (equal arg "{resource_dir}") collect resource-dir
|
2018-01-06 16:42:55 -06:00
|
|
|
else collect arg))))
|
|
|
|
;; Block until the kernel reads the connection file
|
|
|
|
(with-timeout
|
2018-01-12 18:09:30 -06:00
|
|
|
((or timeout 5)
|
|
|
|
(delete-process proc)
|
|
|
|
(error "Kernel did not read connection file within timeout"))
|
2018-01-06 16:42:55 -06:00
|
|
|
(while (equal atime (nth 4 (file-attributes conn-file)))
|
|
|
|
(sleep-for 0 100)))
|
2018-01-08 22:27:13 -06:00
|
|
|
(oset manager kernel proc)
|
2018-01-06 16:42:55 -06:00
|
|
|
(oset manager conn-file (expand-file-name
|
|
|
|
(format "kernel-%d.json" (process-id proc))
|
|
|
|
(file-name-directory conn-file)))
|
|
|
|
(rename-file conn-file (oref manager conn-file))
|
2018-01-04 23:03:18 -06:00
|
|
|
(jupyter-start-channels manager)
|
|
|
|
manager)))))
|
|
|
|
|
|
|
|
(cl-defmethod jupyter-start-channels ((manager jupyter-kernel-manager))
|
|
|
|
"Start a control channel on MANAGER."
|
2018-01-17 20:37:34 -06:00
|
|
|
(let ((channel (oref manager control-channel)))
|
|
|
|
(if channel
|
|
|
|
(unless (jupyter-channel-alive-p channel)
|
2018-01-04 23:03:18 -06:00
|
|
|
(jupyter-start-channel
|
2018-01-17 20:37:34 -06:00
|
|
|
channel :identity (jupyter-session-id (oref manager session))))
|
2018-01-04 23:03:18 -06:00
|
|
|
(let ((conn-info (oref manager conn-info)))
|
|
|
|
(oset manager control-channel
|
|
|
|
(jupyter-control-channel
|
|
|
|
:endpoint (format "%s://%s:%d"
|
|
|
|
(plist-get conn-info :transport)
|
|
|
|
(plist-get conn-info :ip)
|
|
|
|
(plist-get conn-info :control_port))))
|
|
|
|
(jupyter-start-channels manager)))))
|
|
|
|
|
|
|
|
(cl-defmethod jupyter-stop-channels ((manager jupyter-kernel-manager))
|
|
|
|
"Stop the control channel on MANAGER."
|
2018-01-17 20:37:34 -06:00
|
|
|
(let ((channel (oref manager control-channel)))
|
|
|
|
(when channel
|
|
|
|
(jupyter-stop-channel channel)
|
2018-01-04 23:03:18 -06:00
|
|
|
(oset manager control-channel nil))))
|
|
|
|
|
2018-01-13 22:08:14 -06:00
|
|
|
(cl-defmethod jupyter-shutdown-kernel ((manager jupyter-kernel-manager) &optional restart timeout)
|
2018-01-04 23:03:18 -06:00
|
|
|
(when (jupyter-kernel-alive-p manager)
|
2018-01-11 12:13:33 -06:00
|
|
|
(let ((session (oref manager session))
|
2018-01-13 22:08:14 -06:00
|
|
|
(sock (oref (oref manager control-channel) socket))
|
|
|
|
(msg (jupyter-message-shutdown-request :restart restart)))
|
|
|
|
(jupyter-send session sock "shutdown_request" msg)
|
2018-01-11 12:13:33 -06:00
|
|
|
(with-timeout ((or timeout 1)
|
|
|
|
(delete-process (oref manager kernel))
|
2018-01-16 11:52:10 -06:00
|
|
|
(message "Kernel did not shutdown by request (%s)" (oref manager name)))
|
2018-01-11 12:13:33 -06:00
|
|
|
(while (jupyter-kernel-alive-p manager)
|
|
|
|
(sleep-for 0.01)))
|
2018-01-13 22:08:14 -06:00
|
|
|
(if restart
|
|
|
|
(jupyter-start-kernel manager)
|
|
|
|
(jupyter-stop-channels manager)))))
|
2018-01-08 22:30:00 -06:00
|
|
|
|
|
|
|
(cl-defmethod jupyter-interrupt-kernel ((manager jupyter-kernel-manager) &optional timeout)
|
2018-01-17 20:37:34 -06:00
|
|
|
(pcase (plist-get (oref manager spec) :interrupt_mode)
|
2018-01-08 22:30:00 -06:00
|
|
|
("message"
|
|
|
|
(let ((session (oref manager session))
|
2018-01-13 22:10:18 -06:00
|
|
|
(sock (oref (oref manager control-channel) socket))
|
|
|
|
(msg (jupyter-message-interrupt-request)))
|
|
|
|
(jupyter-send session sock "interrupt_request" msg)
|
2018-01-16 11:52:10 -06:00
|
|
|
(with-timeout ((or timeout 1)
|
|
|
|
(message "No interrupt reply from kernel (%s)" (oref manager name)))
|
2018-01-08 22:30:00 -06:00
|
|
|
(while (condition-case nil
|
2018-01-13 22:10:18 -06:00
|
|
|
(prog1 nil (jupyter-recv session sock zmq-NOBLOCK))
|
2018-01-08 22:30:00 -06:00
|
|
|
(zmq-EAGAIN t))
|
|
|
|
(sleep-for 0.01)))))
|
2018-01-13 22:10:18 -06:00
|
|
|
(_ (interrupt-process (oref manager kernel) t))))
|
2018-01-04 23:03:18 -06:00
|
|
|
|
|
|
|
(cl-defmethod jupyter-kernel-alive-p ((manager jupyter-kernel-manager))
|
2018-01-13 22:11:09 -06:00
|
|
|
(when (oref manager kernel)
|
|
|
|
(process-live-p (oref manager kernel))))
|
2018-01-04 23:03:18 -06:00
|
|
|
|
2018-01-12 18:14:04 -06:00
|
|
|
(defun jupyter--wait-until-startup (client &optional timeout)
|
|
|
|
"Wait until CLIENT receives a status: starting message.
|
|
|
|
Return non-nil if the startup message was received by CLIENT
|
|
|
|
within TIMEOUT seconds otherwise return nil. TIMEOUT defaults to
|
|
|
|
1 s. Note that there are no checks to determine if the kernel
|
|
|
|
CLIENT is connected to has already been started."
|
|
|
|
(let* ((started nil)
|
|
|
|
(cb (lambda (msg)
|
|
|
|
(setq started
|
|
|
|
(equal (jupyter-message-get msg :execution_state)
|
|
|
|
"starting"))))
|
|
|
|
(jupyter-include-other-output t))
|
|
|
|
(jupyter-add-hook client 'jupyter-iopub-message-hook cb)
|
|
|
|
(prog1
|
|
|
|
(with-timeout ((or timeout 1) nil)
|
|
|
|
(while (not started)
|
|
|
|
(sleep-for 0.01))
|
|
|
|
t)
|
|
|
|
(jupyter-remove-hook client 'jupyter-iopub-message-hook cb))))
|
|
|
|
|
2018-01-06 16:42:55 -06:00
|
|
|
(defun jupyter-start-new-kernel (kernel-name &optional client-class)
|
2018-01-08 18:11:08 -06:00
|
|
|
"Start a managed Jupyter kernel.
|
|
|
|
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-available-kernelspecs' that has a kernel name with
|
|
|
|
KERNEL-NAME as prefix will be used. Optional argument
|
2018-01-13 13:40:32 -06:00
|
|
|
CLIENT-CLASS should be a subclass of `jupyer-kernel-client' which
|
|
|
|
will be used to initialize a new client connected to the new
|
|
|
|
kernel. CLIENT-CLASS defaults to `jupyter-kernel-client'.
|
2018-01-08 18:11:08 -06:00
|
|
|
|
|
|
|
Return a cons cell (KM . KC) where KM is the
|
2018-01-13 13:40:32 -06:00
|
|
|
`jupyter-kernel-manager' that manages the lifetime of the kernel
|
|
|
|
subprocess. KC is a new client connected to the kernel and whose
|
|
|
|
class is CLIENT-CLASS. The client is connected to the kernel with
|
|
|
|
all channels listening for messages and the heartbeat channel
|
|
|
|
un-paused."
|
2018-01-06 16:42:55 -06:00
|
|
|
(setq client-class (or client-class 'jupyter-kernel-client))
|
|
|
|
(unless (child-of-class-p client-class 'jupyter-kernel-client)
|
|
|
|
(signal 'wrong-type-argument
|
|
|
|
(list '(subclass jupyter-kernel-client) client-class)))
|
|
|
|
(let (km kc)
|
|
|
|
(setq km (jupyter-kernel-manager :name kernel-name))
|
|
|
|
(setq kc (jupyter-make-client km client-class))
|
2018-01-13 13:36:15 -06:00
|
|
|
(unwind-protect
|
2018-01-08 14:01:51 -06:00
|
|
|
(progn
|
2018-01-13 22:10:38 -06:00
|
|
|
(jupyter-start-channels kc)
|
|
|
|
(jupyter-hb-unpause (oref kc hb-channel))
|
|
|
|
(jupyter-start-kernel km 10)
|
2018-01-12 18:14:04 -06:00
|
|
|
(unless (jupyter--wait-until-startup kc 10)
|
|
|
|
(error "Kernel did not send startup message"))
|
2018-01-17 20:28:46 -06:00
|
|
|
(let* ((jupyter-inhibit-handlers t)
|
|
|
|
(info (jupyter-wait-until-received :kernel-info-reply
|
|
|
|
(jupyter-kernel-info-request kc)
|
2018-01-18 21:14:27 -06:00
|
|
|
2)))
|
|
|
|
(if info (oset km kernel-info (jupyter-message-content info))
|
2018-01-13 13:36:15 -06:00
|
|
|
(error "Kernel did not respond to kernel-info request")))
|
|
|
|
(cons km kc))
|
2018-01-18 16:06:22 -06:00
|
|
|
(destructor kc)
|
|
|
|
(destructor km))))
|
2018-01-06 16:42:55 -06:00
|
|
|
|
2018-01-04 23:03:18 -06:00
|
|
|
(provide 'jupyter-kernel-manager)
|
2018-01-08 21:38:32 -06:00
|
|
|
|
|
|
|
;;; jupyter-kernel-manager.el ends here
|