Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

864 lines
37 KiB

  1. ;;; ein-kernel.el --- Communicate with IPython notebook server -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2012- Takafumi Arakaki
  3. ;; Author: Takafumi Arakaki <aka.tkf at gmail.com>
  4. ;; This file is NOT part of GNU Emacs.
  5. ;; ein-kernel.el is free software: you can redistribute it and/or modify
  6. ;; it under the terms of the GNU General Public License as published by
  7. ;; the Free Software Foundation, either version 3 of the License, or
  8. ;; (at your option) any later version.
  9. ;; ein-kernel.el is distributed in the hope that it will be useful,
  10. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. ;; GNU General Public License for more details.
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with ein-kernel.el. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Commentary:
  16. ;; `ein:kernel' is the proxy class of notebook server state.
  17. ;; It agglomerates both the "kernel" and "session" objects of server described here
  18. ;; https://github.com/jupyter/jupyter/wiki/Jupyter-Notebook-Server-API
  19. ;; It may have been better to keep them separate to allow parallel reasoning with
  20. ;; the notebook server, but that time is past.
  21. ;;; Code:
  22. (require 'ansi-color)
  23. (require 'ein-core)
  24. (require 'ein-classes)
  25. (require 'ein-log)
  26. ;; FIXME: use websocket.el directly once v1.0 is released.
  27. (require 'ein-websocket)
  28. (require 'ein-events)
  29. (require 'ein-query)
  30. (require 'ein-ipdb)
  31. ;; "Public" getters. Use them outside of this package.
  32. (defun ein:$kernel-session-url (kernel)
  33. (concat "/api/sessions/" (ein:$kernel-session-id kernel)))
  34. ;;;###autoload
  35. (defalias 'ein:kernel-url-or-port 'ein:$kernel-url-or-port)
  36. ;;;###autoload
  37. (defalias 'ein:kernel-id 'ein:$kernel-kernel-id)
  38. (defcustom ein:pre-kernel-execute-functions nil
  39. "List of functions to call before sending a message to the kernel for execution. Each function is called with the message (see `ein:kernel--get-msg') about to be sent."
  40. :type 'list
  41. :group 'ein)
  42. (defcustom ein:on-shell-reply-functions nil
  43. "List of functions to call when the kernel responds on the shell channel.
  44. Each function should have the call signature: msg-id header content metadata"
  45. :type 'list
  46. :group 'ein)
  47. ;;; Initialization and connection.
  48. (defun ein:kernel-new (url-or-port path kernelspec base-url events &optional api-version)
  49. (make-ein:$kernel
  50. :url-or-port url-or-port
  51. :path path
  52. :kernelspec kernelspec
  53. :events events
  54. :api-version (or api-version 5)
  55. :session-id (ein:utils-uuid)
  56. :kernel-id nil
  57. :websocket nil
  58. :base-url base-url
  59. :stdin-activep nil
  60. :oinfo-cache (make-hash-table :test #'equal)
  61. :username "username"
  62. :msg-callbacks (make-hash-table :test 'equal)))
  63. (defun ein:kernel-del (kernel)
  64. "Destructor for `ein:$kernel'."
  65. (ein:kernel-disconnect kernel))
  66. (defun ein:kernel-language (kernel)
  67. "Return a string naming the language used by kernel `kernel'. Typical return values might be 'python', or 'julia', or 'R' (among others)."
  68. (ein:$kernelspec-language (ein:$kernel-kernelspec kernel)))
  69. (defun ein:kernel--get-msg (kernel msg-type content)
  70. (list
  71. :header (list
  72. :msg_id (ein:utils-uuid)
  73. :username (ein:$kernel-username kernel)
  74. :session (ein:$kernel-session-id kernel)
  75. :version "5.0"
  76. :date (format-time-string "%Y-%m-%dT%T" (current-time)) ; ISO 8601 timestamp
  77. :msg_type msg-type)
  78. :metadata (make-hash-table)
  79. :content content
  80. :parent_header (make-hash-table)))
  81. (cl-defun ein:kernel-session-p (kernel callback &optional iteration)
  82. "Don't make any changes on the server side. CALLBACK with arity 2, kernel and a boolean whether session exists on server."
  83. (unless iteration
  84. (setq iteration 0))
  85. (let ((session-id (ein:$kernel-session-id kernel)))
  86. (ein:query-singleton-ajax
  87. (list 'kernel-session-p session-id)
  88. (ein:url (ein:$kernel-url-or-port kernel) "api/sessions" session-id)
  89. :type "GET"
  90. :sync ein:force-sync
  91. :parser #'ein:json-read
  92. :complete (apply-partially #'ein:kernel-session-p--complete session-id)
  93. :success (apply-partially #'ein:kernel-session-p--success kernel session-id callback)
  94. :error (apply-partially #'ein:kernel-session-p--error kernel callback iteration))))
  95. (cl-defun ein:kernel-session-p--complete (_session-id &key data response &allow-other-keys
  96. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  97. (ein:log 'debug "ein:kernel-session-p--complete %s" resp-string))
  98. (cl-defun ein:kernel-session-p--error (kernel callback iteration &key error-thrown _symbol-status data &allow-other-keys)
  99. (if (ein:aand (plist-get data :message) (cl-search "not found" it))
  100. (when callback (funcall callback kernel nil))
  101. (let* ((max-tries 3)
  102. (tries-left (1- (- max-tries iteration))))
  103. (ein:log 'verbose "ein:kernel-session-p--error [%s], %s tries left"
  104. (car error-thrown) tries-left)
  105. (if (> tries-left 0)
  106. (ein:kernel-session-p kernel callback (1+ iteration))))))
  107. (cl-defun ein:kernel-session-p--success (kernel session-id callback &key data &allow-other-keys)
  108. (let ((session-p (equal (plist-get data :id) session-id)))
  109. (ein:log 'verbose "ein:kernel-session-p--success: session-id=%s session-p=%s"
  110. session-id session-p)
  111. (when callback (funcall callback kernel session-p))))
  112. (cl-defun ein:kernel-restart-session (kernel)
  113. "Server side delete of KERNEL session and subsequent restart with all new state"
  114. (ein:kernel-delete-session
  115. kernel
  116. (lambda (kernel)
  117. (ein:events-trigger (ein:$kernel-events kernel) 'status_restarting.Kernel)
  118. (ein:kernel-retrieve-session kernel 0
  119. (lambda (kernel)
  120. (ein:events-trigger (ein:$kernel-events kernel)
  121. 'status_restarted.Kernel))))))
  122. (cl-defun ein:kernel-retrieve-session (kernel &optional iteration callback)
  123. "Formerly ein:kernel-start, but that was misnomer because 1. the server really starts a session (and an accompanying kernel), and 2. it may not even start a session if one exists for the same path.
  124. If 'picking up from where we last left off', that is, we restart emacs and reconnect to same server, jupyter will hand us back the original, still running session.
  125. The server logic is here (could not find other documentation)
  126. https://github.com/jupyter/notebook/blob/04a686dbaf9dfe553324a03cb9e6f778cf1e3da1/notebook/services/sessions/handlers.py#L56-L81
  127. CALLBACK of arity 1, the kernel.
  128. "
  129. (unless iteration
  130. (setq iteration 0))
  131. (if (<= (ein:$kernel-api-version kernel) 2)
  132. (error "Api %s unsupported" (ein:$kernel-api-version kernel))
  133. (let ((kernel-id (ein:$kernel-kernel-id kernel))
  134. (kernelspec (ein:$kernel-kernelspec kernel))
  135. (path (ein:$kernel-path kernel)))
  136. (ein:query-singleton-ajax
  137. (list 'kernel-retrieve-session kernel-id)
  138. (ein:url (ein:$kernel-url-or-port kernel) "api/sessions")
  139. :type "POST"
  140. :data (json-encode
  141. (cond ((<= (ein:$kernel-api-version kernel) 4)
  142. `(("notebook" .
  143. (("path" . ,path)))
  144. ,@(if kernelspec
  145. `(("kernel" .
  146. (("name" . ,(ein:$kernelspec-name kernelspec))))))))
  147. (t `(("path" . ,path)
  148. ("type" . "notebook")
  149. ,@(if kernelspec
  150. `(("kernel" .
  151. (("name" . ,(ein:$kernelspec-name kernelspec))
  152. ,@(if kernel-id
  153. `(("id" . ,kernel-id)))))))))))
  154. :sync ein:force-sync
  155. :parser #'ein:json-read
  156. :complete (apply-partially #'ein:kernel-retrieve-session--complete kernel callback)
  157. :success (apply-partially #'ein:kernel-retrieve-session--success kernel callback)
  158. :error (apply-partially #'ein:kernel-retrieve-session--error kernel iteration callback)))))
  159. (cl-defun ein:kernel-retrieve-session--complete (_kernel _callback &key data response &allow-other-keys
  160. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  161. (ein:log 'debug "ein:kernel-retrieve-session--complete %s" resp-string))
  162. (cl-defun ein:kernel-retrieve-session--error (kernel iteration callback &key error-thrown _symbol-status &allow-other-keys)
  163. (let* ((max-tries 3)
  164. (tries-left (1- (- max-tries iteration))))
  165. (ein:log 'verbose "ein:kernel-retrieve-session--error [%s], %s tries left"
  166. (car error-thrown) tries-left)
  167. (sleep-for 0 (* (1+ iteration) 500))
  168. (if (> tries-left 0)
  169. (ein:kernel-retrieve-session kernel (1+ iteration) callback))))
  170. (cl-defun ein:kernel-retrieve-session--success (kernel callback &key data &allow-other-keys)
  171. (let ((session-id (plist-get data :id)))
  172. (if (plist-get data :kernel)
  173. (setq data (plist-get data :kernel)))
  174. (cl-destructuring-bind (&key id &allow-other-keys) data
  175. (ein:log 'verbose "ein:kernel-retrieve-session--success: kernel-id=%s session-id=%s"
  176. id session-id)
  177. (setf (ein:$kernel-kernel-id kernel) id)
  178. (setf (ein:$kernel-session-id kernel) session-id)
  179. (setf (ein:$kernel-ws-url kernel) (ein:kernel--ws-url (ein:$kernel-url-or-port kernel)))
  180. (setf (ein:$kernel-kernel-url kernel)
  181. (concat (file-name-as-directory (ein:$kernel-base-url kernel)) id)))
  182. (ein:kernel-start-websocket kernel callback)))
  183. (defun ein:kernel-reconnect-session (kernel &optional callback)
  184. "Check if session still exists. If it does, retrieve it. If it doesn't, ask the user to create a new session (ein:kernel-retrieve-session both retrieves and creates).
  185. CALLBACK takes one argument kernel (e.g., execute cell now that we're reconnected)"
  186. (ein:kernel-disconnect kernel)
  187. (ein:kernel-session-p
  188. kernel
  189. (apply-partially
  190. (lambda (callback* kernel session-p)
  191. (when (or session-p
  192. (and (not noninteractive) (y-or-n-p "Session not found. Restart?")))
  193. (ein:events-trigger (ein:$kernel-events kernel) 'status_reconnecting.Kernel)
  194. (ein:kernel-retrieve-session
  195. kernel 0
  196. (apply-partially
  197. (lambda (callback** kernel)
  198. (ein:events-trigger (ein:$kernel-events kernel)
  199. 'status_reconnected.Kernel)
  200. (when callback** (funcall callback** kernel)))
  201. callback*))))
  202. callback)))
  203. (defun ein:kernel--ws-url (url-or-port)
  204. "Assuming URL-OR-PORT already normalized by `ein:url'
  205. See https://github.com/ipython/ipython/pull/3307"
  206. (let* ((parsed-url (url-generic-parse-url url-or-port))
  207. (protocol (if (string= (url-type parsed-url) "https") "wss" "ws")))
  208. (format "%s://%s:%s%s"
  209. protocol
  210. (url-host parsed-url)
  211. (url-port parsed-url)
  212. (url-filename parsed-url))))
  213. (defun ein:kernel-send-cookie (channel host)
  214. ;; cookie can be an empty string for IPython server with no password,
  215. ;; but something must be sent to start channel.
  216. (let ((cookie (ein:query-get-cookie host "/")))
  217. (ein:websocket-send channel cookie)))
  218. (defun ein:kernel--handle-websocket-reply (kernel _ws frame)
  219. (ein:and-let* ((packet (websocket-frame-payload frame))
  220. (channel (plist-get (ein:json-read-from-string packet) :channel)))
  221. (cond ((string-equal channel "iopub")
  222. (ein:kernel--handle-iopub-reply kernel packet))
  223. ((string-equal channel "shell")
  224. (ein:kernel--handle-shell-reply kernel packet))
  225. ((string-equal channel "stdin")
  226. (ein:kernel--handle-stdin-reply kernel packet))
  227. (t (ein:log 'warn "Received reply from unforeseen channel %s" channel)))))
  228. (defun ein:start-single-websocket (kernel open-callback)
  229. "OPEN-CALLBACK (kernel) (e.g., execute cell)"
  230. (let ((ws-url (concat (ein:$kernel-ws-url kernel)
  231. (ein:$kernel-kernel-url kernel)
  232. "/channels?session_id="
  233. (ein:$kernel-session-id kernel))))
  234. (ein:log 'verbose "WS start: %s" ws-url)
  235. (setf (ein:$kernel-websocket kernel)
  236. (ein:websocket ws-url kernel
  237. (apply-partially #'ein:kernel--handle-websocket-reply kernel)
  238. (lambda (ws)
  239. (let* ((websocket (websocket-client-data ws))
  240. (kernel (ein:$websocket-kernel websocket)))
  241. (unless (ein:$websocket-closed-by-client websocket)
  242. (ein:log 'verbose "WS closed unexpectedly: %s" (websocket-url ws))
  243. (ein:kernel-disconnect kernel))))
  244. (apply-partially
  245. (lambda (cb ws)
  246. (let* ((websocket (websocket-client-data ws))
  247. (kernel (ein:$websocket-kernel websocket)))
  248. (when (ein:kernel-live-p kernel)
  249. (ein:kernel-run-after-start-hook kernel)
  250. (when cb
  251. (funcall cb kernel)))
  252. (ein:log 'verbose "WS opened: %s" (websocket-url ws))))
  253. open-callback)))))
  254. (defun ein:kernel-start-websocket (kernel callback)
  255. (cond ((<= (ein:$kernel-api-version kernel) 2)
  256. (error "Api version %s unsupported" (ein:$kernel-api-version kernel)))
  257. (t (ein:start-single-websocket kernel callback))))
  258. (defun ein:kernel-on-connect (_kernel _content _metadata)
  259. (ein:log 'info "Kernel connect_request_reply received."))
  260. (defun ein:kernel-run-after-start-hook (kernel)
  261. (ein:log 'debug "EIN:KERNEL-RUN-AFTER-START-HOOK")
  262. (mapc #'ein:funcall-packed
  263. (ein:$kernel-after-start-hook kernel)))
  264. (defun ein:kernel-disconnect (kernel)
  265. "Close websocket connection to running kernel, but do not
  266. delete the kernel on the server side"
  267. (ein:events-trigger (ein:$kernel-events kernel) 'status_disconnected.Kernel)
  268. (ein:aif (ein:$kernel-websocket kernel)
  269. (progn (ein:websocket-close it)
  270. (setf (ein:$kernel-websocket kernel) nil))))
  271. (defun ein:kernel-live-p (kernel)
  272. (and (ein:$kernel-p kernel)
  273. (ein:aand (ein:$kernel-websocket kernel) (ein:websocket-open-p it))))
  274. (defun ein:kernel-when-ready (kernel callback)
  275. "Execute CALLBACK of arity 1 (the kernel) when KERNEL is ready. Warn user otherwise."
  276. (if (ein:kernel-live-p kernel)
  277. (funcall callback kernel)
  278. (ein:log 'verbose "Kernel %s unavailable" (ein:$kernel-kernel-id kernel))
  279. (ein:kernel-reconnect-session kernel callback)))
  280. ;;; Main public methods
  281. ;; NOTE: The argument CALLBACKS for the following functions is almost
  282. ;; same as the JS implementation in IPython. However, as Emacs
  283. ;; lisp does not support closure, value is "packed" using
  284. ;; `cons': `car' is the actual callback function and `cdr' is
  285. ;; its first argument. It's like using `cons' instead of
  286. ;; `$.proxy'.
  287. (defun ein:kernel-object-info-request (kernel objname callbacks &optional cursor-pos detail-level)
  288. "Send object info request of OBJNAME to KERNEL.
  289. When calling this method pass a CALLBACKS structure of the form:
  290. (:object_info_reply (FUNCTION . ARGUMENT))
  291. Call signature::
  292. (`funcall' FUNCTION ARGUMENT CONTENT METADATA)
  293. CONTENT and METADATA are given by `object_info_reply' message.
  294. `object_info_reply' message is documented here:
  295. http://ipython.org/ipython-doc/dev/development/messaging.html#object-information
  296. "
  297. (cl-assert (ein:kernel-live-p kernel) nil "object_info_reply: Kernel is not active.")
  298. (when objname
  299. (if (<= (ein:$kernel-api-version kernel) 2)
  300. (error "Api version %s unsupported" (ein:$kernel-api-version kernel)))
  301. (let* ((content (if (< (ein:$kernel-api-version kernel) 5)
  302. (list
  303. ;; :text ""
  304. :oname (format "%s" objname)
  305. :cursor_pos (or cursor-pos 0)
  306. :detail_level (or detail-level 0))
  307. (list
  308. :code (format "%s" objname)
  309. :cursor_pos (or cursor-pos 0)
  310. :detail_level (or detail-level 0))))
  311. (msg (ein:kernel--get-msg kernel "inspect_request"
  312. (append content (list :detail_level 1))))
  313. (msg-id (plist-get (plist-get msg :header) :msg_id)))
  314. (ein:websocket-send-shell-channel kernel msg)
  315. (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks))))
  316. (cl-defun ein:kernel-execute (kernel code &optional callbacks
  317. &key
  318. (silent t)
  319. (store-history t)
  320. (user-expressions (make-hash-table))
  321. (allow-stdin t)
  322. (stop-on-error nil))
  323. "Execute CODE on KERNEL.
  324. When calling this method pass a CALLBACKS structure of the form:
  325. (:execute_reply EXECUTE-REPLY-CALLBACK
  326. :output OUTPUT-CALLBACK
  327. :clear_output CLEAR-OUTPUT-CALLBACK
  328. :set_next_input SET-NEXT-INPUT)
  329. Right hand sides ending -CALLBACK above must cons a FUNCTION and its
  330. `packed' ARGUMENT which is a sublist of args:
  331. (FUNCTION . ARGUMENT)
  332. Call signature
  333. --------------
  334. ::
  335. (`funcall' EXECUTE-REPLY-CALLBACK ARGUMENT CONTENT METADATA)
  336. (`funcall' OUTPUT-CALLBACK ARGUMENT MSG-TYPE CONTENT METADATA)
  337. (`funcall' CLEAR-OUTPUT-CALLBACK ARGUMENT CONTENT METADATA)
  338. (`funcall' SET-NEXT-INPUT ARGUMENT TEXT)
  339. * Both CONTENT and METADATA objects are plist.
  340. * The MSG-TYPE argument for OUTPUT-CALLBACK is a string
  341. (one of `stream', `display_data', `pyout' and `pyerr').
  342. * The CONTENT object for CLEAR-OUTPUT-CALLBACK has
  343. `stdout', `stderr' and `other' fields that are booleans.
  344. * The SET-NEXT-INPUT callback will be passed the `set_next_input' payload,
  345. which is a string.
  346. See `ein:kernel--handle-shell-reply' for how the callbacks are called.
  347. Links
  348. -----
  349. * For general description of CONTENT and METADATA:
  350. http://ipython.org/ipython-doc/dev/development/messaging.html#general-message-format
  351. * `execute_reply' message is documented here:
  352. http://ipython.org/ipython-doc/dev/development/messaging.html#execute
  353. * Output type messages is documented here:
  354. http://ipython.org/ipython-doc/dev/development/messaging.html#messages-on-the-pub-sub-socket
  355. Sample implementations
  356. ----------------------
  357. * `ein:cell--handle-execute-reply'
  358. * `ein:cell--handle-output'
  359. * `ein:cell--handle-clear-output'
  360. * `ein:cell--handle-set-next-input'
  361. "
  362. ;; FIXME: Consider changing callback to use `&key'.
  363. ;; Otherwise, adding new arguments to callback requires
  364. ;; backward incompatible changes (hence a big diff), unlike
  365. ;; Javascript. Downside of this is that there is no short way
  366. ;; to write anonymous callback because there is no `lambda*'.
  367. ;; You can use `function*', but that's bit long...
  368. ;; FIXME: Consider allowing a list of fixed argument so that the
  369. ;; call signature becomes something like:
  370. ;; (funcall FUNCTION [ARG ...] CONTENT METADATA)
  371. (cl-assert (ein:kernel-live-p kernel) nil "execute_reply: Kernel is not active.")
  372. (let* ((content (list
  373. :code code
  374. :silent (or silent json-false)
  375. :store_history (or store-history json-false)
  376. :user_expressions user-expressions
  377. :allow_stdin allow-stdin
  378. :stop_on_error (or stop-on-error json-false)))
  379. (msg (ein:kernel--get-msg kernel "execute_request" content))
  380. (msg-id (plist-get (plist-get msg :header) :msg_id)))
  381. (ein:log 'debug "KERNEL-EXECUTE: code=%s msg_id=%s" code msg-id)
  382. (run-hook-with-args 'ein:pre-kernel-execute-functions msg)
  383. (ein:websocket-send-shell-channel kernel msg)
  384. (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
  385. (unless silent
  386. (mapc #'ein:funcall-packed
  387. (ein:$kernel-after-execute-hook kernel)))
  388. msg-id))
  389. (defun ein:kernel-complete (kernel line cursor-pos callbacks errback)
  390. "Complete code at CURSOR-POS in a string LINE on KERNEL.
  391. CURSOR-POS is the position in the string LINE, not in the buffer.
  392. ERRBACK takes a string (error message).
  393. When calling this method pass a CALLBACKS structure of the form:
  394. (:complete_reply (FUNCTION . ARGUMENT))
  395. Call signature::
  396. (funcall FUNCTION ARGUMENT CONTENT METADATA)
  397. CONTENT and METADATA are given by `complete_reply' message.
  398. `complete_reply' message is documented here:
  399. http://ipython.org/ipython-doc/dev/development/messaging.html#complete
  400. "
  401. (condition-case err
  402. (let* ((content (if (< (ein:$kernel-api-version kernel) 4)
  403. (list
  404. ;; :text ""
  405. :line line
  406. :cursor_pos cursor-pos)
  407. (list
  408. :code line
  409. :cursor_pos cursor-pos)))
  410. (msg (ein:kernel--get-msg kernel "complete_request" content))
  411. (msg-id (plist-get (plist-get msg :header) :msg_id)))
  412. (cl-assert (ein:kernel-live-p kernel) nil "kernel not live")
  413. (ein:websocket-send-shell-channel kernel msg)
  414. (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
  415. msg-id)
  416. (error (if errback (funcall errback (error-message-string err))
  417. (ein:display-warning (error-message-string err) :error)))))
  418. (cl-defun ein:kernel-history-request (kernel callbacks
  419. &key
  420. (output nil)
  421. (raw t)
  422. (hist-access-type "tail")
  423. session
  424. start
  425. stop
  426. (n 10)
  427. pattern
  428. unique)
  429. "Request execution history to KERNEL.
  430. When calling this method pass a CALLBACKS structure of the form:
  431. (:history_reply (FUNCTION . ARGUMENT))
  432. Call signature::
  433. (`funcall' FUNCTION ARGUMENT CONTENT METADATA)
  434. CONTENT and METADATA are given by `history_reply' message.
  435. `history_reply' message is documented here:
  436. http://ipython.org/ipython-doc/dev/development/messaging.html#history
  437. Relevant Python code:
  438. * :py:method:`IPython.zmq.ipkernel.Kernel.history_request`
  439. * :py:class:`IPython.core.history.HistoryAccessor`
  440. "
  441. (cl-assert (ein:kernel-live-p kernel) nil "history_reply: Kernel is not active.")
  442. (let* ((content (list
  443. :output (ein:json-any-to-bool output)
  444. :raw (ein:json-any-to-bool raw)
  445. :hist_access_type hist-access-type
  446. :session session
  447. :start start
  448. :stop stop
  449. :n n
  450. :pattern pattern
  451. :unique unique))
  452. (msg (ein:kernel--get-msg kernel "history_request" content))
  453. (msg-id (plist-get (plist-get msg :header) :msg_id)))
  454. (ein:websocket-send-shell-channel kernel msg)
  455. (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
  456. msg-id))
  457. (defun ein:kernel-connect-request (kernel callbacks)
  458. "Request basic information for a KERNEL.
  459. When calling this method pass a CALLBACKS structure of the form::
  460. (:connect_reply (FUNCTION . ARGUMENT))
  461. Call signature::
  462. (`funcall' FUNCTION ARGUMENT CONTENT METADATA)
  463. CONTENT and METADATA are given by `kernel_info_reply' message.
  464. `connect_request' message is documented here:
  465. http://ipython.org/ipython-doc/dev/development/messaging.html#connect
  466. Example::
  467. (ein:kernel-connect-request
  468. (ein:get-kernel)
  469. '(:kernel_connect_reply (message . \"CONTENT: %S\\nMETADATA: %S\")))
  470. "
  471. ;(cl-assert (ein:kernel-live-p kernel) nil "connect_reply: Kernel is not active.")
  472. (let* ((msg (ein:kernel--get-msg kernel "connect_request" (make-hash-table)))
  473. (msg-id (plist-get (plist-get msg :header) :msg_id)))
  474. (ein:websocket-send-shell-channel kernel msg)
  475. (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
  476. msg-id))
  477. (defun ein:kernel-kernel-info-request (kernel callbacks)
  478. "Request core information of KERNEL.
  479. When calling this method pass a CALLBACKS structure of the form::
  480. (:kernel_info_reply (FUNCTION . ARGUMENT))
  481. Call signature::
  482. (`funcall' FUNCTION ARGUMENT CONTENT METADATA)
  483. CONTENT and METADATA are given by `kernel_info_reply' message.
  484. `kernel_info_reply' message is documented here:
  485. http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info
  486. Example::
  487. (ein:kernel-kernel-info-request
  488. (ein:get-kernel)
  489. '(:kernel_info_reply (message . \"CONTENT: %S\\nMETADATA: %S\")))
  490. "
  491. (cl-assert (ein:kernel-live-p kernel) nil "kernel_info_reply: Kernel is not active.")
  492. (ein:log 'debug "EIN:KERNEL-KERNEL-INFO-REQUEST: Sending request.")
  493. (let* ((msg (ein:kernel--get-msg kernel "kernel_info_request" nil))
  494. (msg-id (plist-get (plist-get msg :header) :msg_id)))
  495. (ein:websocket-send-shell-channel kernel msg)
  496. (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)
  497. msg-id))
  498. (defun ein:kernel-interrupt (kernel)
  499. (when (ein:kernel-live-p kernel)
  500. (ein:log 'info "Interrupting kernel")
  501. (ein:query-singleton-ajax
  502. (list 'kernel-interrupt (ein:$kernel-kernel-id kernel))
  503. (ein:url (ein:$kernel-url-or-port kernel)
  504. (ein:$kernel-kernel-url kernel)
  505. "interrupt")
  506. :type "POST"
  507. :success (lambda (&rest _ignore)
  508. (ein:log 'info "Sent interruption command.")))))
  509. (defun ein:kernel-delete--from-session-id (url session-id &optional callback)
  510. "Stop/delete a running kernel from a session id. May also specify a callback function of 0 args to be called once oepration is complete.
  511. We need this to have proper behavior for the 'Stop' command in the ein:notebooklist buffer."
  512. (ein:query-singleton-ajax
  513. (list 'kernel-delete-session session-id)
  514. (ein:url url "api/sessions" session-id)
  515. :success (apply-partially #'ein:kernel-delete--from-session-complete session-id callback)
  516. :error (apply-partially #'ein:kernel-delete--from-session-error session-id)
  517. :type "DELETE"))
  518. (defun ein:kernel-delete--from-session-complete (session-id callback &rest _)
  519. (ein:log 'info "Deleted session %s and its associated kernel process." session-id)
  520. (when callback
  521. (funcall callback)))
  522. (defun ein:kernel-delete--from-session-error (session-id &rest _)
  523. (ein:log 'info "Error, could not delete session %s." session-id))
  524. (defun ein:kernel-delete-session (kernel &optional callback)
  525. "Regardless of success or error, we clear all state variables of kernel and funcall CALLBACK (kernel)"
  526. (ein:and-let* ((session-id (ein:$kernel-session-id kernel)))
  527. (ein:query-singleton-ajax
  528. (list 'kernel-delete-session session-id)
  529. (ein:url (ein:$kernel-url-or-port kernel) "api/sessions" session-id)
  530. :type "DELETE"
  531. :complete (apply-partially #'ein:kernel-delete-session--complete kernel session-id callback)
  532. :error (apply-partially #'ein:kernel-delete-session--error session-id callback)
  533. :success (apply-partially #'ein:kernel-delete-session--success session-id callback))))
  534. (cl-defun ein:kernel-delete-session--error (session-id _callback
  535. &key _response error-thrown &allow-other-keys)
  536. (ein:log 'error "ein:kernel-delete-session--error %s: ERROR %s DATA %s"
  537. session-id (car error-thrown) (cdr error-thrown)))
  538. (cl-defun ein:kernel-delete-session--success (session-id _callback &key _data _symbol-status _response &allow-other-keys)
  539. (ein:log 'verbose "ein:kernel-delete-session--success: %s deleted" session-id))
  540. (cl-defun ein:kernel-delete-session--complete (kernel _session-id callback &key data response &allow-other-keys
  541. &aux (resp-string (format "STATUS: %s DATA: %s" (request-response-status-code response) data)))
  542. (ein:log 'debug "ein:kernel-delete-session--complete %s" resp-string)
  543. (ein:kernel-disconnect kernel)
  544. (when callback (funcall callback kernel)))
  545. ;; Reply handlers.
  546. (defun ein:kernel-get-callbacks-for-msg (kernel msg-id)
  547. (gethash msg-id (ein:$kernel-msg-callbacks kernel)))
  548. (defun ein:kernel-set-callbacks-for-msg (kernel msg-id callbacks)
  549. (puthash msg-id callbacks (ein:$kernel-msg-callbacks kernel)))
  550. (defun ein:kernel--handle-stdin-reply (kernel packet)
  551. (setf (ein:$kernel-stdin-activep kernel) t)
  552. (cl-destructuring-bind
  553. (&key header _parent_header _metadata content &allow-other-keys)
  554. (ein:json-read-from-string packet)
  555. (let ((msg-type (plist-get header :msg_type))
  556. (msg-id (plist-get header :msg_id))
  557. (password (plist-get content :password)))
  558. (ein:log 'debug "KERNEL--HANDLE-STDIN-REPLY: msg_type=%s msg_id=%s"
  559. msg-type msg-id)
  560. (cond ((string-equal msg-type "input_request")
  561. (if (not (eql password :json-false))
  562. (let* ((passwd (read-passwd (plist-get content :prompt)))
  563. (content (list :value passwd))
  564. (msg (ein:kernel--get-msg kernel "input_reply" content)))
  565. (ein:websocket-send-stdin-channel kernel msg)
  566. (setf (ein:$kernel-stdin-activep kernel) nil))
  567. (cond ((string-match "ipdb>" (plist-get content :prompt)) (ein:run-ipdb-session kernel "ipdb> "))
  568. ((string-match "(Pdb)" (plist-get content :prompt)) (ein:run-ipdb-session kernel "(Pdb) "))
  569. (t (let* ((in (read-string (plist-get content :prompt)))
  570. (content (list :value in))
  571. (msg (ein:kernel--get-msg kernel "input_reply" content)))
  572. (ein:websocket-send-stdin-channel kernel msg)
  573. (setf (ein:$kernel-stdin-activep kernel) nil))))))))))
  574. (defun ein:kernel--handle-shell-reply (kernel packet)
  575. (cl-destructuring-bind
  576. (&key header content metadata parent_header &allow-other-keys)
  577. (ein:json-read-from-string packet)
  578. (let* ((msg-type (plist-get header :msg_type))
  579. (msg-id (plist-get parent_header :msg_id))
  580. (callbacks (ein:kernel-get-callbacks-for-msg kernel msg-id))
  581. (cb (plist-get callbacks (intern (format ":%s" msg-type)))))
  582. (ein:log 'debug "KERNEL--HANDLE-SHELL-REPLY: msg_type=%s msg_id=%s"
  583. msg-type msg-id)
  584. (run-hook-with-args 'ein:on-shell-reply-functions msg-type header content metadata)
  585. (ein:aif cb (ein:funcall-packed it content metadata))
  586. (ein:aif (plist-get content :payload)
  587. (ein:kernel--handle-payload kernel callbacks it))
  588. (let ((events (ein:$kernel-events kernel)))
  589. (ein:case-equal msg-type
  590. (("execute_reply")
  591. (ein:aif (plist-get content :execution_count)
  592. ;; It can be `nil' for silent execution
  593. (ein:events-trigger events 'execution_count.Kernel it))))))))
  594. (defun ein:kernel--handle-payload (kernel callbacks payload)
  595. (cl-loop with events = (ein:$kernel-events kernel)
  596. for p in payload
  597. for text = (or (plist-get p :text)
  598. (plist-get (plist-get p :data)
  599. :text/plain))
  600. for source = (plist-get p :source)
  601. if (member source '("IPython.kernel.zmq.page.page"
  602. "IPython.zmq.page.page"
  603. "page"))
  604. do (when (not (equal (ein:trim text) ""))
  605. (ein:events-trigger
  606. events 'open_with_text.Pager (list :text text)))
  607. else if
  608. (member
  609. source
  610. '("IPython.kernel.zmq.zmqshell.ZMQInteractiveShell.set_next_input"
  611. "IPython.zmq.zmqshell.ZMQInteractiveShell.set_next_input"
  612. "set_next_input"))
  613. do (let ((cb (plist-get callbacks :set_next_input)))
  614. (when cb (ein:funcall-packed cb text)))))
  615. (defun ein:kernel--handle-iopub-reply (kernel packet)
  616. (if (ein:$kernel-stdin-activep kernel)
  617. (ein:ipdb--handle-iopub-reply kernel packet)
  618. (cl-destructuring-bind
  619. (&key content metadata parent_header header &allow-other-keys)
  620. (ein:json-read-from-string packet)
  621. (let* ((msg-type (plist-get header :msg_type))
  622. (msg-id (plist-get parent_header :msg_id))
  623. (callbacks (ein:kernel-get-callbacks-for-msg kernel msg-id))
  624. (events (ein:$kernel-events kernel)))
  625. (ein:log 'debug "KERNEL--HANDLE-IOPUB-REPLY: msg_type=%s msg_id=%s"
  626. msg-type msg-id)
  627. (if (and (not (equal msg-type "status")) (null callbacks))
  628. (ein:log 'verbose "Not processing msg_type=%s msg_id=%s" msg-type msg-id)
  629. (ein:case-equal msg-type
  630. (("stream" "display_data" "pyout" "pyerr" "error" "execute_result")
  631. (ein:aif (plist-get callbacks :output)
  632. (ein:funcall-packed it msg-type content metadata)))
  633. (("status")
  634. (ein:case-equal (plist-get content :execution_state)
  635. (("busy")
  636. (ein:events-trigger events 'status_busy.Kernel))
  637. (("idle")
  638. (ein:events-trigger events 'status_idle.Kernel))
  639. (("dead")
  640. (ein:kernel-disconnect kernel))))
  641. (("data_pub")
  642. (ein:log 'verbose (format "Received data_pub message w/content %s" packet)))
  643. (("clear_output")
  644. (ein:aif (plist-get callbacks :clear_output)
  645. (ein:funcall-packed it content metadata)))))))))
  646. ;;; Utility functions
  647. (defun ein:kernel-filename-to-python (kernel filename)
  648. "See: `ein:filename-to-python'."
  649. (ein:filename-to-python (ein:$kernel-url-or-port kernel) filename))
  650. (defun ein:kernel-filename-from-python (kernel filename)
  651. "See: `ein:filename-from-python'."
  652. (ein:filename-from-python (ein:$kernel-url-or-port kernel) filename))
  653. (defun ein:kernel-construct-defstring (content)
  654. "Construct call signature from CONTENT of ``:object_info_reply``.
  655. Used in `ein:pytools-finish-tooltip', etc."
  656. (or (plist-get content :call_def)
  657. (plist-get content :init_definition)
  658. (plist-get content :definition)))
  659. (defun ein:kernel-construct-help-string (content)
  660. "Construct help string from CONTENT of ``:object_info_reply``.
  661. Used in `ein:pytools-finish-tooltip', etc."
  662. (ein:log 'debug "KERNEL-CONSTRUCT-HELP-STRING")
  663. (let* ((defstring (ein:aand
  664. (ein:kernel-construct-defstring content)
  665. (ansi-color-apply it)
  666. (ein:string-fill-paragraph it)))
  667. (docstring (ein:aand
  668. (or (plist-get content :call_docstring)
  669. (plist-get content :init_docstring)
  670. (plist-get content :docstring)
  671. ;; "<empty docstring>"
  672. )
  673. (ansi-color-apply it)))
  674. (help (ein:aand
  675. (delete nil (list defstring docstring))
  676. (ein:join-str "\n" it))))
  677. (ein:log 'debug "KERNEL-CONSTRUCT-HELP-STRING: help=%s" help)
  678. help))
  679. (defun ein:kernel-request-stream (kernel code func &optional args)
  680. "Run lisp callback FUNC with the output stream returned by Python CODE.
  681. The first argument to the lisp function FUNC is the stream output
  682. as a string and the rest of the argument is the optional ARGS."
  683. (ein:kernel-execute
  684. kernel
  685. code
  686. (list :output (cons (lambda (packed msg-type content _metadata)
  687. (let ((func (car packed))
  688. (args (cdr packed)))
  689. (when (equal msg-type "stream")
  690. (ein:aif (plist-get content :text)
  691. (apply func it args)))))
  692. (cons func args)))))
  693. (cl-defun ein:kernel-history-request-synchronously
  694. (kernel &rest args &key (timeout 0.5) (tick-time 0.05) &allow-other-keys)
  695. "Send the history request and wait TIMEOUT seconds.
  696. Return a list (CONTENT METADATA).
  697. This function checks the request reply every TICK-TIME seconds.
  698. See `ein:kernel-history-request' for other usable options."
  699. ;; As `result' and `finished' are set in callback, make sure they
  700. ;; won't be trapped in other let-bindings.
  701. (let (result finished)
  702. (apply
  703. #'ein:kernel-history-request
  704. kernel
  705. (list :history_reply
  706. (cons (lambda (_ignore content metadata)
  707. (setq result (list content metadata))
  708. (setq finished t))
  709. nil))
  710. args)
  711. (cl-loop repeat (floor (/ timeout tick-time))
  712. do (sit-for tick-time)
  713. when finished
  714. return t
  715. finally (error "Timeout"))
  716. result))
  717. (defun ein:kernel-history-search-synchronously (kernel pattern &rest args)
  718. "Search execution history in KERNEL using PATTERN.
  719. Return matched history as a list of strings.
  720. See `ein:kernel-history-request-synchronously' and
  721. `ein:kernel-history-request' for usable options."
  722. (let ((reply
  723. (apply #'ein:kernel-history-request-synchronously
  724. kernel
  725. :hist-access-type "search"
  726. :pattern pattern
  727. args)))
  728. (mapcar #'caddr (plist-get (car reply) :history))))
  729. (provide 'ein-kernel)
  730. ;;; ein-kernel.el ends here