;;; ein-kernel.el --- Communicate with IPython notebook server ;; Copyright (C) 2012- Takafumi Arakaki ;; Author: Takafumi Arakaki ;; This file is NOT part of GNU Emacs. ;; ein-kernel.el 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 3 of the License, or ;; (at your option) any later version. ;; ein-kernel.el 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 ein-kernel.el. If not, see . ;;; Commentary: ;; ;;; Code: (eval-when-compile (require 'cl)) (require 'ein-log) (require 'ein-utils) (require 'ein-websocket) (require 'url) (defstruct ein:$kernel kernel-id shell-channel iopub-channel base-url kernel-url ws-url running username session-id) (defvar ein:base-kernel-url "/") ;; Currently there is no way to know this setting. Maybe I should ask ;; IPython developers for an API to get this from notebook server. (defun ein:kernel-init () (make-ein:$kernel :kernel-id nil :shell-channel nil :iopub-channel nil :base-url (concat ein:base-kernel-url "kernels") :running nil :username "username" :session-id (ein:utils-uuid))) (defun ein:kernel-get-msg (kernel msg-type content) (list :header (list :msg_id (ein:utils-uuid) :username (ein:$kernel-username kernel) :session (ein:$kernel-session-id kernel) :msg_type msg-type) :content content :parent_header (make-hash-table))) (defun ein:kernel-start (kernel notebook-id callback &optional cbargs) "Start kernel of the notebook whose id is NOTEBOOK-ID. CALLBACK is called after kernel is started with optional argument CBARGS." (unless (ein:$kernel-running kernel) (let* ((qs (format "notebook=%s" notebook-id)) (url (concat (url-expand-file-name (ein:$kernel-base-url kernel) (ein:base-project-url)) "?" qs)) (url-request-method "POST")) (url-retrieve url (lambda (status kernel callback cbargs) (ein:kernel--handle-start-kernel kernel (ein:json-read) callback cbargs) (kill-buffer (current-buffer))) (list kernel callback cbargs))))) (defun ein:kernel-restart (kernel callback &optional cbargs) (ein:log 'info "Restarting kernel") (when (ein:$kernel-running kernel) (ein:kernel-stop-channels kernel) (let ((url (concat (ein:$kernel-kernel-url kernel) "/restart"))) (url-retrieve url (lambda (status kernel callback cbargs) (ein:kernel--handle-start-kernel kernel (ein:json-read) callback cbargs) (kill-buffer (current-buffer))) (list kernel callback cbargs))))) (defun ein:kernel--handle-start-kernel (kernel json callback &optional cbargs) (setf (ein:$kernel-running kernel) t) (setf (ein:$kernel-kernel-id kernel) (plist-get json :kernel_id)) (setf (ein:$kernel-ws-url kernel) (plist-get json :ws_url)) (setf (ein:$kernel-kernel-url kernel) (concat (ein:$kernel-base-url kernel) "/" (ein:$kernel-kernel-id kernel))) (ein:kernel-start-channels kernel) (apply callback cbargs)) (defun ein:kernel--websocket-closed (kernel ws-url early) ;; FIXME: `message' is not good choice for showing multiple line ;; message. I should implement ein-pager.el first and show the ;; message using its function. (if early (ein:log 'warn "Websocket connection to %s could not be established. You will NOT be able to run code. Your websocket.el may not be compatible with the websocket version in the server, or if the url does not look right, there could be an error in the server's configuration." ws-url) (ein:log 'warn "Websocket connection closed unexpectedly. The kernel will no longer be responsive."))) (defun ein:kernel-send-cookie (channel) ;; This is required to open channel. In IPython's kernel.js, it sends ;; `document.cookie'. This is an empty string anyway. (ein:websocket-send channel "")) (defun ein:kernel--ws-closed-callback (websocket kernel arg) ;; NOTE: The argument ARG should not be "unpacked" using `&rest'. ;; It must be given as a list to hold state `already-called-onclose' ;; so it can be modified in this function. (destructuring-bind (&key already-called-onclose ws-url early) arg (unless already-called-onclose (plist-put arg :already-called-onclose t) (unless (ein:$websocket-closed-by-client websocket) ;; Use "event-was-clean" when it is implemented in websocket.el. (ein:kernel--websocket-closed kernel ws-url early))))) (defun ein:kernel-start-channels (kernel) (ein:kernel-stop-channels kernel) (let* ((ws-url (concat (ein:$kernel-ws-url kernel) (ein:$kernel-kernel-url kernel))) (onclose-arg (list :ws-url ws-url :already-called-onclose nil :early t))) (ein:log 'info "Starting WS: %S" ws-url) (setf (ein:$kernel-shell-channel kernel) (ein:websocket (concat ws-url "/shell"))) (setf (ein:$kernel-iopub-channel kernel) (ein:websocket (concat ws-url "/iopub"))) (loop for c in (list (ein:$kernel-shell-channel kernel) (ein:$kernel-iopub-channel kernel)) do (setf (ein:$websocket-onclose-args c) (list kernel onclose-arg)) do (setf (ein:$websocket-onopen c) (lexical-let ((channel c)) (lambda () (ein:kernel-send-cookie channel)))) do (setf (ein:$websocket-onclose c) #'ein:kernel--ws-closed-callback)) ;; switch from early-close to late-close message after 1s (run-at-time 1 nil (lambda (onclose-arg) (plist-put onclose-arg :early nil) (ein:log 'debug "(via run-at-time) onclose-arg changed to: %S" onclose-arg)) onclose-arg))) ;; NOTE: `onclose-arg' can be accessed as: ;; (nth 1 (ein:$websocket-onclose-args (ein:$kernel-shell-channel (ein:$notebook-kernel ein:notebook)))) (defun ein:kernel-stop-channels (kernel) (when (ein:$kernel-shell-channel kernel) (setf (ein:$websocket-onclose (ein:$kernel-shell-channel kernel)) nil) (ein:websocket-close (ein:$kernel-shell-channel kernel)) (setf (ein:$kernel-shell-channel kernel) nil)) (when (ein:$kernel-iopub-channel kernel) (setf (ein:$websocket-onclose (ein:$kernel-iopub-channel kernel)) nil) (ein:websocket-close (ein:$kernel-iopub-channel kernel)) (setf (ein:$kernel-iopub-channel kernel) nil))) (defun ein:kernel-object-info-request (kernel objname) (when objname (let* ((content (list :oname (format "%S" objname))) (msg (ein:kernel-get-msg kernel "object_info_request" content))) (ein:websocket-send (ein:$kernel-shell-channel kernel) (json-encode msg)) (plist-get (plist-get msg :header) :msg_id)))) (defun ein:kernel-execute (kernel code) (let* ((content (list :code code :silent json-false :user_variables [] :user_expressions (make-hash-table) :allow_stdin json-false)) (msg (ein:kernel-get-msg kernel "execute_request" content))) (ein:websocket-send (ein:$kernel-shell-channel kernel) (json-encode msg)) (plist-get (plist-get msg :header) :msg_id))) (defun ein:kernel-complete (kernel line cursor-pos) (let* ((content (list :text "" :line line :cursor_pos cursor-pos)) (msg (ein:kernel-get-msg kernel "execute_request" content))) (ein:websocket-send (ein:$kernel-shell-channel kernel) (json-encode msg)) (plist-get (plist-get msg :header) :msg_id))) (defun ein:kernel-interrupt (kernel) (when (ein:$kernel-running kernel) (ein:log 'info "Interrupting kernel") (url-retrieve (concat (ein:$kernel-kernel-url kernel) "/interrupt") (lambda (s) (ein:log 'info "Sent interruption command.") (kill-buffer (current-buffer)))))) (defun ein:kernel-kill (kernel) (when (ein:$kernel-running kernel) (setf (ein:$kernel-running kernel) nil) (let ((url-request-method "DELETE") ;; cache = false (http://api.jquery.com/jQuery.ajax/) (url (concat (ein:$kernel-kernel-url kernel) (format-time-string "?_=%s")))) (url-retrieve url (lambda (s) (ein:log 'info "Notebook kernel is killed") (kill-buffer (current-buffer))))))) (provide 'ein-kernel) ;;; ein-kernel.el ends here