2018-10-15 16:57:22 -04:00
|
|
|
;;; ein-process.el --- Notebook list buffer
|
|
|
|
|
|
|
|
;; Copyright (C) 2018- John M. Miller
|
|
|
|
|
|
|
|
;; Authors: Takafumi Arakaki <aka.tkf at gmail.com>
|
|
|
|
;; John M. Miller <millejoh at mac.com>
|
|
|
|
|
|
|
|
;; This file is NOT part of GNU Emacs.
|
|
|
|
|
|
|
|
;; ein-process.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-process.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-process.el. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
|
|
(eval-when-compile (require 'cl))
|
|
|
|
|
|
|
|
(require 'ein-core)
|
|
|
|
(require 'ein-jupyter)
|
|
|
|
(require 'ein-file)
|
|
|
|
(require 'ein-notebooklist)
|
|
|
|
(require 'f)
|
|
|
|
|
|
|
|
(defcustom ein:process-jupyter-regexp "\\(jupyter\\|ipython\\)\\(-\\|\\s-+\\)note"
|
|
|
|
"Regexp by which we recognize notebook servers."
|
|
|
|
:type 'string
|
|
|
|
:group 'ein)
|
|
|
|
|
|
|
|
|
|
|
|
(defcustom ein:process-lsof "lsof"
|
|
|
|
"Executable for lsof command."
|
|
|
|
:type 'string
|
|
|
|
:group 'ein)
|
|
|
|
|
|
|
|
(defun ein:process-divine-dir (pid args &optional error-buffer)
|
|
|
|
"Returns notebook-dir or cwd of PID. Supply ERROR-BUFFER to capture stderr"
|
|
|
|
(if (string-match "\\bnotebook-dir\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
|
|
|
(directory-file-name (match-string 2 args))
|
|
|
|
(if (executable-find ein:process-lsof)
|
|
|
|
(ein:trim-right
|
|
|
|
(with-output-to-string
|
|
|
|
(shell-command (format "%s -p %d -a -d cwd -Fn | grep ^n | tail -c +2"
|
|
|
|
ein:process-lsof pid)
|
|
|
|
standard-output error-buffer))))))
|
|
|
|
|
|
|
|
(defun ein:process-divine-port (pid args &optional error-buffer)
|
|
|
|
"Returns port on which PID is listening or 0 if none. Supply ERROR-BUFFER to capture stderr"
|
|
|
|
(if (string-match "\\bport\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
|
|
|
(string-to-number (match-string 2 args))
|
|
|
|
(if (executable-find ein:process-lsof)
|
|
|
|
(string-to-number
|
|
|
|
(ein:trim-right
|
|
|
|
(with-output-to-string
|
|
|
|
(shell-command (format "%s -p %d -a -iTCP -sTCP:LISTEN -Fn | grep ^n | sed \"s/[^0-9]//g\""
|
|
|
|
ein:process-lsof pid)
|
|
|
|
standard-output error-buffer)))))))
|
|
|
|
|
|
|
|
(defun ein:process-divine-ip (pid args)
|
|
|
|
"Returns notebook-ip of PID"
|
|
|
|
(if (string-match "\\bip\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
|
|
|
(match-string 2 args)
|
|
|
|
ein:url-localhost))
|
|
|
|
|
2018-10-18 19:01:43 -04:00
|
|
|
(defcustom ein:process-jupyter-regexp "\\(jupyter\\|ipython\\)\\(-\\|\\s-+\\)note"
|
|
|
|
"Regexp by which we recognize notebook servers."
|
|
|
|
:type 'string
|
|
|
|
:group 'ein)
|
|
|
|
|
|
|
|
|
|
|
|
(defcustom ein:process-lsof "lsof"
|
|
|
|
"Executable for lsof command."
|
|
|
|
:type 'string
|
|
|
|
:group 'ein)
|
|
|
|
|
|
|
|
(defun ein:process-divine-dir (pid args &optional error-buffer)
|
|
|
|
(if (string-match "\\bnotebook-dir\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
|
|
|
(directory-file-name (match-string 2 args))
|
|
|
|
(if (executable-find ein:process-lsof)
|
|
|
|
(ein:trim-right
|
|
|
|
(with-output-to-string
|
|
|
|
(shell-command (format "%s -p %d -a -d cwd -Fn | grep ^n | tail -c +2"
|
|
|
|
ein:process-lsof pid)
|
|
|
|
standard-output error-buffer))))))
|
|
|
|
|
|
|
|
(defun ein:process-divine-port (pid args &optional error-buffer)
|
|
|
|
"Returns port on which PID is listening or 0 if none. Supply ERROR-BUFFER to capture stderr"
|
|
|
|
(if (string-match "\\bport\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
|
|
|
(string-to-number (match-string 2 args))
|
|
|
|
(if (executable-find ein:process-lsof)
|
|
|
|
(string-to-number
|
|
|
|
(ein:trim-right
|
|
|
|
(with-output-to-string
|
|
|
|
(shell-command (format "%s -p %d -a -iTCP -sTCP:LISTEN -Fn | grep ^n | sed \"s/[^0-9]//g\""
|
|
|
|
ein:process-lsof pid)
|
|
|
|
standard-output error-buffer)))))))
|
|
|
|
|
|
|
|
(defun ein:process-divine-ip (pid args)
|
|
|
|
"Returns notebook-ip of PID"
|
|
|
|
(if (string-match "\\bip\\(=\\|\\s-+\\)\\(\\S-+\\)" args)
|
|
|
|
(match-string 2 args)
|
|
|
|
"localhost"))
|
|
|
|
|
2018-10-15 16:57:22 -04:00
|
|
|
(defstruct ein:$process
|
|
|
|
"Hold process variables.
|
|
|
|
|
|
|
|
`ein:$process-pid' : integer
|
|
|
|
PID.
|
|
|
|
|
|
|
|
`ein:$process-port': integer
|
|
|
|
Arg of --port or .
|
|
|
|
|
|
|
|
`ein:$process-ip' : string
|
|
|
|
Arg of --notebook-ip or 'localhost'.
|
|
|
|
|
|
|
|
`ein:$process-dir' : string
|
|
|
|
Arg of --notebook-dir or 'readlink -e /proc/<pid>/cwd'."
|
|
|
|
pid
|
|
|
|
port
|
|
|
|
ip
|
|
|
|
dir
|
|
|
|
)
|
|
|
|
|
|
|
|
(ein:deflocal ein:%processes% (make-hash-table :test #'equal)
|
|
|
|
"Process table of `ein:$process' keyed on dir.")
|
|
|
|
|
|
|
|
(defun ein:process-processes ()
|
|
|
|
(ein:hash-vals ein:%processes%))
|
|
|
|
|
|
|
|
(defun ein:process-alive-p (proc)
|
|
|
|
(not (null (process-attributes (ein:$process-pid proc)))))
|
|
|
|
|
|
|
|
(defun ein:process-suitable-notebook-dir (filename)
|
|
|
|
"Return the uppermost parent dir of DIR that contains ipynb files."
|
|
|
|
(let ((fn (expand-file-name filename)))
|
|
|
|
(loop with directory = (directory-file-name
|
|
|
|
(if (f-file? fn) (f-parent fn) fn))
|
|
|
|
with suitable = directory
|
|
|
|
until (string= (file-name-nondirectory directory) "")
|
|
|
|
do (if (directory-files directory nil "\\.ipynb$")
|
|
|
|
(setq suitable directory))
|
|
|
|
(setq directory (directory-file-name (file-name-directory directory)))
|
|
|
|
finally return suitable)))
|
|
|
|
|
|
|
|
(defun ein:process-refresh-processes ()
|
|
|
|
(loop for pid in (list-system-processes)
|
|
|
|
for attrs = (process-attributes pid)
|
|
|
|
for args = (alist-get 'args attrs)
|
|
|
|
with seen = (mapcar #'ein:$process-pid (ein:hash-vals ein:%processes%))
|
|
|
|
if (and (null (member pid seen))
|
|
|
|
(string-match ein:process-jupyter-regexp (alist-get 'comm attrs)))
|
|
|
|
;; bummer should use output of jupyter notebook list --json
|
|
|
|
do (ein:and-let* ((dir (ein:process-divine-dir pid args))
|
|
|
|
(port (ein:process-divine-port pid args))
|
|
|
|
(ip (ein:process-divine-ip pid args)))
|
|
|
|
(puthash dir (make-ein:$process :pid pid
|
|
|
|
:port port
|
|
|
|
:ip ip
|
|
|
|
:dir dir)
|
|
|
|
ein:%processes%))
|
|
|
|
end))
|
|
|
|
|
|
|
|
(defun ein:process-dir-match (filename)
|
|
|
|
"Return ein:process whose directory is prefix of FILENAME."
|
|
|
|
(loop for dir in (ein:hash-keys ein:%processes%)
|
|
|
|
when (search dir filename)
|
|
|
|
return (gethash dir ein:%processes%)))
|
|
|
|
|
|
|
|
(defsubst ein:process-url-or-port (proc)
|
|
|
|
"Naively construct url-or-port from ein:process PROC's port and ip fields"
|
|
|
|
(ein:url (format "http://%s:%s" (ein:$process-ip proc) (ein:$process-port proc))))
|
|
|
|
|
|
|
|
(defsubst ein:process-path (proc filename)
|
|
|
|
"Construct path by eliding PROC's dir from filename"
|
|
|
|
(subseq filename (length (file-name-as-directory (ein:$process-dir proc)))))
|
|
|
|
|
|
|
|
(defun ein:process-open-notebook* (filename callback)
|
|
|
|
"Open FILENAME as a notebook and start a notebook server if necessary. CALLBACK with arity 2 (passed into ein:notebook-open--callback)."
|
|
|
|
(ein:process-refresh-processes)
|
|
|
|
(let* ((proc (ein:process-dir-match filename)))
|
|
|
|
(when (and proc (not (ein:process-alive-p proc)))
|
|
|
|
(ein:log 'warn "Server pid=%s dir=%s no longer running" (ein:$process-pid proc) (ein:$process-dir proc))
|
|
|
|
(remhash (ein:$process-dir proc) ein:%processes%)
|
|
|
|
(setq proc nil))
|
|
|
|
(if proc
|
|
|
|
(let* ((url-or-port (ein:process-url-or-port proc))
|
|
|
|
(path (ein:process-path proc filename))
|
|
|
|
(callback1 (apply-partially (lambda (url-or-port* path* callback* buffer)
|
|
|
|
(ein:notebook-open
|
|
|
|
url-or-port* path* nil callback*))
|
|
|
|
url-or-port path callback)))
|
|
|
|
(if (ein:notebooklist-list-get url-or-port)
|
|
|
|
(ein:notebook-open url-or-port path nil callback)
|
|
|
|
(ein:notebooklist-login url-or-port callback1)))
|
|
|
|
(let* ((nbdir (read-directory-name "Notebook directory: "
|
|
|
|
(ein:process-suitable-notebook-dir filename)))
|
|
|
|
(path (subseq filename (length (file-name-as-directory nbdir))))
|
|
|
|
(callback1 (apply-partially (lambda (path* callback* buffer)
|
|
|
|
(ein:notebook-open
|
|
|
|
(car (ein:jupyter-server-conn-info))
|
|
|
|
path* nil callback*))
|
|
|
|
path callback)))
|
|
|
|
(apply #'ein:jupyter-server-start
|
|
|
|
(list ein:jupyter-default-server-command nbdir nil callback1))))))
|
|
|
|
|
|
|
|
(defun ein:process-open-notebook (&optional filename buffer-callback)
|
|
|
|
"When FILENAME is unspecified the variable `buffer-file-name'
|
|
|
|
is used instead. BUFFER-CALLBACK is called after opening notebook with the
|
|
|
|
current buffer as the only one argument."
|
|
|
|
(interactive)
|
|
|
|
(unless filename (setq filename buffer-file-name))
|
|
|
|
(assert filename nil "Not visiting a file")
|
|
|
|
(let ((callback2 (apply-partially (lambda (buffer buffer-callback* notebook created
|
|
|
|
&rest args)
|
|
|
|
(when (buffer-live-p buffer)
|
|
|
|
(funcall buffer-callback* buffer)))
|
|
|
|
(current-buffer) (or buffer-callback #'ignore))))
|
|
|
|
(ein:process-open-notebook* (expand-file-name filename) callback2)))
|
|
|
|
|
|
|
|
(defun ein:process-find-file-callback ()
|
|
|
|
"A callback function for `find-file-hook' to open notebook."
|
|
|
|
(interactive)
|
|
|
|
(ein:and-let* ((filename buffer-file-name)
|
|
|
|
((string-match-p "\\.ipynb$" filename)))
|
|
|
|
(ein:process-open-notebook filename #'kill-buffer-if-not-modified)))
|
|
|
|
|
|
|
|
(provide 'ein-process)
|
|
|
|
|
|
|
|
;;; ein-process.el ends here
|