2018-01-08 21:38:32 -06:00
|
|
|
;;; jupyter-client.el --- A Jupyter kernel client -*- lexical-binding: t -*-
|
|
|
|
|
|
|
|
;; Copyright (C) 2018 Nathaniel Nicandro
|
|
|
|
|
|
|
|
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
|
|
|
;; Created: 06 Jan 2018
|
|
|
|
;; Version: 0.0.1
|
|
|
|
|
|
|
|
;; 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)
|
2017-12-13 11:27:13 -06:00
|
|
|
(require 'jupyter-channels)
|
|
|
|
(require 'jupyter-messages)
|
2018-01-04 23:03:18 -06:00
|
|
|
(eval-when-compile (require 'cl))
|
2018-01-04 20:58:28 -06:00
|
|
|
|
|
|
|
(defvar jupyter--debug nil
|
|
|
|
"Set to non-nil to emit sent and received messages to *Messages*.")
|
|
|
|
|
|
|
|
(defvar jupyter-default-timeout 1
|
|
|
|
"The default timeout in seconds for `jupyter-wait-until'.")
|
2017-12-14 13:39:30 -06:00
|
|
|
|
2017-12-22 00:50:56 -06:00
|
|
|
(defclass jupyter-kernel-client (jupyter-connection)
|
2017-12-19 18:02:55 -06:00
|
|
|
((requests
|
2017-12-13 11:27:13 -06:00
|
|
|
:type hash-table
|
2017-12-15 22:26:21 -06:00
|
|
|
:initform (make-hash-table :test 'equal)
|
2017-12-13 11:27:13 -06:00
|
|
|
:documentation "A hash table with message ID's as keys. This
|
|
|
|
is used to register callback functions to run once a reply from
|
|
|
|
a previously sent request is received. See
|
2018-01-02 01:02:35 -06:00
|
|
|
`jupyter-add-callback'. Note that this is also used to filter
|
|
|
|
received messages that originated from a previous request by
|
|
|
|
this client. Whenever the client sends a message in which a
|
2017-12-15 18:14:28 -06:00
|
|
|
reply is expected, it sets an entry in this table to represent
|
|
|
|
the fact that the message has been sent. So if there is a
|
|
|
|
non-nil value for a message ID it means that a message has been
|
|
|
|
sent and the client is expecting a reply from the kernel.")
|
2017-12-17 02:39:16 -06:00
|
|
|
(ioloop
|
|
|
|
:type (or null process)
|
2017-12-16 18:54:40 -06:00
|
|
|
:initform nil
|
|
|
|
:documentation "The process which polls for events on all
|
|
|
|
live channels of the client.")
|
2018-01-08 22:34:42 -06:00
|
|
|
;; NOTE: With the current implementation all channels except the heartbeat
|
|
|
|
;; channel actually communicate with the kernel through the ioloop
|
|
|
|
;; subprocess. This means that the socket field of the channels are
|
|
|
|
;; not actually used. They are mainly used to dispatch received
|
|
|
|
;; messages from the IOLoop subprocess and to hold the endpoint
|
|
|
|
;; information of the connection.
|
2017-12-13 11:27:13 -06:00
|
|
|
(shell-channel
|
2017-12-21 18:12:25 -06:00
|
|
|
:type (or null jupyter-shell-channel)
|
2018-01-08 22:33:07 -06:00
|
|
|
:initform nil
|
2017-12-13 11:27:13 -06:00
|
|
|
:initarg :shell-channel
|
|
|
|
:documentation "The shell channel.")
|
|
|
|
(iopub-channel
|
2017-12-21 18:12:25 -06:00
|
|
|
:type (or null jupyter-iopub-channel)
|
|
|
|
:initform nil
|
2017-12-13 11:27:13 -06:00
|
|
|
:initarg :iopub-channel
|
|
|
|
:documentation "The IOPub channel.")
|
|
|
|
(hb-channel
|
2017-12-21 18:12:25 -06:00
|
|
|
:type (or null jupyter-hb-channel)
|
|
|
|
:initform nil
|
2017-12-13 11:27:13 -06:00
|
|
|
:initarg :hb-channel
|
|
|
|
:documentation "The heartbeat channel.")
|
|
|
|
(stdin-channel
|
2017-12-21 18:12:25 -06:00
|
|
|
:type (or null jupyter-stdin-channel)
|
|
|
|
:initform nil
|
2017-12-13 11:27:13 -06:00
|
|
|
:initarg :stdin-channel
|
|
|
|
:documentation "The stdin channel.")))
|
|
|
|
|
2018-01-08 13:59:42 -06:00
|
|
|
(cl-defmethod initialize-instance ((client jupyter-kernel-client) &rest _slots)
|
|
|
|
(cl-call-next-method)
|
|
|
|
;; Add a catch all request for headless messages received from the kernel.
|
|
|
|
;; Set -id to a non-nil value so that `jupyter-request-id' works.
|
|
|
|
(puthash nil (make-jupyter-request :-id "" :run-handlers-p nil)
|
|
|
|
(oref client requests)))
|
|
|
|
|
|
|
|
(defun jupyter-missing-request (client)
|
|
|
|
(cl-check-type client jupyter-kernel-client)
|
|
|
|
(gethash nil (oref client requests)))
|
|
|
|
|
2018-01-06 16:37:18 -06:00
|
|
|
(cl-defmethod jupyter-initialize-connection ((client jupyter-kernel-client)
|
|
|
|
&optional file-or-plist)
|
2018-01-04 20:59:01 -06:00
|
|
|
"Initialize CLIENT with a connection FILE-OR-PLIST.
|
2018-01-06 16:37:18 -06:00
|
|
|
When FILE-OR-PLIST is a file name, read the JSON connection
|
|
|
|
information from the file and initialize CLIENT's connection and
|
|
|
|
channels from the file's contents. When FILE-OR-PLIST is a plist,
|
|
|
|
initialize CLIENT's connection and channels from the plist. When
|
|
|
|
FILE-OR-PLIST is nil, then the `conn-info' slot of CLIENT is used
|
|
|
|
to initialize the connection. The necessary keys to initialize a
|
|
|
|
connection can be found at
|
|
|
|
http://jupyter-client.readthedocs.io/en/latest/kernels.html#connection-files.
|
|
|
|
|
|
|
|
As a side effect, if CLIENT is already connected to a kernel its
|
|
|
|
connection is terminated before initializing."
|
|
|
|
(let ((conn-info (if file-or-plist
|
|
|
|
(oset client conn-info
|
|
|
|
(if (json-plist-p file-or-plist) file-or-plist
|
|
|
|
(let ((json-array-type 'list)
|
|
|
|
(json-object-type 'plist)
|
|
|
|
(json-false nil))
|
|
|
|
(json-read-file file-or-plist))))
|
2018-01-11 00:26:03 -06:00
|
|
|
(or (ignore-errors (oref client conn-info))
|
|
|
|
(signal 'unbound-slot
|
|
|
|
(list 'json-plist client 'conn-info))))))
|
2017-12-22 00:50:56 -06:00
|
|
|
(cl-destructuring-bind
|
2018-01-07 19:54:52 -06:00
|
|
|
(&key shell_port iopub_port stdin_port hb_port ip
|
|
|
|
key transport signature_scheme
|
2017-12-22 00:50:56 -06:00
|
|
|
&allow-other-keys)
|
|
|
|
conn-info
|
|
|
|
(when (and (> (length key) 0)
|
|
|
|
(not (functionp (intern signature_scheme))))
|
|
|
|
(error "Unsupported signature scheme: %s" signature_scheme))
|
|
|
|
;; Stop the channels if connected to some other kernel
|
|
|
|
(jupyter-stop-channels client)
|
2018-01-11 00:26:03 -06:00
|
|
|
;; A kernel manager may have already initialized the session, see
|
|
|
|
;; `jupyter-make-client'
|
|
|
|
(unless (and (ignore-errors (oref client session))
|
|
|
|
(equal (jupyter-session-key (oref client session)) key))
|
|
|
|
(oset client session (jupyter-session :key key)))
|
|
|
|
(cl-loop
|
|
|
|
with addr = (concat transport "://" ip)
|
|
|
|
for (channel . port) in `((stdin-channel . ,stdin_port)
|
|
|
|
(shell-channel . ,shell_port)
|
|
|
|
(hb-channel . ,hb_port)
|
|
|
|
(iopub-channel . ,iopub_port))
|
|
|
|
for class = (intern (concat "jupyter-" (symbol-name channel)))
|
|
|
|
do (setf (slot-value client channel)
|
|
|
|
(make-instance
|
|
|
|
class
|
|
|
|
;; So channels have access to the client's session
|
|
|
|
:parent-instance client
|
|
|
|
:endpoint (format "%s:%d" addr port)))))))
|
2017-12-22 00:50:56 -06:00
|
|
|
|
2017-12-15 18:21:54 -06:00
|
|
|
;;; Lower level sending/receiving
|
|
|
|
|
2017-12-31 15:22:48 -06:00
|
|
|
(cl-defmethod jupyter-send ((client jupyter-kernel-client)
|
|
|
|
channel
|
|
|
|
type
|
|
|
|
message
|
|
|
|
&optional flags)
|
2018-01-08 18:11:08 -06:00
|
|
|
"Send a message on CLIENT's CHANNEL.
|
|
|
|
Return a `jupyter-request' representing the sent message. CHANNEL
|
|
|
|
is one of the channel's of CLIENT. TYPE is one of the values in
|
|
|
|
`jupyter-message-types' and is the type of the MESSAGE. If FLAGS
|
|
|
|
is non-nil, it has the same meaning as FLAGS in `zmq-send'. You
|
|
|
|
can manipulate how to handle messages received in response to the
|
|
|
|
sent message, see `jupyter-add-callback' and
|
|
|
|
`jupyter-request-inhibit-handlers'."
|
2017-12-15 18:21:54 -06:00
|
|
|
(declare (indent 1))
|
2018-01-08 22:33:46 -06:00
|
|
|
(let ((ioloop (oref client ioloop)))
|
|
|
|
(unless ioloop
|
|
|
|
(signal 'wrong-type-argument (list 'process ioloop 'ioloop)))
|
|
|
|
(zmq-subprocess-send (oref client ioloop)
|
|
|
|
(list 'send (oref channel type) type message flags))
|
|
|
|
;; Anything sent to stdin is a reply not a request so don't add it to
|
|
|
|
;; `:jupyter-pending-requests.'
|
|
|
|
(unless (eq (oref channel type) :stdin)
|
|
|
|
(let ((req (make-jupyter-request)))
|
|
|
|
(jupyter--ioloop-push-request client req)
|
|
|
|
req))))
|
2017-12-15 18:21:54 -06:00
|
|
|
|
2018-01-07 23:21:53 -06:00
|
|
|
;;; IOLoop subprocess communication
|
|
|
|
|
|
|
|
(defmacro jupyter--ioloop-do-command (session poller channels)
|
|
|
|
"Read and execute a command from stdin.
|
|
|
|
SESSION is a variable bound to a `jupyter-session' object, POLLER
|
|
|
|
is a variable bound to a `zmq-poller' object. and CHANNELS is a
|
|
|
|
variable bound to an alist of (SOCK . CTYPE) pairs where SOCK is
|
|
|
|
a `zmq-socket' representing a `jupyter-channel' that has a
|
|
|
|
channel type of CTYPE.
|
|
|
|
|
|
|
|
The parent Emacs process should send the ioloop subprocess cons
|
|
|
|
cells of the form:
|
|
|
|
|
|
|
|
(CMD . DATA)
|
|
|
|
|
|
|
|
where CMD is a symbol representing the command to perform and
|
|
|
|
DATA is any information needed to run the command. The available
|
|
|
|
commands that an ioloop subprocess can perform are:
|
|
|
|
|
|
|
|
- '(quit) :: Terminate the ioloop.
|
|
|
|
|
|
|
|
- '(start-channel . CTYPE) :: Re-connect the socket in CHANNELS
|
|
|
|
that represents a `jupyter-channel' whose type is CTYPE.
|
|
|
|
|
|
|
|
- '(stop-channel . CTYPE) :: Disconnect the socket that
|
|
|
|
corresponds to a channel with type CTYPE in CHANNELS.
|
|
|
|
|
|
|
|
- '(send CTYPE . ARGS) :: Call `jupyter-send' with arguments
|
|
|
|
SESSION, the socket corresponding to CTYPE, and ARGS. Where
|
|
|
|
ARGS is a list of the remaining arguments necessary for the
|
|
|
|
`jupyter-send' call.
|
|
|
|
|
|
|
|
Any other command sent to the subprocess will be ignored."
|
|
|
|
;; TODO: Would like to convert this to `pcase' but there seems to be issues
|
|
|
|
;; with `pcase', it never matches the pattern correctly in the subprocess
|
|
|
|
;; even though it matches perfectly well in the parent emacs
|
|
|
|
`(cl-destructuring-bind (cmd . args)
|
|
|
|
(zmq-subprocess-read)
|
|
|
|
(cl-case cmd
|
|
|
|
(send
|
|
|
|
(cl-destructuring-bind (ctype . args) args
|
|
|
|
(let ((sock (car (rassoc ctype ,channels))))
|
|
|
|
(zmq-prin1
|
|
|
|
(list 'sent ctype (apply #'jupyter-send ,session sock args))))))
|
|
|
|
(start-channel
|
|
|
|
(let ((sock (car (rassoc args ,channels))))
|
|
|
|
(zmq-connect sock (zmq-socket-get sock zmq-LAST_ENDPOINT))
|
|
|
|
(zmq-poller-register ,poller sock zmq-POLLIN)))
|
|
|
|
(stop-channel
|
|
|
|
(let ((sock (car (rassoc args ,channels))))
|
|
|
|
(zmq-poller-unregister ,poller sock)
|
|
|
|
(condition-case err
|
|
|
|
(zmq-disconnect
|
|
|
|
sock (zmq-socket-get sock zmq-LAST_ENDPOINT))
|
|
|
|
(zmq-ENOENT nil))))
|
|
|
|
(quit
|
|
|
|
(signal 'quit nil))
|
|
|
|
(otherwise (error "Unhandled command (%s)" cmd)))))
|
|
|
|
|
|
|
|
(defmacro jupyter--ioloop-queue-message (messages priorities elem)
|
|
|
|
"Add a single message to MESSAGES.
|
|
|
|
MESSAGES should be a variable name which is bound to a list or
|
|
|
|
nil, PRIORITIES should be a variable name which is bound to an
|
|
|
|
alist of (CTYPE . PRIORITY) pairs where CTYPE is a
|
|
|
|
`jupyter-channel' type and PRIORITY is a number representing the
|
|
|
|
priority of the channel. ELEM should be a cons cell
|
|
|
|
|
|
|
|
(CTYPE . IDENTS-MSG)
|
|
|
|
|
|
|
|
where CTYPE is the `jupyter-channel' type that IDENTS-MSG was
|
|
|
|
received on. Note that IDENTS-MSG should be the cons cell
|
|
|
|
returned by `jupyter-recv'.
|
|
|
|
|
|
|
|
Multiple calls to the expansion of
|
|
|
|
`jupyter--ioloop-queue-message' with the same MESSAGES list will
|
|
|
|
place ELEM on the list at a position that depends on the
|
|
|
|
`jupyter-message-time' of MSG and the priority of CTYPE. MESSAGES
|
|
|
|
will sorted by increasing `jupyter-message-time' of its messages
|
|
|
|
and in case two messages have `equal' message times, the message
|
|
|
|
whose channel type has a higher priority will come before the
|
|
|
|
message that has a channel type with the lower priority."
|
|
|
|
(declare (indent 2))
|
|
|
|
`(let ((elem ,elem))
|
|
|
|
(if (null ,messages) (push elem ,messages)
|
|
|
|
;; Put elem in its sorted position
|
2018-01-11 00:22:19 -06:00
|
|
|
(let ((ctype (car elem))
|
2018-01-07 23:21:53 -06:00
|
|
|
(mt (jupyter-message-time (cddr elem)))
|
|
|
|
(head ,messages)
|
|
|
|
(tail ,messages))
|
|
|
|
(while (and
|
|
|
|
tail
|
|
|
|
;; Non-nil if msg should come after tail
|
|
|
|
(let ((tctype (caar tail))
|
|
|
|
(tmt (jupyter-message-time (cddar tail))))
|
|
|
|
(or (time-less-p tmt mt)
|
|
|
|
(when (equal mt tmt)
|
|
|
|
(< (alist-get ctype ,priorities)
|
|
|
|
(alist-get tctype ,priorities))))))
|
|
|
|
(setq
|
|
|
|
head tail
|
|
|
|
tail (cdr tail)))
|
|
|
|
(if (eq head tail) (setq ,messages (cons elem head))
|
|
|
|
(setcdr head (cons elem tail)))))))
|
|
|
|
|
2018-01-11 12:04:42 -06:00
|
|
|
(defmacro jupyter--ioloop-collect-messages
|
|
|
|
(session poller channels messages priorities timeout)
|
|
|
|
"Collect messages from kernel.
|
|
|
|
SESSION, POLLER, CHANNELS, MESSAGES, PRIORITIES, and TIMEOUT
|
|
|
|
should all be variable names bound to objects with the following
|
|
|
|
meanings:
|
|
|
|
|
|
|
|
SESSION - A `jupyter-session'
|
|
|
|
|
|
|
|
POLLER - A `zmq-poller'
|
|
|
|
|
|
|
|
CHANNELS - An alist of (SOCK . CTYPE) pairs where sock is a
|
|
|
|
`zmq-socket' representing a `jupyter-channel' with
|
|
|
|
type CTYPE.
|
|
|
|
|
|
|
|
MESSAGES - A variable in which to store the collected list of
|
|
|
|
messages collected during this polling period. If the
|
|
|
|
variable is already bound to a list, new messages
|
|
|
|
added to it will be sorted based on the `:date' field
|
|
|
|
of the Jupyter message also taking into account
|
|
|
|
PRIORITIES.
|
|
|
|
|
|
|
|
PRIORITIES - An alist of (CTYPE . PRIORITY) pairs where CTYPE is
|
|
|
|
a `jupyter-channel' type with PRIORITY, a number. If
|
|
|
|
one channel has a higher priority than another and
|
|
|
|
two messages, one from each channel, have the same
|
|
|
|
`:date' field, the message with the higher channel
|
|
|
|
priority will have its message come before the
|
|
|
|
message whose channel has a lower priority in the
|
|
|
|
sorted order."
|
|
|
|
`(let ((events (zmq-poller-wait-all ,poller 4 ,timeout)))
|
|
|
|
(when (alist-get 0 events)
|
|
|
|
;; Got input from stdin, do the command it
|
|
|
|
;; specifies
|
|
|
|
(setf (alist-get 0 events nil 'remove) nil)
|
|
|
|
(jupyter--ioloop-do-command ,session ,poller ,channels))
|
|
|
|
(dolist (sock (mapcar #'car events))
|
|
|
|
(jupyter--ioloop-queue-message ,messages ,priorities
|
|
|
|
(cons (alist-get sock channels)
|
|
|
|
(jupyter-recv ,session sock))))
|
|
|
|
events))
|
|
|
|
|
|
|
|
;; TODO: Make this more debuggable, I've spent hours wondering why I wasn't
|
|
|
|
;; receiving messages only to find out (caar elem) should have been (car elem)
|
|
|
|
;; in `jupyter--ioloop-queue-message'. For some reason the `condition-case' in
|
|
|
|
;; `zmq--init-subprocess' is not sending back the error.
|
2017-12-17 02:39:16 -06:00
|
|
|
(defun jupyter--ioloop (client)
|
2018-01-07 23:21:53 -06:00
|
|
|
"Return the function used for communicating with CLIENT's kernel."
|
2018-01-11 12:04:42 -06:00
|
|
|
(let* ((session (oref client session))
|
|
|
|
(sid (jupyter-session-id session))
|
|
|
|
(skey (jupyter-session-key session))
|
|
|
|
(iopub-ep (oref (oref client iopub-channel) endpoint))
|
|
|
|
(shell-ep (oref (oref client shell-channel) endpoint))
|
|
|
|
(stdin-ep (oref (oref client stdin-channel) endpoint)))
|
2017-12-17 02:39:16 -06:00
|
|
|
`(lambda (ctx)
|
2018-01-07 23:21:53 -06:00
|
|
|
(push ,(file-name-directory (locate-library "jupyter-base")) load-path)
|
|
|
|
(require 'jupyter-channels)
|
|
|
|
(require 'jupyter-messages)
|
2018-01-11 12:04:42 -06:00
|
|
|
(let* ((session (jupyter-session :id ,sid :key ,skey))
|
|
|
|
(iopub (jupyter-connect-channel :iopub ,iopub-ep ,sid))
|
|
|
|
(shell (jupyter-connect-channel :shell ,shell-ep ,sid))
|
|
|
|
(stdin (jupyter-connect-channel :stdin ,stdin-ep ,sid))
|
2018-01-11 00:25:08 -06:00
|
|
|
(priorities '((:shell . 4)
|
|
|
|
(:iopub . 2)
|
|
|
|
(:stdin . 2)))
|
|
|
|
(channels `((,stdin . :stdin)
|
|
|
|
(,shell . :shell)
|
|
|
|
(,iopub . :iopub)))
|
2017-12-21 18:16:15 -06:00
|
|
|
(idle-count 0)
|
2018-01-06 21:02:22 -06:00
|
|
|
(timeout 20)
|
2018-01-07 23:21:53 -06:00
|
|
|
(messages nil))
|
|
|
|
(zmq-socket-set iopub zmq-SUBSCRIBE "")
|
2018-01-11 12:04:42 -06:00
|
|
|
(condition-case nil
|
2018-01-07 23:21:53 -06:00
|
|
|
(with-zmq-poller poller
|
|
|
|
;; Poll for stdin messages
|
|
|
|
(zmq-poller-register poller 0 zmq-POLLIN)
|
|
|
|
(mapc (lambda (x) (zmq-poller-register poller (car x) zmq-POLLIN))
|
|
|
|
channels)
|
|
|
|
(while t
|
2018-01-11 12:04:42 -06:00
|
|
|
(if (jupyter--ioloop-collect-messages
|
|
|
|
session poller channels messages priorities timeout)
|
|
|
|
(setq idle-count 0 timeout 20)
|
|
|
|
(setq idle-count (1+ idle-count))
|
|
|
|
;; Lengthen timeout so as to not waste CPU cycles
|
|
|
|
(when (= idle-count 100) (setq timeout 100)))
|
|
|
|
;; Pool at least some messages, but not at the cost of
|
|
|
|
;; responsiveness. If messages are being blasted at us by the
|
|
|
|
;; kernel ensure that they still get through and not pooled
|
|
|
|
;; indefinately.
|
|
|
|
(when (or (= idle-count 5) (> (length messages) 10))
|
|
|
|
(while messages
|
|
|
|
(zmq-prin1 (cons 'recvd (pop messages)))))))
|
2018-01-07 23:21:53 -06:00
|
|
|
(quit
|
|
|
|
(mapc (lambda (x)
|
|
|
|
(zmq-socket-set (car x) zmq-LINGER 0)
|
|
|
|
(zmq-close (car x)))
|
|
|
|
channels)
|
|
|
|
(zmq-prin1 (list 'quit))))))))
|
2017-12-19 18:47:57 -06:00
|
|
|
|
2018-01-02 00:56:02 -06:00
|
|
|
(defun jupyter--ioloop-pop-request (client)
|
2018-01-07 14:06:14 -06:00
|
|
|
"Remove a pending request from CLIENT's ioloop subprocess.
|
|
|
|
Specifically remove the oldest element of the ring located in the
|
|
|
|
`:jupyter-pending-requests' property of CLIENT's ioloop
|
|
|
|
subprocess."
|
2018-01-02 00:56:02 -06:00
|
|
|
(let* ((ring (process-get (oref client ioloop) :jupyter-pending-requests))
|
|
|
|
(req (ring-remove ring)))
|
|
|
|
req))
|
|
|
|
|
|
|
|
(defun jupyter--ioloop-push-request (client req)
|
2018-01-07 14:06:14 -06:00
|
|
|
"Insert a request into CLIENT's pending requests.
|
|
|
|
REQ is inserted as the newest element in CLIENT's pending
|
|
|
|
requests. See `jupyter--ioloop-pop-request' for where pending
|
|
|
|
requests are stored for CLIENT."
|
2018-01-02 00:56:02 -06:00
|
|
|
(let* ((ioloop (oref client ioloop))
|
|
|
|
(ring (or (process-get ioloop :jupyter-pending-requests)
|
|
|
|
(let ((ring (make-ring 10)))
|
|
|
|
(process-put ioloop :jupyter-pending-requests ring)
|
|
|
|
ring))))
|
|
|
|
(ring-insert+extend ring req 'grow)))
|
|
|
|
|
2018-01-08 13:57:15 -06:00
|
|
|
(defun jupyter--ioloop-sentinel (client _ioloop event)
|
2018-01-07 14:06:14 -06:00
|
|
|
"The process sentinel for CLIENT's IOLOOP subprocess.
|
|
|
|
When EVENT is one of the events signifying that the process is
|
|
|
|
dead, stop the heartbeat channel and set the IOLOOP slot to nil
|
|
|
|
in CLIENT."
|
2017-12-27 00:00:59 -06:00
|
|
|
(cond
|
2017-12-30 23:35:04 -06:00
|
|
|
((cl-loop for type in '("exited" "failed" "finished" "killed" "deleted")
|
|
|
|
thereis (string-prefix-p type event))
|
2018-01-04 17:08:41 -06:00
|
|
|
(jupyter-stop-channel (oref client hb-channel))
|
2017-12-27 00:00:59 -06:00
|
|
|
(oset client ioloop nil))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-17 02:39:16 -06:00
|
|
|
(defun jupyter--ioloop-filter (client event)
|
2018-01-06 21:04:03 -06:00
|
|
|
"The process filter for CLIENT's ioloop subprocess.
|
|
|
|
EVENT will be an s-expression emitted from the function returned
|
|
|
|
by `jupyter--ioloop'."
|
|
|
|
(pcase event
|
|
|
|
(`(sent ,ctype ,msg-id)
|
|
|
|
(when jupyter--debug
|
|
|
|
(message "SEND: %s" msg-id))
|
|
|
|
(unless (eq ctype :stdin)
|
|
|
|
;; Anything sent on stdin is a reply and therefore never added to
|
|
|
|
;; `:jupyter-pending-requests'
|
|
|
|
(let ((req (jupyter--ioloop-pop-request client)))
|
|
|
|
(setf (jupyter-request--id req) msg-id)
|
|
|
|
(puthash msg-id req (oref client requests)))))
|
2018-01-07 23:21:53 -06:00
|
|
|
(`(recvd ,ctype ,idents . ,msg)
|
2018-01-06 21:04:03 -06:00
|
|
|
(when jupyter--debug
|
|
|
|
(message "RECV: %s %s %s"
|
2018-01-07 23:21:53 -06:00
|
|
|
(jupyter-message-type msg)
|
|
|
|
(jupyter-message-parent-id msg)
|
|
|
|
(jupyter-message-content msg)))
|
2018-01-06 21:04:03 -06:00
|
|
|
(let ((channel (cl-find-if (lambda (c) (eq (oref c type) ctype))
|
|
|
|
(mapcar (lambda (x) (slot-value client x))
|
|
|
|
'(stdin-channel
|
|
|
|
shell-channel
|
|
|
|
iopub-channel)))))
|
2018-01-07 23:47:20 -06:00
|
|
|
(jupyter-queue-message channel (cons idents msg))
|
2018-01-06 21:04:03 -06:00
|
|
|
(run-with-timer 0.001 nil #'jupyter-handle-message client channel)))
|
|
|
|
('(quit)
|
|
|
|
;; Cleanup handled in sentinel
|
|
|
|
(when jupyter--debug
|
2018-01-08 22:44:32 -06:00
|
|
|
(message "CLIENT CLOSED")))))
|
2017-12-31 15:22:48 -06:00
|
|
|
|
2017-12-13 11:27:13 -06:00
|
|
|
(cl-defmethod jupyter-start-channels ((client jupyter-kernel-client)
|
|
|
|
&key (shell t)
|
|
|
|
(iopub t)
|
|
|
|
(stdin t)
|
|
|
|
(hb t))
|
2017-12-15 18:14:28 -06:00
|
|
|
"Start the pre-configured channels of CLIENT.
|
|
|
|
This function calls `jupyter-start-channel' for every channel
|
|
|
|
that has a non-nil value passed to this function. All channels
|
|
|
|
are started by default, so to prevent a channel from starting you
|
|
|
|
would have to pass a nil value for the channel's key. As an
|
|
|
|
example, to prevent the control channel from starting you would
|
|
|
|
call this function like so
|
|
|
|
|
|
|
|
(jupyter-start-channels client :control nil)
|
|
|
|
|
|
|
|
In addition to calling `jupyter-start-channel', a subprocess is
|
|
|
|
created for each channel which monitors the channel's socket for
|
|
|
|
input events. Note that this polling subprocess is not created
|
|
|
|
for the heartbeat channel."
|
2017-12-21 18:12:25 -06:00
|
|
|
(unless (oref client ioloop)
|
2018-01-08 22:42:50 -06:00
|
|
|
;; TODO: Currently there is no way to stop/start a channel individually
|
|
|
|
;; outside of this method. Create channel methods which are aware of a
|
|
|
|
;; client's ioloop so that you can send commands to the ioloop to start and
|
|
|
|
;; stop a channel. Also figure out a way to block until the ioloop says it
|
|
|
|
;; has finished with the operation. This may need changes in
|
|
|
|
;; `jupyter--ioloop'
|
|
|
|
(let ((ioloop (zmq-start-process
|
|
|
|
(jupyter--ioloop client)
|
|
|
|
(apply-partially #'jupyter--ioloop-filter client)
|
|
|
|
(apply-partially #'jupyter--ioloop-sentinel client))))
|
|
|
|
(oset client ioloop ioloop)
|
|
|
|
(when hb (jupyter-start-channel (oref client hb-channel)))
|
|
|
|
(unless shell
|
|
|
|
(zmq-subprocess-send ioloop '(stop-channel :shell)))
|
|
|
|
(unless iopub
|
|
|
|
(zmq-subprocess-send ioloop '(stop-channel :iopub)))
|
|
|
|
(unless stdin
|
|
|
|
(zmq-subprocess-send ioloop '(stop-channel :stdin))))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-19 18:13:42 -06:00
|
|
|
(cl-defmethod jupyter-stop-channels ((client jupyter-kernel-client))
|
|
|
|
"Stop any running channels of CLIENT."
|
2017-12-21 18:12:25 -06:00
|
|
|
(when (oref client hb-channel)
|
|
|
|
(jupyter-stop-channel (oref client hb-channel)))
|
2017-12-19 18:13:42 -06:00
|
|
|
(let ((ioloop (oref client ioloop)))
|
|
|
|
(when ioloop
|
2017-12-19 18:47:57 -06:00
|
|
|
(zmq-subprocess-send ioloop (cons 'quit nil))
|
2017-12-27 00:00:59 -06:00
|
|
|
(with-timeout (1 (delete-process ioloop))
|
2017-12-19 18:47:57 -06:00
|
|
|
(while (oref client ioloop)
|
|
|
|
(sleep-for 0 100))))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-15 18:14:28 -06:00
|
|
|
(cl-defmethod jupyter-channels-running-p ((client jupyter-kernel-client))
|
|
|
|
"Are any channels of CLIENT alive?"
|
2017-12-13 11:27:13 -06:00
|
|
|
(cl-loop
|
|
|
|
for channel in (list 'shell-channel
|
|
|
|
'iopub-channel
|
|
|
|
'hb-channel
|
|
|
|
'stdin-channel)
|
2018-01-08 22:42:50 -06:00
|
|
|
;; FIXME: This does not work with the current implementation of channels
|
2018-01-04 20:44:15 -06:00
|
|
|
thereis (jupyter-channel-alive-p (slot-value client channel))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-15 22:26:21 -06:00
|
|
|
;;; Message callbacks
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 15:31:39 -06:00
|
|
|
(defun jupyter--run-callbacks (req msg)
|
|
|
|
"Run REQ's MSG callbacks.
|
|
|
|
See `jupyter-add-callback'."
|
2017-12-19 18:02:55 -06:00
|
|
|
(when req
|
|
|
|
(let* ((callbacks (jupyter-request-callbacks req))
|
2018-01-06 19:47:47 -06:00
|
|
|
(cb-all-types (cdr (assoc t callbacks)))
|
|
|
|
(cb-for-type (cdr (assoc (jupyter-message-type msg) callbacks))))
|
|
|
|
(and cb-all-types (funcall cb-all-types msg))
|
|
|
|
(and cb-for-type (funcall cb-for-type msg)))))
|
2017-12-19 18:02:55 -06:00
|
|
|
|
2018-01-06 15:31:39 -06:00
|
|
|
(defun jupyter--add-callback (req msg-type cb)
|
2018-01-06 16:37:18 -06:00
|
|
|
"Helper function for `jupyter-add-callback'.
|
|
|
|
REQ is a `jupyter-request' object, MSG-TYPE should be one of the
|
|
|
|
keywords corresponding to a received message type in
|
|
|
|
`jupyter-message-types', and CB will be the callback that will be
|
|
|
|
run when MSG-TYPE is received for REQ."
|
2018-01-06 15:31:39 -06:00
|
|
|
(setq msg-type (or (plist-get jupyter-message-types msg-type)
|
|
|
|
;; A msg-type of t means that FUNCTION is run for all
|
|
|
|
;; messages associated with a request.
|
|
|
|
(eq msg-type t)))
|
|
|
|
(unless msg-type
|
|
|
|
(error "Not a valid message type (`%s')" msg-type))
|
|
|
|
(let ((callbacks (jupyter-request-callbacks req)))
|
|
|
|
(if (null callbacks)
|
|
|
|
(setf (jupyter-request-callbacks req)
|
|
|
|
(list (cons msg-type cb)))
|
|
|
|
(let ((cb-for-type (assoc msg-type callbacks)))
|
|
|
|
(if (not cb-for-type)
|
|
|
|
(nconc callbacks (list (cons msg-type cb)))
|
|
|
|
(setq cb (apply-partially
|
|
|
|
(lambda (cb1 cb2 msg)
|
|
|
|
(funcall cb1 msg)
|
|
|
|
(funcall cb2 msg))
|
|
|
|
(cdr cb-for-type)
|
|
|
|
cb))
|
|
|
|
(setcdr cb-for-type cb))))))
|
|
|
|
|
|
|
|
(defun jupyter-add-callback (req msg-type cb &rest callbacks)
|
|
|
|
"Add a callback to run when a message is received for a request.
|
|
|
|
REQ is a `jupyter-request' returned by one of the request methods
|
|
|
|
of a `jupyter-kernel-client'. MSG-TYPE is a keyword corresponding
|
2018-01-08 18:11:08 -06:00
|
|
|
to one of the keys in `jupyter-message-types'. CB is the callback
|
|
|
|
function which will run with a single argument, a message whose
|
|
|
|
`jupyter-message-parent-id' is `equal' to the
|
|
|
|
`jupyter-request-id' of REQ and whose `jupyter-message-type'
|
|
|
|
corresponds to the value of MSG-TYPE in `jupyter-message-types'.
|
|
|
|
MSG-TYPE can also be a list, in which case run CB for every
|
|
|
|
MSG-TYPE in the list. If MSG-TYPE is t, then run CB for every
|
|
|
|
message received for REQ.
|
2018-01-06 16:37:18 -06:00
|
|
|
|
|
|
|
Any additional arguments to `jupyter-add-callback' are
|
|
|
|
interpreted as additional CALLBACKS to add to REQ. So to add
|
|
|
|
multiple callbacks to a request you would do
|
2018-01-06 15:31:39 -06:00
|
|
|
|
|
|
|
(jupyter-add-callback
|
|
|
|
(jupyter-execute-request client :code \"1 + 2\")
|
|
|
|
:status (lambda (msg) ...)
|
|
|
|
:execute-reply (lambda (msg) ...)
|
|
|
|
:execute-result (lambda (msg) ...))"
|
|
|
|
(declare (indent 1))
|
|
|
|
(if (jupyter-request-idle-received-p req)
|
|
|
|
(error "Request already received idle message")
|
|
|
|
(setq callbacks (append (list msg-type cb) callbacks))
|
|
|
|
(cl-loop for (msg-type cb) on callbacks by 'cddr
|
|
|
|
if (listp msg-type)
|
|
|
|
do (mapc (lambda (mt) (jupyter--add-callback req mt cb)) msg-type)
|
|
|
|
else do (jupyter--add-callback req msg-type cb))))
|
|
|
|
|
2018-01-06 16:37:18 -06:00
|
|
|
(defun jupyter-wait-until (req msg-type cb &optional timeout)
|
|
|
|
"Wait until conditions for a request are satisfied.
|
|
|
|
REQ, MSG-TYPE, and CB have the same meaning as in
|
|
|
|
`jupyter-add-callback'. If CB returns a non-nil within TIMEOUT
|
|
|
|
seconds, return the message that caused CB to return non-nil. If
|
|
|
|
CB never returns a non-nil value within TIMEOUT, return nil. Note
|
|
|
|
that if no TIMEOUT is given, `jupyter-default-timeout' is used."
|
2018-01-06 15:31:39 -06:00
|
|
|
(declare (indent 1))
|
2018-01-04 20:58:28 -06:00
|
|
|
(setq timeout (or timeout jupyter-default-timeout))
|
2017-12-19 18:16:58 -06:00
|
|
|
(cl-check-type timeout number)
|
2018-01-08 21:43:07 -06:00
|
|
|
(let ((msg nil))
|
2018-01-06 15:31:39 -06:00
|
|
|
(jupyter-add-callback req
|
2018-01-06 16:37:18 -06:00
|
|
|
msg-type (lambda (m) (setq msg (when (funcall cb m) m))))
|
2018-01-02 13:23:28 -06:00
|
|
|
(with-timeout (timeout nil)
|
|
|
|
(while (null msg)
|
|
|
|
(sleep-for 0.01))
|
|
|
|
msg)))
|
2017-12-15 18:18:41 -06:00
|
|
|
|
2017-12-31 13:54:43 -06:00
|
|
|
(defun jupyter-wait-until-idle (req &optional timeout)
|
2018-01-06 16:37:18 -06:00
|
|
|
"Wait until a status: idle message is received for a request.
|
|
|
|
REQ has the same meaning as in `jupyter-add-callback'. If an idle
|
|
|
|
message for REQ is received within TIMEOUT seconds, return the
|
|
|
|
message. Otherwise return nil if the message was not received
|
|
|
|
within TIMEOUT. Note that if no TIMEOUT is given, it defaults to
|
|
|
|
`jupyter-default-timeout'."
|
2018-01-06 15:31:39 -06:00
|
|
|
(jupyter-wait-until req :status #'jupyter-message-status-idle-p timeout))
|
2017-12-15 18:18:41 -06:00
|
|
|
|
2017-12-31 13:54:43 -06:00
|
|
|
(defun jupyter-wait-until-received (msg-type req &optional timeout)
|
2018-01-06 16:37:18 -06:00
|
|
|
"Wait until a message of a certain type is received for a request.
|
|
|
|
MSG-TYPE and REQ has the same meaning as their corresponding
|
|
|
|
argument in `jupyter-add-callback'. If no message that matches
|
|
|
|
MSG-TYPE is received for REQ within TIMEOUT seconds, return nil.
|
|
|
|
Otherwise return the first message that matched MSG-TYPE. Note
|
|
|
|
that if no TIMEOUT is given, it defaults to
|
|
|
|
`jupyter-default-timeout'."
|
2017-12-31 13:54:43 -06:00
|
|
|
(declare (indent 1))
|
2018-01-06 15:31:39 -06:00
|
|
|
(jupyter-wait-until req msg-type #'identity timeout))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 19:43:21 -06:00
|
|
|
(cl-defmethod jupyter-handle-message ((client jupyter-kernel-client) channel)
|
2017-12-13 11:27:13 -06:00
|
|
|
"Process a message on CLIENT's CHANNEL.
|
2018-01-06 19:43:21 -06:00
|
|
|
When a message is received from the kernel, the
|
|
|
|
`jupyter-handle-message' method is called on the client. The
|
|
|
|
client method runs any callbacks for the message and possibly
|
|
|
|
runs the client handler for the channel the message was received
|
|
|
|
on. The channel's `jupyter-handle-message' method will then pass
|
|
|
|
the message to the appropriate message handler based on message
|
|
|
|
type which terminates the execution path.
|
|
|
|
|
|
|
|
So when a message is received from the kernel the following steps
|
|
|
|
are taken:
|
|
|
|
|
|
|
|
- `jupyter-handle-message' (client)
|
|
|
|
- Run callbacks for message
|
|
|
|
- Possibly run channel handlers
|
|
|
|
- `jupyter-handle-message' (channel)
|
|
|
|
- Based on message type, dispatch to
|
|
|
|
`jupyter-handle-execute-result',
|
|
|
|
`jupyter-handle-kernel-info-reply', ...
|
2018-01-08 18:11:08 -06:00
|
|
|
- Remove request from client request table when idle message is received"
|
2018-01-04 23:27:41 -06:00
|
|
|
(when (jupyter-messages-available-p channel)
|
|
|
|
(let* ((msg (jupyter-get-message channel))
|
2018-01-06 19:43:21 -06:00
|
|
|
(pmsg-id (jupyter-message-parent-id msg))
|
|
|
|
(requests (oref client requests))
|
|
|
|
(req (gethash pmsg-id requests)))
|
|
|
|
(if (not req)
|
|
|
|
;; Always run handlers of IOPub messages, even when they are not
|
|
|
|
;; associated with any request that was sent by us.
|
|
|
|
;;
|
|
|
|
;; TODO: Would we always want this?
|
|
|
|
(when (eq (oref channel type) :iopub)
|
|
|
|
(jupyter-handle-message channel client nil msg))
|
|
|
|
(unwind-protect
|
2018-01-06 15:31:39 -06:00
|
|
|
(jupyter--run-callbacks req msg)
|
2017-12-15 22:26:21 -06:00
|
|
|
(unwind-protect
|
2018-01-06 19:47:47 -06:00
|
|
|
(when (jupyter-request-run-handlers-p req)
|
|
|
|
(jupyter-handle-message channel client req msg))
|
2018-01-08 13:59:42 -06:00
|
|
|
;; Checking for pmsg-id prevents the removal of
|
|
|
|
;; `jupyter-missing-request' for the client
|
|
|
|
(when (and pmsg-id (jupyter-message-status-idle-p msg))
|
2018-01-04 15:52:04 -06:00
|
|
|
(setf (jupyter-request-idle-received-p req) t)
|
2018-01-06 16:37:18 -06:00
|
|
|
;; TODO: Messages associated with the request might still be
|
|
|
|
;; received when the request is removed from the requests table.
|
|
|
|
(remhash pmsg-id requests))))))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-02 01:02:35 -06:00
|
|
|
;;; STDIN message requests/handlers
|
2018-01-06 19:43:21 -06:00
|
|
|
|
2018-01-04 15:56:04 -06:00
|
|
|
(cl-defmethod jupyter-handle-message ((channel jupyter-stdin-channel)
|
|
|
|
client
|
|
|
|
req
|
|
|
|
msg)
|
2017-12-13 11:27:13 -06:00
|
|
|
(cl-destructuring-bind (&key prompt password &allow-other-keys)
|
2018-01-06 19:43:21 -06:00
|
|
|
(jupyter-message-content msg)
|
|
|
|
(jupyter-handle-input-reply client req prompt password)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-input-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
prompt
|
|
|
|
password)
|
2017-12-15 18:14:28 -06:00
|
|
|
"Handle an input request from CLIENT's kernel.
|
|
|
|
PROMPT is the prompt the kernel would like to show the user. If
|
|
|
|
PASSWORD is non-nil, then `read-passwd' is used to get input from
|
|
|
|
the user. Otherwise `read-from-minibuffer' is used."
|
2018-01-02 01:02:35 -06:00
|
|
|
;; TODO: Allow for quiting the input request. In this case, I suppose send an
|
|
|
|
;; interrupt request to the kernel
|
2017-12-13 11:27:13 -06:00
|
|
|
(let ((channel (oref client stdin-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-input-reply
|
2017-12-13 11:27:13 -06:00
|
|
|
:value (funcall (if password #'read-passwd
|
|
|
|
#'read-from-minibuffer)
|
|
|
|
prompt))))
|
|
|
|
;; TODO: Check for 'allow_stdin'
|
|
|
|
;; http://jupyter-client.readthedocs.io/en/latest/messaging.html#stdin-messages
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "input_reply" msg)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-02 01:02:35 -06:00
|
|
|
;;; SHELL message requests/handlers
|
2017-12-13 11:27:13 -06:00
|
|
|
|
|
|
|
;; http://jupyter-client.readthedocs.io/en/latest/messaging.html#messages-on-the-shell-router-dealer-channel
|
2018-01-04 15:56:04 -06:00
|
|
|
(cl-defmethod jupyter-handle-message ((channel jupyter-shell-channel)
|
|
|
|
client
|
|
|
|
req
|
|
|
|
msg)
|
2018-01-06 19:43:21 -06:00
|
|
|
(let ((content (jupyter-message-content msg)))
|
|
|
|
(cl-destructuring-bind (&key status ename evalue &allow-other-keys) content
|
|
|
|
;; is_complete_reply messages have a status other than "ok" so just
|
|
|
|
;; ensure that the status does not correspond to an error.
|
2017-12-19 11:55:07 -06:00
|
|
|
(if (not (member status '("error" "abort")))
|
2018-01-06 19:43:21 -06:00
|
|
|
(pcase (jupyter-message-type msg)
|
2017-12-14 13:34:03 -06:00
|
|
|
("execute_reply"
|
|
|
|
(cl-destructuring-bind (&key execution_count
|
|
|
|
user_expressions
|
|
|
|
payload
|
|
|
|
&allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-execute-reply
|
2017-12-19 18:02:55 -06:00
|
|
|
client req execution_count user_expressions payload)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("inspect_reply"
|
|
|
|
(cl-destructuring-bind (&key found
|
|
|
|
data
|
|
|
|
metadata
|
|
|
|
&allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-inspect-reply
|
2017-12-19 18:02:55 -06:00
|
|
|
client req found data metadata)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("complete_reply"
|
|
|
|
(cl-destructuring-bind (&key matches
|
|
|
|
cursor_start
|
|
|
|
cursor_end
|
|
|
|
metadata
|
|
|
|
&allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-complete-reply
|
2017-12-19 18:02:55 -06:00
|
|
|
client req matches cursor_start cursor_end metadata)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("history_reply"
|
|
|
|
(cl-destructuring-bind (&key history &allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-history-reply client req history)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("is_complete_reply"
|
|
|
|
(cl-destructuring-bind (&key status indent &allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-is-complete-reply client req status indent)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("comm_info_reply"
|
|
|
|
(cl-destructuring-bind (&key comms &allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-comm-info-reply client req comms)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("kernel_info_reply"
|
|
|
|
(cl-destructuring-bind (&key protocol_version
|
|
|
|
implementation
|
|
|
|
implementation_version
|
|
|
|
language_info
|
|
|
|
banner
|
|
|
|
help_links
|
|
|
|
&allow-other-keys)
|
|
|
|
content
|
2018-01-02 00:38:46 -06:00
|
|
|
(jupyter-handle-kernel-info-reply
|
2018-01-04 15:56:04 -06:00
|
|
|
client req protocol_version implementation
|
|
|
|
implementation_version language_info banner help_links)))
|
2018-01-08 13:58:19 -06:00
|
|
|
(_
|
|
|
|
(warn "Message type not handled (%s)" (jupyter-message-type msg))))
|
|
|
|
;; FIXME: Do something about errors here?
|
2017-12-31 13:16:59 -06:00
|
|
|
;; (if (equal status "error")
|
|
|
|
;; (error "Error (%s): %s"
|
|
|
|
;; (plist-get content :ename) (plist-get content :evalue))
|
|
|
|
;; (error "Error: aborted"))
|
|
|
|
))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-execute-request ((client jupyter-kernel-client)
|
|
|
|
&key code
|
|
|
|
(silent nil)
|
|
|
|
(store-history t)
|
|
|
|
(user-expressions nil)
|
|
|
|
(allow-stdin t)
|
|
|
|
(stop-on-error nil))
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send an execute request."
|
2017-12-13 11:27:13 -06:00
|
|
|
(declare (indent 1))
|
|
|
|
(let ((channel (oref client shell-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-execute-request
|
2017-12-13 11:27:13 -06:00
|
|
|
:code code
|
|
|
|
:silent silent
|
|
|
|
:store-history store-history
|
|
|
|
:user-expressions user-expressions
|
|
|
|
:allow-stdin allow-stdin
|
|
|
|
:stop-on-error stop-on-error)))
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "execute_request" msg)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-execute-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
execution-count
|
|
|
|
user-expressions
|
|
|
|
payload)
|
|
|
|
"Default execute reply handler."
|
|
|
|
nil)
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-inspect-request ((client jupyter-kernel-client)
|
|
|
|
&key code
|
|
|
|
(pos 0)
|
|
|
|
(detail 0))
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send an inspect request."
|
2017-12-14 13:34:03 -06:00
|
|
|
(declare (indent 1))
|
|
|
|
(let ((channel (oref client shell-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-inspect-request
|
2017-12-14 13:34:03 -06:00
|
|
|
:code code :pos pos :detail detail)))
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "inspect_request" msg)))
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-inspect-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
found
|
|
|
|
data
|
|
|
|
metadata)
|
|
|
|
"Default inspect reply handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-complete-request ((client jupyter-kernel-client)
|
|
|
|
&key code
|
|
|
|
(pos 0))
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send a complete request."
|
2017-12-14 13:34:03 -06:00
|
|
|
(declare (indent 1))
|
|
|
|
(let ((channel (oref client shell-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-complete-request
|
2017-12-14 13:34:03 -06:00
|
|
|
:code code :pos pos)))
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "complete_request" msg)))
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-complete-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
matches
|
|
|
|
cursor-start
|
|
|
|
cursor-end
|
|
|
|
metadata)
|
|
|
|
"Default complete reply handler."
|
|
|
|
nil)
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-history-request ((client jupyter-kernel-client)
|
|
|
|
&key
|
|
|
|
output
|
|
|
|
raw
|
|
|
|
(hist-access-type "tail")
|
|
|
|
session
|
|
|
|
start
|
|
|
|
stop
|
|
|
|
(n 10)
|
|
|
|
pattern
|
|
|
|
unique)
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send a history request."
|
2017-12-14 13:34:03 -06:00
|
|
|
(declare (indent 1))
|
|
|
|
(let ((channel (oref client shell-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-history-request
|
2017-12-14 13:34:03 -06:00
|
|
|
:output output
|
|
|
|
:raw raw
|
|
|
|
:hist-access-type hist-access-type
|
|
|
|
:session session
|
|
|
|
:start start
|
|
|
|
:stop stop
|
|
|
|
:n n
|
2017-12-17 02:58:11 -06:00
|
|
|
:pattern pattern
|
2017-12-14 13:34:03 -06:00
|
|
|
:unique unique)))
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "history_request" msg)))
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-history-reply ((client jupyter-kernel-client)
|
2018-01-08 23:14:42 -06:00
|
|
|
req
|
|
|
|
history)
|
2018-01-06 22:50:55 -06:00
|
|
|
"Default history reply handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-is-complete-request ((client jupyter-kernel-client)
|
|
|
|
&key code)
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send an is-complete request."
|
2017-12-14 13:34:03 -06:00
|
|
|
(declare (indent 1))
|
|
|
|
(let ((channel (oref client shell-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-is-complete-request
|
2017-12-14 13:34:03 -06:00
|
|
|
:code code)))
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "is_complete_request" msg)))
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-is-complete-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
status
|
|
|
|
indent)
|
|
|
|
"Default is complete reply handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-comm-info-request ((client jupyter-kernel-client)
|
|
|
|
&key target-name)
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send a comm-info request."
|
2017-12-14 13:34:03 -06:00
|
|
|
(declare (indent 1))
|
|
|
|
(let ((channel (oref client shell-channel))
|
2017-12-31 15:19:23 -06:00
|
|
|
(msg (jupyter-message-comm-info-request
|
2017-12-14 13:34:03 -06:00
|
|
|
:target-name target-name)))
|
2017-12-31 15:22:48 -06:00
|
|
|
(jupyter-send client channel "comm_info_request" msg)))
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-comm-info-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
comms)
|
|
|
|
"Default comm info. reply handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-kernel-info-request ((client jupyter-kernel-client))
|
2017-12-15 18:14:28 -06:00
|
|
|
"Send a kernel-info request."
|
2018-01-04 15:56:04 -06:00
|
|
|
(let ((channel (oref client shell-channel))
|
|
|
|
(msg (jupyter-message-kernel-info-request)))
|
2017-12-31 15:19:23 -06:00
|
|
|
(jupyter-send client channel "kernel_info_request" msg)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-kernel-info-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
protocol-version
|
|
|
|
implementation
|
|
|
|
implementation-version
|
|
|
|
language-info
|
|
|
|
banner
|
|
|
|
help-links)
|
|
|
|
"Default kernel-info reply handler."
|
|
|
|
nil)
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-08 22:30:00 -06:00
|
|
|
(cl-defgeneric jupyter-shutdown-request ((client jupyter-kernel-client)
|
|
|
|
&optional restart)
|
|
|
|
"Request a shutdown of CLIENT's kernel.
|
|
|
|
If RESTART is non-nil, request a restart instead of a complete shutdown."
|
|
|
|
(let ((channel (oref client shell-channel))
|
|
|
|
(msg (jupyter-message-shutdown-request :restart restart)))
|
|
|
|
(jupyter-send client channel "shutdown_request" msg)))
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-handle-shutdown-reply ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
restart)
|
|
|
|
"Default shutdown reply handler."
|
|
|
|
nil)
|
|
|
|
|
2018-01-02 00:38:46 -06:00
|
|
|
;;; IOPUB message handlers
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-04 15:56:04 -06:00
|
|
|
(cl-defmethod jupyter-handle-message ((channel jupyter-iopub-channel)
|
|
|
|
client
|
|
|
|
req
|
|
|
|
msg)
|
2018-01-02 00:38:46 -06:00
|
|
|
(let ((content (jupyter-message-content msg)))
|
|
|
|
(pcase (jupyter-message-type msg)
|
2017-12-19 11:54:10 -06:00
|
|
|
("shutdown_reply"
|
|
|
|
(cl-destructuring-bind (&key restart &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-shutdown-reply
|
|
|
|
client req restart)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("stream"
|
|
|
|
(cl-destructuring-bind (&key name text &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-stream
|
|
|
|
client req name text)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("execute_input"
|
|
|
|
(cl-destructuring-bind (&key code execution_count &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-execute-input
|
|
|
|
client req code execution_count)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("execute_result"
|
|
|
|
(cl-destructuring-bind (&key execution_count
|
|
|
|
data
|
|
|
|
metadata
|
|
|
|
&allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-execute-result
|
|
|
|
client req execution_count data metadata)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("error"
|
|
|
|
(cl-destructuring-bind (&key ename evalue traceback &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-error
|
|
|
|
client req ename evalue traceback)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("status"
|
|
|
|
(cl-destructuring-bind (&key execution_state &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-status
|
|
|
|
client req execution_state)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("clear_output"
|
|
|
|
(cl-destructuring-bind (&key wait &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-clear-output
|
|
|
|
client req wait)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("display_data"
|
|
|
|
(cl-destructuring-bind (&key data metadata transient &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-display-data
|
|
|
|
client req data metadata transient)))
|
2017-12-14 13:34:03 -06:00
|
|
|
("update_display_data"
|
|
|
|
(cl-destructuring-bind (&key data metadata transient &allow-other-keys)
|
|
|
|
content
|
2018-01-04 15:56:04 -06:00
|
|
|
(jupyter-handle-update-display-data
|
|
|
|
client req data metadata transient)))
|
2018-01-08 13:58:19 -06:00
|
|
|
(_
|
|
|
|
(warn "Message type not handled (%s)" (jupyter-message-type msg))))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-stream ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
name
|
|
|
|
text)
|
|
|
|
"Default stream handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-execute-input ((client jupyter-kernel-client)
|
2017-12-19 18:02:55 -06:00
|
|
|
req
|
2018-01-06 22:50:55 -06:00
|
|
|
code
|
|
|
|
execution-count)
|
|
|
|
"Default execute input handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-execute-result ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
execution-count
|
|
|
|
data
|
|
|
|
metadata)
|
|
|
|
"Default execute result handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-error ((client jupyter-kernel-client)
|
2018-01-04 15:56:04 -06:00
|
|
|
req
|
2018-01-06 22:50:55 -06:00
|
|
|
ename
|
|
|
|
evalue
|
|
|
|
traceback)
|
|
|
|
"Default error handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-status ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
execution-state)
|
|
|
|
"Default status handler."
|
|
|
|
nil)
|
2017-12-14 13:34:03 -06:00
|
|
|
|
2018-01-06 22:50:55 -06:00
|
|
|
(cl-defgeneric jupyter-handle-clear-output ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
wait)
|
|
|
|
"Default clear output handler."
|
|
|
|
nil)
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-handle-display-data ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
data
|
|
|
|
metadata
|
|
|
|
transient)
|
|
|
|
"Default display data handler."
|
|
|
|
nil)
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-handle-display-data ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
data
|
|
|
|
metadata
|
|
|
|
transient)
|
|
|
|
"Default display data handler."
|
|
|
|
nil)
|
|
|
|
|
|
|
|
(cl-defgeneric jupyter-handle-update-display-data ((client jupyter-kernel-client)
|
|
|
|
req
|
|
|
|
data
|
|
|
|
metadata
|
|
|
|
transient)
|
|
|
|
"Default update display handler"
|
|
|
|
nil)
|
2017-12-13 11:27:13 -06:00
|
|
|
|
|
|
|
(provide 'jupyter-client)
|
2018-01-08 21:38:32 -06:00
|
|
|
|
|
|
|
;;; jupyter-client.el ends here
|