emacs-jupyter/jupyter-zmq-channel.el

267 lines
11 KiB
EmacsLisp
Raw Normal View History

;;; jupyter-zmq-channel.el --- A Jupyter channel implementation using ZMQ sockets -*- lexical-binding: t -*-
2018-01-08 21:38:32 -06:00
;; Copyright (C) 2019 Nathaniel Nicandro
2018-01-08 21:38:32 -06:00
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 27 Jun 2019
2019-07-24 15:23:04 -05:00
;; Version: 0.8.1
2018-01-08 21:38:32 -06:00
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
2019-05-31 09:44:39 -05:00
;; published by the Free Software Foundation; either version 3, or (at
2018-01-08 21:38:32 -06:00
;; 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:
;; Implements synchronous channel types using ZMQ sockets. Each channel is
;; essentially a wrapper around a `zmq-socket' constrained to a socket type by
;; the type of the channel and with an associated `zmq-IDENTITY' obtained from
;; the `jupyter-session' that must be associated with the channel. A heartbeat
2018-11-08 13:41:39 -06:00
;; channel is distinct from the other channels in that it is implemented using
;; a timer which periodically pings the kernel depending on how its configured.
;; In order for communication to occur on the other channels, one of
;; `jupyter-send' or `jupyter-recv' must be called after starting the channel
;; with `jupyter-start-channel'.
2018-01-08 21:38:32 -06:00
;;; Code:
(require 'jupyter-messages)
Do not depend strongly on zmq Having the `jupyter-comm-layer` abstraction means we do not need to do so. * jupyter-base.el (zmq): Un-require. (jupyter-socket-types): Move to `jupyter-channels.el`. (jupyter-session): Don't mention zmq in doc string. (jupyter-available-local-ports, jupyter-make-ssh-tunnel): New functions. (jupyter-tunnel-connection): Use them. * jupyter-channel-ioloop-comm.el: New file. * jupyter-channels.el (jupyter-messages): Un-require. (jupyter-comm-layer, zmq): New requires. (jupyter-socket-types): Moved from `jupyter-base.el`. (jupyter-send, jupyter-recv): Implementations for `jupyter-session` moved from `jupyter-messages.el`. (jupyter-sync-channel-comm): `jupyter-comm-layer` implementation for `jupyter-sync-channel` objects moved from `jupyter-comm-layer.el`. * jupyter-comm-layer.el (jupyter-channel-ioloop): Un-require. (jupyter-sync-channel-comm): Move implementation to `jupyter-channels.el`. (jupyter-ioloop-comm): Move implementation to new file `jupyter-ioloop-comm.el`. (jupyter-channel-ioloop-comm): Move implementation to new file `jupyter-channel-ioloop-comm.el`. * jupyter-ioloop-comm.el: New file. * jupyter-ioloop.el (zmq): Require. * jupyter-kernel-manager.el (jupyter-make-client): Ensure `jupyter-channel-ioloop-comm` is required. * jupyter-messages.el (jupyter-send) (jupyter-recv): Moved to `jupyter-channels.el` * jupyter-repl.el (jupyter-connect-repl): Ensure `jupyter-channel-ioloop-comm` is required. * test/jupyter-test.el (jupyter-available-local-ports): New test. * test/test-helper.el (jupyter-channel-ioloop-comm): New require.
2019-06-28 20:03:00 -05:00
(require 'zmq)
(require 'jupyter-channel)
(eval-when-compile (require 'subr-x))
2018-10-16 16:45:56 -05:00
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(declare-function jupyter-ioloop-poller-remove "jupyter-ioloop")
(declare-function jupyter-ioloop-poller-add "jupyter-ioloop")
Do not depend strongly on zmq Having the `jupyter-comm-layer` abstraction means we do not need to do so. * jupyter-base.el (zmq): Un-require. (jupyter-socket-types): Move to `jupyter-channels.el`. (jupyter-session): Don't mention zmq in doc string. (jupyter-available-local-ports, jupyter-make-ssh-tunnel): New functions. (jupyter-tunnel-connection): Use them. * jupyter-channel-ioloop-comm.el: New file. * jupyter-channels.el (jupyter-messages): Un-require. (jupyter-comm-layer, zmq): New requires. (jupyter-socket-types): Moved from `jupyter-base.el`. (jupyter-send, jupyter-recv): Implementations for `jupyter-session` moved from `jupyter-messages.el`. (jupyter-sync-channel-comm): `jupyter-comm-layer` implementation for `jupyter-sync-channel` objects moved from `jupyter-comm-layer.el`. * jupyter-comm-layer.el (jupyter-channel-ioloop): Un-require. (jupyter-sync-channel-comm): Move implementation to `jupyter-channels.el`. (jupyter-ioloop-comm): Move implementation to new file `jupyter-ioloop-comm.el`. (jupyter-channel-ioloop-comm): Move implementation to new file `jupyter-channel-ioloop-comm.el`. * jupyter-ioloop-comm.el: New file. * jupyter-ioloop.el (zmq): Require. * jupyter-kernel-manager.el (jupyter-make-client): Ensure `jupyter-channel-ioloop-comm` is required. * jupyter-messages.el (jupyter-send) (jupyter-recv): Moved to `jupyter-channels.el` * jupyter-repl.el (jupyter-connect-repl): Ensure `jupyter-channel-ioloop-comm` is required. * test/jupyter-test.el (jupyter-available-local-ports): New test. * test/test-helper.el (jupyter-channel-ioloop-comm): New require.
2019-06-28 20:03:00 -05:00
(defconst jupyter-socket-types
(list :hb zmq-REQ
:shell zmq-DEALER
:iopub zmq-SUB
:stdin zmq-DEALER
:control zmq-DEALER)
"The socket types for the various channels used by `jupyter'.")
(defclass jupyter-zmq-channel (jupyter-channel)
((socket
2017-12-13 11:27:13 -06:00
:type (or null zmq-socket)
:initform nil
:documentation "The socket used for communicating with the kernel.")))
(defun jupyter-connect-endpoint (type endpoint &optional identity)
"Create socket with TYPE and connect to ENDPOINT.
If IDENTITY is non-nil, it will be set as the ROUTING-ID of the
socket. Return the created socket."
(let ((sock (zmq-socket (zmq-current-context) type)))
(prog1 sock
(zmq-socket-set sock zmq-LINGER 1000)
(when identity
(zmq-socket-set sock zmq-ROUTING-ID identity))
(zmq-connect sock endpoint))))
(defun jupyter-connect-channel (ctype endpoint &optional identity)
"Create a socket based on a Jupyter channel type.
CTYPE is one of the symbols `:hb', `:stdin', `:shell',
`:control', or `:iopub' and represents the type of channel to
connect to ENDPOINT. If IDENTITY is non-nil, it will be set as
the ROUTING-ID of the socket. Return the created socket."
(let ((sock-type (plist-get jupyter-socket-types ctype)))
(unless sock-type
(error "Invalid channel type (%s)" ctype))
(jupyter-connect-endpoint sock-type endpoint identity)))
(cl-defmethod jupyter-start-channel ((channel jupyter-zmq-channel)
&key (identity (jupyter-session-id
(oref channel session))))
(unless (jupyter-channel-alive-p channel)
(let ((socket (jupyter-connect-channel
(oref channel type) (oref channel endpoint) identity)))
(oset channel socket socket)
(cl-case (oref channel type)
(:iopub
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(zmq-socket-set socket zmq-SUBSCRIBE ""))))
(when (and (functionp 'jupyter-ioloop-environment-p)
(jupyter-ioloop-environment-p))
(jupyter-ioloop-poller-add (oref channel socket) zmq-POLLIN))))
(cl-defmethod jupyter-stop-channel ((channel jupyter-zmq-channel))
(when (jupyter-channel-alive-p channel)
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(when (and (functionp 'jupyter-ioloop-environment-p)
(jupyter-ioloop-environment-p))
(jupyter-ioloop-poller-remove (oref channel socket)))
(with-slots (socket) channel
(zmq-disconnect socket (zmq-socket-get socket zmq-LAST-ENDPOINT)))
(oset channel socket nil)))
(cl-defmethod jupyter-channel-alive-p ((channel jupyter-zmq-channel))
(not (null (oref channel socket))))
(cl-defmethod jupyter-send ((channel jupyter-zmq-channel) type message &optional msg-id)
(jupyter-send (oref channel session) (oref channel socket) type message msg-id))
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(cl-defmethod jupyter-recv ((channel jupyter-zmq-channel) &optional dont-wait)
(condition-case nil
(jupyter-recv (oref channel session) (oref channel socket)
(when dont-wait zmq-DONTWAIT))
(zmq-EAGAIN nil)))
Do not depend strongly on zmq Having the `jupyter-comm-layer` abstraction means we do not need to do so. * jupyter-base.el (zmq): Un-require. (jupyter-socket-types): Move to `jupyter-channels.el`. (jupyter-session): Don't mention zmq in doc string. (jupyter-available-local-ports, jupyter-make-ssh-tunnel): New functions. (jupyter-tunnel-connection): Use them. * jupyter-channel-ioloop-comm.el: New file. * jupyter-channels.el (jupyter-messages): Un-require. (jupyter-comm-layer, zmq): New requires. (jupyter-socket-types): Moved from `jupyter-base.el`. (jupyter-send, jupyter-recv): Implementations for `jupyter-session` moved from `jupyter-messages.el`. (jupyter-sync-channel-comm): `jupyter-comm-layer` implementation for `jupyter-sync-channel` objects moved from `jupyter-comm-layer.el`. * jupyter-comm-layer.el (jupyter-channel-ioloop): Un-require. (jupyter-sync-channel-comm): Move implementation to `jupyter-channels.el`. (jupyter-ioloop-comm): Move implementation to new file `jupyter-ioloop-comm.el`. (jupyter-channel-ioloop-comm): Move implementation to new file `jupyter-channel-ioloop-comm.el`. * jupyter-ioloop-comm.el: New file. * jupyter-ioloop.el (zmq): Require. * jupyter-kernel-manager.el (jupyter-make-client): Ensure `jupyter-channel-ioloop-comm` is required. * jupyter-messages.el (jupyter-send) (jupyter-recv): Moved to `jupyter-channels.el` * jupyter-repl.el (jupyter-connect-repl): Ensure `jupyter-channel-ioloop-comm` is required. * test/jupyter-test.el (jupyter-available-local-ports): New test. * test/test-helper.el (jupyter-channel-ioloop-comm): New require.
2019-06-28 20:03:00 -05:00
(cl-defmethod jupyter-send ((session jupyter-session)
socket
type
message
&optional
msg-id
flags)
"For SESSION, send a message on SOCKET.
TYPE is message type of MESSAGE, one of the keys in
`jupyter-message-types'. MESSAGE is the message content.
Optionally supply a MSG-ID to the message, if this is nil a new
message ID will be generated. FLAGS has the same meaning as in
`zmq-send'. Return the message ID of the sent message."
(declare (indent 1))
(cl-destructuring-bind (id . msg)
(jupyter-encode-message session type
:msg-id msg-id :content message)
(prog1 id
(zmq-send-multipart socket msg flags))))
(cl-defmethod jupyter-recv ((session jupyter-session) socket &optional flags)
"For SESSION, receive a message on SOCKET with FLAGS.
FLAGS is passed to SOCKET according to `zmq-recv'. Return a cons cell
(IDENTS . MSG)
where IDENTS are the ZMQ identities associated with MSG and MSG
is the message property list whose fields can be accessed through
calls to `jupyter-message-content', `jupyter-message-parent-id',
and other such functions."
(let ((msg (zmq-recv-multipart socket flags)))
(when msg
(cl-destructuring-bind (idents . parts)
(jupyter--split-identities msg)
(cons idents (jupyter-decode-message session parts))))))
2018-01-08 21:38:32 -06:00
;;; Heartbeat channel
(defvar jupyter-hb-max-failures 3
"Number of heartbeat failures until the kernel is considered unreachable.
A ping is sent to the kernel on a heartbeat channel and waits
until `time-to-dead' seconds to see if the kernel sent a ping
back. If the kernel doesn't send a ping back after
`jupyter-hb-max-failures', the callback associated with the
heartbeat channel is called. See `jupyter-hb-on-kernel-dead'.")
(defclass jupyter-hb-channel (jupyter-zmq-channel)
2017-12-13 11:27:13 -06:00
((type
:type keyword
:initform :hb
:documentation "The type of this channel is `:hb'.")
2017-12-13 11:27:13 -06:00
(time-to-dead
2018-10-16 16:45:56 -05:00
:type number
:initform 10
2017-12-13 11:27:13 -06:00
:documentation "The time in seconds to wait for a response
2018-09-09 21:33:05 -05:00
from the kernel until the connection is assumed to be dead. Note
that this slot only takes effect when starting the channel.")
(dead-cb
:type function
:initform #'ignore
:documentation "A callback function that takes 0 arguments
and is called when the kernel has not responded for
\(* `jupyter-hb-max-failures' `time-to-dead'\) seconds.")
2017-12-13 11:27:13 -06:00
(beating
:type (or boolean symbol)
:initform t
:documentation "A flag variable indicating that the heartbeat
2018-09-09 21:33:05 -05:00
channel is communicating with the kernel.")
2017-12-13 11:27:13 -06:00
(paused
:type boolean
:initform t
2017-12-13 11:27:13 -06:00
:documentation "A flag variable indicating that the heartbeat
2018-09-09 16:19:06 -05:00
channel is paused and not communicating with the kernel. To
pause the heartbeat channel use `jupyter-hb-pause', to unpause
use `jupyter-hb-unpause'."))
2017-12-13 11:27:13 -06:00
:documentation "A base class for heartbeat channels.")
(cl-defmethod jupyter-channel-alive-p ((channel jupyter-hb-channel))
"Return non-nil if CHANNEL is alive."
2018-09-09 16:19:06 -05:00
(zmq-socket-p (oref channel socket)))
2017-12-13 11:27:13 -06:00
2018-11-14 01:28:09 -06:00
(defun jupyter-hb--pingable-p (channel)
(and (not (oref channel paused))
(jupyter-channel-alive-p channel)))
(cl-defmethod jupyter-hb-beating-p ((channel jupyter-hb-channel))
2018-09-09 21:33:05 -05:00
"Return non-nil if CHANNEL is reachable."
2018-11-14 01:28:09 -06:00
(and (jupyter-hb--pingable-p channel)
2018-09-09 16:19:06 -05:00
(oref channel beating)))
2017-12-13 11:27:13 -06:00
(cl-defmethod jupyter-hb-pause ((channel jupyter-hb-channel))
"Pause checking for heartbeat events on CHANNEL."
(oset channel paused t))
2017-12-13 11:27:13 -06:00
(cl-defmethod jupyter-hb-unpause ((channel jupyter-hb-channel))
"Un-pause checking for heatbeat events on CHANNEL."
2018-09-09 16:19:06 -05:00
(when (oref channel paused)
(if (jupyter-channel-alive-p channel)
;; Consume a pending message from the kernel if there is one. We send a
;; ping and then schedule a timer which fires TIME-TO-DEAD seconds
;; later to receive the ping back from the kernel and start the process
;; all over again. If the channel is paused before TIME-TO-DEAD
;; seconds, there may still be a ping from the kernel waiting.
(ignore-errors (zmq-recv (oref channel socket) zmq-DONTWAIT))
2018-11-11 21:23:42 -06:00
(jupyter-start-channel channel))
(oset channel paused nil)
(jupyter-hb--send-ping channel)))
2017-12-13 11:27:13 -06:00
(cl-defmethod jupyter-hb-on-kernel-dead ((channel jupyter-hb-channel) fun)
"When the kernel connected to CHANNEL dies, call FUN.
2018-10-16 16:45:56 -05:00
A kernel is considered dead when CHANNEL does not receive a
response after \(* `jupyter-hb-max-failures' `time-to-dead'\)
seconds has elapsed without the kernel sending a ping back."
(declare (indent 1))
(oset channel dead-cb fun))
(defun jupyter-hb--send-ping (channel &optional failed-count)
2018-11-14 01:28:09 -06:00
(when (jupyter-hb--pingable-p channel)
(condition-case nil
(zmq-send (oref channel socket) "ping")
;; FIXME: Should be a part of `jupyter-hb--pingable-p'
(zmq-ENOTSOCK
(jupyter-hb-pause channel)
(oset channel socket nil)))
(run-with-timer
(oref channel time-to-dead) nil
(lambda (channel-ref)
(when-let* ((channel (jupyter-weak-ref-resolve channel-ref))
(sock (and (jupyter-hb--pingable-p channel)
2019-01-16 20:30:44 -06:00
(oref channel socket))))
(oset channel beating
(condition-case nil
(and (zmq-recv sock zmq-DONTWAIT) t)
((zmq-EINTR zmq-EAGAIN) nil)))
(if (oref channel beating)
(jupyter-hb--send-ping channel)
;; Reset the socket
(jupyter-stop-channel channel)
(jupyter-start-channel channel)
(or failed-count (setq failed-count 0))
(if (< failed-count jupyter-hb-max-failures)
(jupyter-hb--send-ping channel (1+ failed-count))
2019-01-16 20:30:44 -06:00
(oset channel paused t)
(when (functionp (oref channel dead-cb))
(funcall (oref channel dead-cb)))))))
(jupyter-weak-ref channel))))
2018-09-09 16:19:06 -05:00
(provide 'jupyter-zmq-channel)
;;; jupyter-zmq-channel.el ends here