2017-02-15 14:18:16 -06:00
;;; ein-jupyter.el --- Manage the jupyter notebook server
2017-02-15 15:06:38 -06:00
;; Copyright (C) 2017 John M. Miller
2017-02-15 14:18:16 -06:00
;; Authors: John M. Miller <millejoh at mac.com>
;; This file is NOT part of GNU Emacs.
;; ein-jupyter.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-jupyter.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-jupyter.el. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
2018-10-15 14:13:51 -04:00
( require 'ein-core )
( require 'ein-notebooklist )
2018-10-15 16:57:22 -04:00
( require 'ein-dev )
2018-10-15 14:13:51 -04:00
2017-02-15 14:18:16 -06:00
( defcustom ein:jupyter-server-buffer-name " *ein:jupyter-server* "
2017-02-15 15:02:07 -06:00
" The name of the buffer to run a jupyter notebook server
session in. "
:group 'ein
:type 'string )
2017-02-15 14:18:16 -06:00
2017-10-03 10:04:41 -05:00
( defcustom ein:jupyter-server-run-timeout 60000
2017-09-03 10:29:54 -05:00
" Time, in milliseconds, to wait for the jupyter server to start before declaring timeout and cancelling the operation. "
:group 'ein
:type 'integer )
2019-01-22 08:15:36 -05:00
( defcustom ein:jupyter-server-args ' ( " --no-browser " )
2017-02-20 18:07:17 -06:00
" Add any additional command line options you wish to include
with the call to the jupyter notebook. "
:group 'ein
:type ' ( repeat string ) )
2017-02-16 08:11:58 -06:00
( defcustom ein:jupyter-default-notebook-directory nil
" If you are tired of always being queried for the location of
the notebook directory, you can set it here for future calls to
` ein:jupyter-server-start ' "
:group 'ein
:type ' ( directory ) )
2017-02-15 14:18:16 -06:00
( defvar *ein:jupyter-server-accept-timeout* 60 )
2018-10-15 16:57:22 -04:00
( defvar *ein:jupyter-server-process-name* " EIN: Jupyter notebook server " )
2017-02-16 08:11:58 -06:00
2017-02-15 14:18:16 -06:00
( defvar *ein:last-jupyter-command* nil )
( defvar *ein:last-jupyter-directory* nil )
2018-10-15 16:57:22 -04:00
( defsubst ein:jupyter-server-process ( )
" Return the emacs process object of our session "
( get-buffer-process ( get-buffer ein:jupyter-server-buffer-name ) ) )
2017-07-12 14:38:04 -05:00
( defun ein:jupyter-server--run ( buf cmd dir &optional args )
2019-03-21 14:37:19 -04:00
( when ein:debug
( add-to-list 'ein:jupyter-server-args " --debug " ) )
2018-12-01 18:54:58 -05:00
( let* ( ( vargs ( append ( if dir
2019-02-14 15:28:18 -05:00
` ( " notebook "
, ( format " --notebook-dir=%s "
( convert-standard-filename dir ) ) ) )
args
ein:jupyter-server-args ) )
2018-12-01 18:54:58 -05:00
( proc ( apply #' start-process
2019-02-14 15:28:18 -05:00
*ein:jupyter-server-process-name* buf cmd vargs ) ) )
( ein:log 'info " ein:jupyter-server--run: %s %s " cmd ( ein:join-str " " vargs ) )
2018-09-26 10:07:50 -04:00
( set-process-query-on-exit-flag proc nil )
2017-07-12 14:38:04 -05:00
proc ) )
2019-02-14 15:28:18 -05:00
( defun ein:jupyter-server-conn-info ( &optional buffer-name )
2018-10-15 16:57:22 -04:00
" Return the url-or-port and password for BUFFER or the global session. "
2019-02-14 15:28:18 -05:00
( unless buffer-name
( setq buffer-name ein:jupyter-server-buffer-name ) )
( let ( ( buffer ( get-buffer buffer-name ) )
( result ' ( nil nil ) ) )
2018-10-15 16:57:22 -04:00
( if buffer
( with-current-buffer buffer
( save-excursion
( goto-char ( point-max ) )
2018-10-20 11:40:36 -04:00
( re-search-backward ( format " Process %s " *ein:jupyter-server-process-name* )
2018-10-15 16:57:22 -04:00
nil " " ) ;; important if we start-stop-start
2018-12-01 18:54:58 -05:00
( when ( re-search-forward " \\ ([[:alnum:]]+ \\ ) is \\ ( now \\ )? running " nil t )
( let ( ( hub-p ( search " jupyterhub " ( downcase ( match-string 1 ) ) ) ) )
( when ( re-search-forward " \\ (https?://[^:]*:[0-9]+ \\ ) \\ (?:/ \\ ?token= \\ ([[:alnum:]]+ \\ ) \\ )? " nil t )
( let ( ( raw-url ( match-string 1 ) )
( token ( or ( match-string 2 ) ( and ( not hub-p ) " " ) ) ) )
( setq result ( list ( ein:url raw-url ) token ) ) ) ) ) ) ) ) )
2018-10-15 16:57:22 -04:00
result ) )
2017-07-12 14:38:04 -05:00
2018-10-15 16:57:22 -04:00
( defun ein:jupyter-server-login-and-open ( &optional callback )
2017-07-12 14:38:04 -05:00
" Log in and open a notebooklist buffer for a running jupyter notebook server.
Determine if there is a running jupyter server ( started via a
call to ` ein:jupyter-server-start ' ) and then try to guess if
token authentication is enabled. If a token is found use it to generate a
call to ` ein:notebooklist-login ' and once authenticated open the notebooklist buffer
via a call to ` ein:notebooklist-open '. "
( interactive )
2018-10-15 16:57:22 -04:00
( when ( ein:jupyter-server-process )
2018-10-11 16:53:02 -04:00
( multiple-value-bind ( url-or-port password ) ( ein:jupyter-server-conn-info )
2018-10-15 16:57:22 -04:00
( ein:notebooklist-login url-or-port callback ) ) ) )
2017-07-12 14:38:04 -05:00
2018-10-24 13:12:16 -04:00
( defsubst ein:set-process-sentinel ( proc url-or-port )
2018-12-01 18:54:58 -05:00
" URL-OR-PORT might get redirected from (ein:jupyter-server-conn-info).
This is currently only the case for jupyterhub.
Once login handshake provides the new URL-OR-PORT, we set various state as pertains
our singleton jupyter server process here. "
;; Would have used `add-function' if it didn't produce gv-ref warnings.
2018-10-24 13:12:16 -04:00
( set-process-sentinel
proc
2018-12-01 18:54:58 -05:00
( apply-partially ( lambda ( url-or-port* sentinel proc* event )
( ein:aif sentinel ( funcall it proc* event ) )
( funcall #' ein:notebooklist-sentinel url-or-port* proc* event ) )
2018-10-24 13:12:16 -04:00
url-or-port ( process-sentinel proc ) ) ) )
2017-07-12 14:38:04 -05:00
;;;###autoload
2019-02-14 15:28:18 -05:00
( defun ein:jupyter-server-start ( server-cmd-path notebook-directory
&optional no-login-p login-callback port )
2018-12-01 18:54:58 -05:00
" Start SERVER-CMD_PATH with `--notebook-dir' NOTEBOOK-DIRECTORY. Login after connection established unless NO-LOGIN-P is set. LOGIN-CALLBACK takes two arguments, the buffer created by ein:notebooklist-open--finish, and the url-or-port argument of ein:notebooklist-open*.
2017-02-15 15:02:07 -06:00
This command opens an asynchronous process running the jupyter
2018-10-11 16:53:02 -04:00
notebook server and then tries to detect the url and password to
2017-02-15 15:02:07 -06:00
generate automatic calls to ` ein:notebooklist-login ' and
` ein:notebooklist-open '.
2017-11-16 15:31:41 +08:00
With \\ [ universal-argument ] prefix arg, it will prompt the user for the path to
the jupyter executable first. Else, it will try to use the
value of ` *ein:last-jupyter-command* ' or the value of the
customizable variable ` ein:jupyter-default-server-command '.
Then it prompts the user for the path of the root directory
containing the notebooks the user wants to access.
2017-02-15 15:02:07 -06:00
The buffer named by ` ein:jupyter-server-buffer-name ' will contain
the log of the running jupyter server. "
2017-11-16 15:31:41 +08:00
( interactive
( let* ( ( default-command ( or *ein:last-jupyter-command*
ein:jupyter-default-server-command ) )
( server-cmd-path
2017-11-28 07:16:20 -06:00
( executable-find ( if current-prefix-arg
2018-10-15 16:57:22 -04:00
( read-file-name " Server command: " default-directory nil nil
default-command )
default-command ) ) )
( notebook-directory
( read-directory-name " Notebook directory: "
2017-11-16 15:31:41 +08:00
( or *ein:last-jupyter-directory*
ein:jupyter-default-notebook-directory ) ) ) )
2018-12-01 18:54:58 -05:00
( list server-cmd-path notebook-directory nil ( lambda ( buffer url-or-port )
( pop-to-buffer buffer ) ) ) ) )
2019-02-26 18:04:50 -05:00
( unless ( and ( stringp server-cmd-path )
( file-exists-p server-cmd-path )
2017-06-06 15:25:44 -05:00
( file-executable-p server-cmd-path ) )
2019-02-26 18:04:50 -05:00
( error " Command %s not found or not executable "
( or *ein:last-jupyter-command*
ein:jupyter-default-server-command ) ) )
2017-06-06 15:25:44 -05:00
( setf *ein:last-jupyter-command* server-cmd-path
*ein:last-jupyter-directory* notebook-directory )
2018-10-15 16:57:22 -04:00
( if ( ein:jupyter-server-process )
2019-02-14 15:28:18 -05:00
( error " Please first M-x ein:stop " ) )
2017-09-12 16:22:19 -05:00
( add-hook 'kill-emacs-hook #' ( lambda ( )
2018-10-14 21:24:50 -04:00
( ignore-errors ( ein:jupyter-server-stop t ) ) ) )
2018-12-01 18:54:58 -05:00
( let ( ( proc ( ein:jupyter-server--run ein:jupyter-server-buffer-name
*ein:last-jupyter-command*
2019-02-14 15:28:18 -05:00
*ein:last-jupyter-directory*
( if ( numberp port )
2019-05-16 15:01:45 -04:00
` ( " --port " , ( format " %s " port )
" --port-retries " " 0 " ) ) ) ) )
2017-10-23 19:40:06 -05:00
( when ( eql system-type 'windows-nt )
2017-11-04 11:27:31 -05:00
( accept-process-output proc ( / ein:jupyter-server-run-timeout 1000 ) ) )
2018-12-01 18:54:58 -05:00
( loop repeat 30
until ( car ( ein:jupyter-server-conn-info ein:jupyter-server-buffer-name ) )
do ( sleep-for 0 500 )
finally do
( unless ( car ( ein:jupyter-server-conn-info ein:jupyter-server-buffer-name ) )
( ein:log 'warn " Jupyter server failed to start, cancelling operation " )
( ein:jupyter-server-stop t ) ) )
( when ( and ( not no-login-p ) ( ein:jupyter-server-process ) )
( unless login-callback
( setq login-callback #' ignore ) )
( add-function :after login-callback
( apply-partially ( lambda ( proc* buffer url-or-port )
( ein:set-process-sentinel proc* url-or-port ) )
proc ) )
( ein:jupyter-server-login-and-open login-callback ) ) ) )
2017-02-15 14:18:16 -06:00
2018-11-09 13:02:43 -05:00
;;;###autoload
( defalias 'ein:run 'ein:jupyter-server-start )
;;;###autoload
( defalias 'ein:stop 'ein:jupyter-server-stop )
2017-02-15 15:06:38 -06:00
;;;###autoload
2018-09-26 10:07:50 -04:00
( defun ein:jupyter-server-stop ( &optional force log )
2017-02-15 14:18:16 -06:00
( interactive )
2019-02-14 15:28:18 -05:00
( ein:and-let* ( ( proc ( ein:jupyter-server-process ) )
( _ok ( or force ( y-or-n-p " Stop server and close notebooks? " ) ) ) )
2019-05-03 09:23:29 -04:00
( ein:notebook-close-notebooks t )
2018-12-01 18:54:58 -05:00
( loop repeat 10
2019-05-03 09:23:29 -04:00
do ( ein:query-running-process-table )
until ( zerop ( hash-table-count ein:query-running-process-table ) )
2018-12-01 18:54:58 -05:00
do ( sleep-for 0 500 ) )
2018-10-15 16:57:22 -04:00
;; Both (quit-process) and (delete-process) leaked child kernels, so signal
2019-02-14 15:28:18 -05:00
( if ( eql system-type 'windows-nt )
( delete-process proc )
2019-04-29 12:09:27 -04:00
( lexical-let* ( ( proc proc )
( pid ( process-id proc ) ) )
2019-02-14 15:28:18 -05:00
( ein:log 'verbose " Signaled %s with pid %s " proc pid )
2019-04-29 12:09:27 -04:00
( signal-process pid 15 )
( run-at-time 2 nil ( lambda ( )
( ein:log 'verbose " Resignaled %s with pid %s " proc pid )
( signal-process pid 15 ) ) ) ) )
2019-02-14 15:28:18 -05:00
( ein:log 'info " Stopped Jupyter notebook server. " )
;; `ein:notebooklist-sentinel' frequently does not trigger
( multiple-value-bind ( url-or-port _password ) ( ein:jupyter-server-conn-info )
( ein:notebooklist-list-remove url-or-port ) )
2018-09-26 10:07:50 -04:00
( when log
( with-current-buffer ein:jupyter-server-buffer-name
2018-10-15 16:57:22 -04:00
( write-region ( point-min ) ( point-max ) log ) ) ) ) )
2017-02-15 14:18:16 -06:00
2017-02-15 15:06:38 -06:00
( provide 'ein-jupyter )