2017-12-13 11:27:13 -06:00
|
|
|
(require 'hmac-def)
|
|
|
|
(require 'cl-lib)
|
2017-12-14 13:29:31 -06:00
|
|
|
(require 'json)
|
2017-12-22 00:38:03 -06:00
|
|
|
(require 'jupyter-channels)
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-15 22:29:05 -06:00
|
|
|
(defconst jupyter-protocol-version "5.3"
|
|
|
|
"The jupyter protocol version that is implemented.")
|
|
|
|
|
|
|
|
(defconst jupyter-message-delimiter "<IDS|MSG>"
|
|
|
|
"The message delimiter required in the jupyter messaging
|
|
|
|
protocol.")
|
|
|
|
|
|
|
|
(defconst jupyter--false :json-false
|
|
|
|
"The symbol used to disambiguate nil from a true boolean
|
|
|
|
false.")
|
|
|
|
|
2017-12-15 18:16:53 -06:00
|
|
|
(defconst jupyter--received-message-types
|
|
|
|
(list 'execute-result "execute_result"
|
|
|
|
'execute-reply "execute_reply"
|
|
|
|
'inspect-reply "inspect_reply"
|
|
|
|
'complete-reply "complete_reply"
|
|
|
|
'history-reply "history_reply"
|
|
|
|
'is-complete-reply "is_complete_reply"
|
|
|
|
'comm-info-reply "comm_info_reply"
|
|
|
|
'kernel-info-reply "kernel_info_reply"
|
|
|
|
'shutdown-reply "shutdown_reply"
|
|
|
|
'interrupt-reply "interrupt_reply"
|
|
|
|
'stream "stream"
|
|
|
|
'display-data "display_data"
|
|
|
|
'update-display-data "update_display_data"
|
|
|
|
'execute-input "execute_input"
|
|
|
|
'error "error"
|
|
|
|
'status "status"
|
|
|
|
'clear-output "clear_output"
|
|
|
|
'input-reply "input_reply")
|
|
|
|
"A plist mapping symbols to received message types.
|
|
|
|
This is used to give some protection against invalid message
|
|
|
|
types in `jupyter-add-receive-callback'. If the MSG-TYPE argument
|
|
|
|
of `jupyter-add-receive-callback' does not match one of the keys
|
|
|
|
in this plist, an error is thrown.")
|
2017-12-13 11:27:13 -06:00
|
|
|
|
|
|
|
;;; Session object
|
|
|
|
|
|
|
|
(cl-defstruct (jupyter-session
|
|
|
|
(:constructor nil)
|
|
|
|
(:constructor
|
|
|
|
jupyter-session
|
2017-12-14 13:28:58 -06:00
|
|
|
(&key (key nil) &aux (id (jupyter--new-uuid)))))
|
2017-12-13 11:27:13 -06:00
|
|
|
(id nil :read-only t)
|
|
|
|
(key nil :read-only t))
|
|
|
|
|
|
|
|
;;; Signing messages
|
|
|
|
|
|
|
|
;; https://tools.ietf.org/html/rfc4868
|
|
|
|
(defun sha256 (object)
|
|
|
|
(secure-hash 'sha256 object nil nil t))
|
|
|
|
(define-hmac-function hmac-sha256 sha256 64 32)
|
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--sign-message (session parts)
|
2017-12-13 11:27:13 -06:00
|
|
|
(if (> (length (jupyter-session-key session)) 0)
|
|
|
|
(cl-loop
|
|
|
|
for b across (hmac-sha256 (mapconcat #'identity parts "")
|
|
|
|
(jupyter-session-key session))
|
|
|
|
concat (format "%02x" b))
|
|
|
|
""))
|
|
|
|
|
2017-12-22 00:38:03 -06:00
|
|
|
;; TODO: Better UUID randomness, `cl-random' seeds the random state with the
|
|
|
|
;; current time but only to second resolution.
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--new-uuid ()
|
2017-12-13 11:27:13 -06:00
|
|
|
"Make a version 4 UUID."
|
2017-12-14 16:14:12 -06:00
|
|
|
(format "%04x%04x-%04x-%04x-%04x-%06x%06x"
|
|
|
|
(cl-random 65536)
|
|
|
|
(cl-random 65536)
|
|
|
|
(cl-random 65536)
|
|
|
|
;; https://tools.ietf.org/html/rfc4122
|
|
|
|
(let ((r (cl-random 65536)))
|
|
|
|
(if (= (byteorder) ?l)
|
|
|
|
;; ?l = little-endian
|
|
|
|
(logior (logand r 4095) 16384)
|
|
|
|
;; big-endian
|
|
|
|
(logior (logand r 65295) 64)))
|
|
|
|
(let ((r (cl-random 65536)))
|
|
|
|
(if (= (byteorder) ?l)
|
|
|
|
(logior (logand r 49151) 32768)
|
|
|
|
(logior (logand r 65471) 128)))
|
|
|
|
(cl-random 16777216)
|
|
|
|
(cl-random 16777216)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--split-identities (parts)
|
2017-12-13 11:27:13 -06:00
|
|
|
"Extract the identities from a list of message PARTS."
|
|
|
|
(let ((idents nil))
|
|
|
|
(if (catch 'found-delim
|
|
|
|
(while (car parts)
|
|
|
|
(when (string= (car parts) jupyter-message-delimiter)
|
|
|
|
(setq parts (cdr parts)
|
|
|
|
idents (nreverse idents))
|
|
|
|
(throw 'found-delim t))
|
|
|
|
(setq idents (cons (car parts) idents)
|
|
|
|
parts (cdr parts))))
|
|
|
|
(cons idents parts)
|
|
|
|
(error "Message delimiter not in message list"))))
|
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--message-header (session msg-type)
|
2017-12-13 11:27:13 -06:00
|
|
|
(list
|
2017-12-14 13:28:58 -06:00
|
|
|
:msg_id (jupyter--new-uuid)
|
2017-12-13 11:27:13 -06:00
|
|
|
:msg_type msg-type
|
|
|
|
:version jupyter-protocol-version
|
|
|
|
:username user-login-name
|
|
|
|
:session (jupyter-session-id session)
|
2017-12-21 18:09:45 -06:00
|
|
|
:date (format-time-string "%FT%T.%6N%z" (current-time))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
|
|
|
;;; Encode/decoding messages
|
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--encode-object (object)
|
2017-12-21 18:10:19 -06:00
|
|
|
;; Fix encoding recursive objects so that nil will get turned into "{}"
|
|
|
|
(cl-letf (((symbol-function 'json-encode-keyword)
|
|
|
|
(lambda (keyword)
|
|
|
|
(cond ((eq keyword t) "true")
|
|
|
|
((eq keyword json-false) "false")
|
|
|
|
((eq keyword json-null) "{}")))))
|
|
|
|
(encode-coding-string (json-encode-plist object) 'utf-8)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--decode-string (str)
|
2017-12-13 11:27:13 -06:00
|
|
|
(let ((json-object-type 'plist)
|
2017-12-22 00:38:03 -06:00
|
|
|
(json-array-type 'list)
|
|
|
|
(json-false nil))
|
2017-12-13 11:27:13 -06:00
|
|
|
(json-read-from-string (decode-coding-string str 'utf-8))))
|
|
|
|
|
2017-12-22 00:38:03 -06:00
|
|
|
(defun jupyter--decode-time (str)
|
|
|
|
(let* ((time (date-to-time str)))
|
|
|
|
(when (string-match "T.+\\(\\(?:\\.\\|,\\)[0-9]+\\)" str)
|
2017-12-27 00:13:43 -06:00
|
|
|
(setq time (list (car time)
|
|
|
|
(cadr time)
|
|
|
|
(ceiling (* 1000000 (string-to-number
|
|
|
|
(match-string 1 str))))
|
|
|
|
0)))
|
2017-12-22 00:38:03 -06:00
|
|
|
time))
|
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(cl-defun jupyter--encode-message (session
|
|
|
|
type
|
|
|
|
&key idents
|
|
|
|
content
|
|
|
|
parent-header
|
|
|
|
metadata
|
|
|
|
buffers)
|
2017-12-13 11:27:13 -06:00
|
|
|
(declare (indent 2))
|
|
|
|
(cl-check-type session jupyter-session)
|
|
|
|
(cl-check-type metadata json-plist)
|
|
|
|
(cl-check-type content json-plist)
|
|
|
|
(cl-check-type buffers list)
|
2017-12-14 13:28:58 -06:00
|
|
|
(let* ((header (jupyter--message-header session type))
|
2017-12-13 11:27:13 -06:00
|
|
|
(msg-id (plist-get header :msg_id))
|
2017-12-14 13:28:58 -06:00
|
|
|
(parts (mapcar #'jupyter--encode-object (list header
|
|
|
|
parent-header
|
|
|
|
metadata
|
|
|
|
content))))
|
2017-12-13 11:27:13 -06:00
|
|
|
(cons msg-id
|
|
|
|
(append
|
|
|
|
(when idents (if (stringp idents) (list idents) idents))
|
|
|
|
(list jupyter-message-delimiter
|
2017-12-14 13:28:58 -06:00
|
|
|
(jupyter--sign-message session parts))
|
2017-12-13 11:27:13 -06:00
|
|
|
parts
|
|
|
|
buffers))))
|
|
|
|
|
2017-12-14 13:28:58 -06:00
|
|
|
(defun jupyter--decode-message (session parts)
|
2017-12-13 11:27:13 -06:00
|
|
|
(when (< (length parts) 5)
|
|
|
|
(error "Malformed message. Minimum length of parts is 5."))
|
|
|
|
(when (jupyter-session-key session)
|
|
|
|
(let ((signature (car parts)))
|
|
|
|
(when (seq-empty-p signature)
|
|
|
|
(error "Unsigned message."))
|
|
|
|
;; TODO: digest_history
|
|
|
|
;; https://github.com/jupyter/jupyter_client/blob/7a0278af7c1652ac32356d6f00ae29d24d78e61c/jupyter_client/session.py#L915
|
2017-12-14 13:28:58 -06:00
|
|
|
(unless (string= (jupyter--sign-message session (seq-subseq parts 1 5))
|
2017-12-13 11:27:13 -06:00
|
|
|
signature)
|
|
|
|
(error "Invalid signature: %s" signature))))
|
|
|
|
(cl-destructuring-bind
|
|
|
|
(header parent-header metadata content &optional buffers)
|
|
|
|
(cdr parts)
|
2017-12-22 00:38:03 -06:00
|
|
|
(let ((header (jupyter--decode-string header))
|
|
|
|
(parent-header (jupyter--decode-string parent-header)))
|
|
|
|
;; Decode dates to time objects as returned by `current-time'
|
2017-12-27 00:15:48 -06:00
|
|
|
(mapc (lambda (plist)
|
|
|
|
(let ((date (plist-get plist :date)))
|
|
|
|
(when date
|
|
|
|
(plist-put
|
|
|
|
plist :date (jupyter--decode-time date)))))
|
2017-12-22 00:38:03 -06:00
|
|
|
(list header parent-header))
|
2017-12-13 11:27:13 -06:00
|
|
|
(list
|
|
|
|
:header header
|
|
|
|
:msg_id (plist-get header :msg_id)
|
|
|
|
:msg_type (plist-get header :msg_type)
|
2017-12-22 00:38:03 -06:00
|
|
|
:parent_header parent-header
|
2017-12-14 13:28:58 -06:00
|
|
|
:metadata (jupyter--decode-string metadata)
|
|
|
|
:content (jupyter--decode-string content)
|
2017-12-13 11:27:13 -06:00
|
|
|
:buffers buffers))))
|
|
|
|
|
2017-12-17 02:39:16 -06:00
|
|
|
;;; Sending/receiving
|
|
|
|
|
|
|
|
(cl-defmethod jupyter--send-encoded ((session jupyter-session)
|
|
|
|
socket
|
|
|
|
type
|
|
|
|
message
|
|
|
|
&optional flags)
|
|
|
|
(declare (indent 1))
|
|
|
|
(cl-destructuring-bind (msg-id . msg)
|
|
|
|
(jupyter--encode-message session type :content message)
|
|
|
|
;; TODO: Check for EAGAIN and reschedule the message for sending
|
|
|
|
(zmq-send-multipart socket msg flags)
|
|
|
|
msg-id))
|
|
|
|
|
|
|
|
(cl-defmethod jupyter--recv-decoded ((session jupyter-session) socket &optional flags)
|
2017-12-27 00:17:03 -06:00
|
|
|
(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))))))
|
2017-12-17 02:39:16 -06:00
|
|
|
|
2017-12-22 00:38:03 -06:00
|
|
|
;;; Control messages
|
|
|
|
|
|
|
|
(cl-defun jupyter-interrupt-request ()
|
|
|
|
(list))
|
|
|
|
|
2017-12-13 11:27:13 -06:00
|
|
|
;;; stdin messages
|
|
|
|
|
2017-12-14 13:29:31 -06:00
|
|
|
(cl-defun jupyter-input-reply (&key value)
|
2017-12-13 11:27:13 -06:00
|
|
|
(cl-check-type value string)
|
|
|
|
(list :value value))
|
|
|
|
|
|
|
|
;;; shell messages
|
|
|
|
|
2017-12-22 00:38:03 -06:00
|
|
|
(cl-defun jupyter-kernel-info-request ()
|
|
|
|
(list))
|
|
|
|
|
2017-12-13 11:27:13 -06:00
|
|
|
(cl-defun jupyter-execute-request (&key
|
|
|
|
code
|
|
|
|
(silent nil)
|
|
|
|
(store-history t)
|
|
|
|
(user-expressions nil)
|
|
|
|
(allow-stdin t)
|
|
|
|
(stop-on-error nil))
|
|
|
|
(cl-check-type code string)
|
|
|
|
(cl-check-type user-expressions json-plist)
|
2017-12-14 13:29:31 -06:00
|
|
|
(list :code code :silent (if silent t jupyter--false)
|
|
|
|
:store_history (if store-history t jupyter--false)
|
2017-12-13 11:27:13 -06:00
|
|
|
:user_expressions user-expressions
|
2017-12-14 13:29:31 -06:00
|
|
|
:allow_stdin (if allow-stdin t jupyter--false)
|
|
|
|
:stop_on_error (if stop-on-error t jupyter--false)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-14 13:29:31 -06:00
|
|
|
(cl-defun jupyter-inspect-request (&key code pos detail)
|
2017-12-13 11:27:13 -06:00
|
|
|
(setq detail (or detail 0))
|
|
|
|
(unless (member detail '(0 1))
|
|
|
|
(error "Detail can only be 0 or 1 (%s)" detail))
|
|
|
|
(when (markerp pos)
|
|
|
|
(setq pos (marker-position pos)))
|
|
|
|
(cl-check-type code string)
|
|
|
|
(cl-check-type pos integer)
|
|
|
|
(list :code code :cursor_pos pos :detail_level detail))
|
|
|
|
|
2017-12-14 13:29:31 -06:00
|
|
|
(cl-defun jupyter-complete-request (&key code pos)
|
2017-12-13 11:27:13 -06:00
|
|
|
(when (markerp pos)
|
|
|
|
(setq pos (marker-position pos)))
|
|
|
|
(cl-check-type code string)
|
|
|
|
(cl-check-type pos integer)
|
|
|
|
(list :code code :cursor_pos pos))
|
|
|
|
|
|
|
|
(cl-defun jupyter-history-request (&key
|
|
|
|
output
|
|
|
|
raw
|
|
|
|
hist-access-type
|
|
|
|
session
|
|
|
|
start
|
|
|
|
stop
|
|
|
|
n
|
|
|
|
pattern
|
|
|
|
unique)
|
|
|
|
(unless (member hist-access-type '("range" "tail" "search"))
|
|
|
|
(error "History access type can only be one of (range, tail, search)"))
|
|
|
|
(append
|
2017-12-14 13:29:31 -06:00
|
|
|
(list :output (if output t jupyter--false) :raw (if raw t jupyter--false)
|
2017-12-13 11:27:13 -06:00
|
|
|
:hist_access_type hist-access-type)
|
|
|
|
(cond
|
|
|
|
((equal hist-access-type "range")
|
|
|
|
(cl-check-type session integer)
|
|
|
|
(cl-check-type start integer)
|
|
|
|
(cl-check-type stop integer)
|
|
|
|
(list :session session :start start :stop stop))
|
|
|
|
((equal hist-access-type "tail")
|
|
|
|
(cl-check-type n integer)
|
|
|
|
(list :n n))
|
|
|
|
((equal hist-access-type "search")
|
|
|
|
(cl-check-type pattern string)
|
|
|
|
(cl-check-type n integer)
|
2017-12-14 13:29:31 -06:00
|
|
|
(list :pattern pattern :unique (if unique t jupyter--false) :n n)))))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-14 13:29:31 -06:00
|
|
|
(cl-defun jupyter-is-complete-request (&key code)
|
2017-12-13 11:27:13 -06:00
|
|
|
(cl-check-type code string)
|
|
|
|
(list :code code))
|
|
|
|
|
2017-12-14 13:29:31 -06:00
|
|
|
(cl-defun jupyter-comm-info-request (&key target-name)
|
2017-12-13 11:27:13 -06:00
|
|
|
(when target-name
|
|
|
|
(cl-check-type target-name string)
|
|
|
|
(list :target_name target-name)))
|
|
|
|
|
2017-12-14 13:29:31 -06:00
|
|
|
(cl-defun jupyter-shutdown-request (&key restart)
|
|
|
|
(list :restart (if restart t jupyter--false)))
|
2017-12-13 11:27:13 -06:00
|
|
|
|
2017-12-15 22:28:45 -06:00
|
|
|
;;; Convenience functions
|
|
|
|
|
|
|
|
(defun jupyter-message-id (msg)
|
|
|
|
(plist-get msg :msg_id))
|
|
|
|
|
|
|
|
(defun jupyter-message-parent-id (msg)
|
|
|
|
(jupyter-message-id
|
|
|
|
(plist-get msg :parent_header)))
|
|
|
|
|
|
|
|
(defun jupyter-message-status-idle-p (msg)
|
|
|
|
(and (equal (plist-get msg :msg_type) "status")
|
|
|
|
(equal (plist-get (plist-get msg :content) :execution_state)
|
|
|
|
"idle")))
|
|
|
|
|
2017-12-22 00:38:03 -06:00
|
|
|
(defun jupyter-message-content (msg)
|
|
|
|
(plist-get msg :content))
|
|
|
|
|
|
|
|
(defun jupyter-message-time (msg)
|
|
|
|
"Convert the `:date' field of the message into a time object.
|
|
|
|
See `current-time'."
|
|
|
|
(plist-get (plist-get msg :header) :date))
|
|
|
|
|
2017-12-15 22:28:45 -06:00
|
|
|
(defun jupyter-message-type (msg)
|
|
|
|
(plist-get msg :msg_type))
|
|
|
|
|
2017-12-13 11:27:13 -06:00
|
|
|
(provide 'jupyter-messages)
|
|
|
|
|
|
|
|
;; Local Variables:
|
|
|
|
;; byte-compile-warnings: (not free-vars)
|
|
|
|
;; End:
|