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 )
2017-02-20 18:07:17 -06:00
( defcustom ein:jupyter-server-args nil
" 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-02-15 14:18:16 -06:00
( defun ein:jupyter-server--cmd ( path dir )
2017-02-20 18:07:17 -06:00
( append ( list path
" notebook "
2017-04-07 08:18:41 -05:00
( format " --notebook-dir=%s " ( convert-standard-filename dir ) ) )
2017-02-20 18:07:17 -06:00
ein:jupyter-server-args ) )
2017-07-12 14:38:04 -05:00
( defun ein:jupyter-server--run ( buf cmd dir &optional args )
( let ( ( proc ( apply #' start-process
2018-10-15 16:57:22 -04:00
*ein:jupyter-server-process-name*
2017-07-12 14:38:04 -05:00
buf
cmd
" notebook "
( format " --notebook-dir=%s " ( convert-standard-filename dir ) )
( or args ein:jupyter-server-args ) ) ) )
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 ) )
2018-10-15 16:57:22 -04:00
( defun ein:jupyter-server-conn-info ( &optional buffer )
" Return the url-or-port and password for BUFFER or the global session. "
( unless buffer
( setq buffer ( get-buffer ein:jupyter-server-buffer-name ) ) )
( let ( ( result ' ( nil nil ) ) )
( if buffer
( with-current-buffer buffer
( save-excursion
( goto-char ( point-max ) )
( re-search-backward ( format " %s finished " *ein:jupyter-server-process-name* )
nil " " ) ;; important if we start-stop-start
( if ( and ( re-search-forward " otebook [iI]s [rR]unning " nil t )
( re-search-forward " \\ (https?://[^:]+:[0-9]+ \\ ) \\ (?:/ \\ ?token= \\ ([[:alnum:]]+ \\ ) \\ )? " nil t ) )
( let ( ( raw-url ( match-string 1 ) )
( token ( match-string 2 ) ) )
( setq result ( list ( ein:url raw-url ) token ) ) ) ) ) ) )
result ) )
2017-07-12 14:38:04 -05:00
2017-02-15 15:06:38 -06:00
;;;###autoload
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
;;;###autoload
2018-10-15 16:57:22 -04:00
( defun ein:jupyter-server-start ( server-cmd-path notebook-directory &optional no-login-p login-callback )
" Start SERVER-CMD_PATH with `--notebook-dir' NOTEBOOK-DIRECTORY. Login after connection established unless NO-LOGIN-P is set. LOGIN-CALLBACK taking single argument, the buffer created by ein:notebooklist-open--finish.
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-10-15 16:57:22 -04:00
( list server-cmd-path notebook-directory nil #' pop-to-buffer ) ) )
2017-06-06 15:25:44 -05:00
( assert ( and ( file-exists-p server-cmd-path )
( file-executable-p server-cmd-path ) )
t " Command %s is not valid! " server-cmd-path )
( 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 )
( error " Please first M-x ein:jupyter-server-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-10-15 16:57:22 -04:00
( lexical-let* ( done-p
( no-login-p no-login-p )
( login-callback login-callback )
( proc ( ein:jupyter-server--run ein:jupyter-server-buffer-name
*ein:last-jupyter-command*
*ein:last-jupyter-directory* ) )
( buf ( process-buffer proc ) ) )
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-10-15 16:57:22 -04:00
( if ein:dev-prefer-deferred
( deferred:$
( deferred:timeout
ein:jupyter-server-run-timeout 'timeout
( deferred:lambda ( )
( if ( car ( ein:jupyter-server-conn-info ) )
no-login-p
( deferred:nextc ( deferred:wait ( / ein:jupyter-server-run-timeout 5 ) ) self ) ) ) )
( deferred:nextc it
( lambda ( no-login-p )
( if ( eq no-login-p 'timeout )
( progn
( setf done-p 'error )
( ein:log 'warn " Jupyter server failed to start, cancelling operation. " )
( ein:jupyter-server-stop t ) )
( setf done-p t )
( unless no-login-p
( ein:jupyter-server-login-and-open login-callback ) ) ) ) ) )
( loop repeat 30
until ( car ( ein:jupyter-server-conn-info buf ) )
do ( sleep-for 0 500 )
finally do
( if ( car ( ein:jupyter-server-conn-info buf ) )
( setf done-p t )
( setf done-p " error " )
( ein:log 'warn " Jupyter server failed to start, cancelling operation " )
( ein:jupyter-server-stop t ) ) )
( unless no-login-p
( ein:jupyter-server-login-and-open login-callback ) ) ) ) )
2017-02-15 14:18:16 -06:00
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 15:02:07 -06:00
" Stop a running jupyter notebook server.
Use this command to stop a running jupyter notebook server. If
there is no running server then no action will be taken.
"
2017-02-15 14:18:16 -06:00
( interactive )
2018-10-15 16:57:22 -04:00
( when ( and ( ein:jupyter-server-process )
2017-09-03 10:29:54 -05:00
( or force ( y-or-n-p " Kill jupyter server and close all open notebooks? " ) ) )
2017-02-15 15:02:07 -06:00
( let ( ( unsaved ( ein:notebook-opened-notebooks #' ein:notebook-modified-p ) )
( check-for-saved ( make-hash-table :test #' equal ) ) )
( when unsaved
( loop for nb in unsaved
when ( y-or-n-p ( format " Save notebook %s before stopping the server? " ( ein:$notebook-notebook-name nb ) ) )
do ( progn
( setf ( gethash ( ein:$notebook-notebook-name nb ) check-for-saved ) t )
( ein:notebook-save-notebook nb 0
#' ( lambda ( name check-hash )
( remhash name check-hash ) )
( list ( ein:$notebook-notebook-name nb ) check-for-saved ) ) ) ) )
( loop for x upfrom 0 by 1
until ( or ( = ( hash-table-count check-for-saved ) 0 )
( > x 1000000 ) )
do ( sit-for 0.1 ) ) )
2018-09-26 10:25:48 -04:00
2017-02-15 15:02:07 -06:00
( mapc #' ein:notebook-close ( ein:notebook-opened-notebooks ) )
2018-09-26 10:25:48 -04:00
2018-10-15 16:57:22 -04:00
;; Both (quit-process) and (delete-process) leaked child kernels, so signal
( ein:aif ( ein:jupyter-server-process )
( let ( ( pid ( process-id it ) ) )
( ein:log 'verbose " Signaled %s with pid %s " it pid )
( ein:log 'info " Stopped Jupyter notebook server. " )
( signal-process ( process-id it ) 15 ) ) )
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-09-03 10:29:54 -05:00
( defun ein:jupyter-server-list--cmd ( &optional args )
( append ( list " notebook "
" list " )
args ) )
( defun ein:jupyter-query-running-notebooks ( )
( with-temp-buffer
( let ( ( res ( apply #' call-process ( or *ein:last-jupyter-command*
ein:jupyter-default-server-command )
nil
t
nil
( ein:jupyter-server-list--cmd ) ) )
( contents ( rest ( s-lines ( buffer-string ) ) ) ) )
contents ) ) )
2017-02-15 15:06:38 -06:00
( provide 'ein-jupyter )