-
Notifications
You must be signed in to change notification settings - Fork 123
/
Copy pathein-process.el
215 lines (179 loc) · 9.01 KB
/
ein-process.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
;;; ein-process.el --- Notebook list buffer -*- lexical-binding:t -*-
;; 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:
(require 'ein-jupyter)
(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))
(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)
(cl-defstruct ein:$process
"Hold process variables.
`ein:$process-pid' : integer
PID.
`ein:$process-url': string
URL
`ein:$process-dir' : string
Arg of --notebook-dir or 'readlink -e /proc/<pid>/cwd'."
pid
url
dir
)
(ein:deflocal ein:%processes% (make-hash-table :test #'equal)
"Process table of `ein:$process' keyed on dir.")
(defun ein:process-processes ()
(hash-table-values ein:%processes%))
(defun ein:process-alive-p (proc)
(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)))
(cl-loop with directory = (directory-file-name
(if (file-regular-p fn)
(file-name-directory (directory-file-name 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 ()
"Use `jupyter notebook list --json` to populate ein:%processes%"
(clrhash ein:%processes%)
(cl-loop for line in (condition-case err
(apply #'process-lines
ein:jupyter-server-command
(append (split-string (or ein:jupyter-server-use-subcommand ""))
'("list" "--json")))
;; often there is no local jupyter installation
(error (ein:log 'info "ein:process-refresh-processes: %s" err) nil))
do (cl-destructuring-bind
(&key pid url notebook_dir &allow-other-keys)
(ein:json-read-from-string line)
(puthash (directory-file-name notebook_dir)
(make-ein:$process :pid pid
:url (ein:url url)
:dir (directory-file-name notebook_dir))
ein:%processes%))))
(defun ein:process-dir-match (filename)
"Return ein:process whose directory is prefix of FILENAME."
(cl-loop for dir in (hash-table-keys ein:%processes%)
when (cl-search dir filename)
return (gethash dir ein:%processes%)))
(defun ein:process-url-match (url-or-port)
"Return ein:process whose url matches URL-OR-PORT."
(cl-loop with parsed-url-or-port = (url-generic-parse-url url-or-port)
for proc in (ein:process-processes)
for parsed-url-proc = (url-generic-parse-url (ein:process-url-or-port proc))
when (and (string= (url-host parsed-url-or-port) (url-host parsed-url-proc))
(= (url-port parsed-url-or-port) (url-port parsed-url-proc)))
return proc))
(defsubst ein:process-url-or-port (proc)
"Naively construct url-or-port from ein:process PROC's port and ip fields"
(ein:$process-url proc))
(defsubst ein:process-path (proc filename)
"Construct path by eliding PROC's dir from filename."
(cl-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)))
(if proc
(let* ((url-or-port (ein:process-url-or-port proc))
(path (ein:process-path proc filename))
(callback2 (apply-partially (lambda (path* callback* _buffer url-or-port)
(ein:notebook-open
url-or-port path* nil callback*))
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 callback2)))
(let* ((nbdir (read-directory-name "Notebook directory: "
(ein:process-suitable-notebook-dir filename)))
(path
(concat (if ein:jupyter-use-containers
(file-name-as-directory (file-name-base ein:jupyter-docker-mount-point))
"")
(cl-subseq (expand-file-name filename)
(length (file-name-as-directory (expand-file-name nbdir))))))
(callback2 (apply-partially (lambda (path* callback* buffer url-or-port)
(pop-to-buffer buffer)
(ein:notebook-open url-or-port
path* nil callback*))
path callback)))
(ein:jupyter-server-start (executable-find ein:jupyter-server-command)
nbdir nil callback2)))))
(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 notebook opened."
(interactive)
(unless filename (setq filename buffer-file-name))
(cl-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)
(-when-let* ((filename buffer-file-name)
(match-p (string-match-p "\\.ipynb$" filename)))
(ein:process-open-notebook filename #'kill-buffer-if-not-modified)))
(provide 'ein-process)
;;; ein-process.el ends here