emacs-jupyter/jupyter-env.el
Nathaniel Nicandro 1ac3d658e9 Simplify connection file removal
This is mostly a refactor of the old behavior except now the removal
of connection files happens in `kill-emacs-hook` and also when
cleaning up dead kernel processes.

* jupyter-env.el
(jupyter-write-connection-file): Turn into a `defun`.  Remove the
connection file removal code.

* jupyter-kernel-process-manager.el
(jupyter--kernel-processes): Mention new form of elements in doc.
(jupyter-delete-connection-files): New function.  Add it to
`kill-emacs-hook` at top-level.
(jupyter--gc-kernel-processes):
(jupyter--start-kernel-processes): Use new form of
`jupyter--kernel-processes`.
(jupyter-local-tcp-conn-info): Relocate to...

* test/jupyter-test.el: ...here.
(jupyter-write-connection-file): Update test.
2023-02-13 20:21:14 -06:00

166 lines
6.9 KiB
EmacsLisp

;;; jupyter-env.el --- Query the jupyter shell command for information -*- lexical-binding: t -*-
;; Copyright (C) 2019-2020 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 27 Jun 2019
;; This program 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, or (at
;; your option) any later version.
;; This program 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 GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; Custom variables and functions related to calling the jupyter shell command
;; and its sub-commands for information.
;;; Code:
(require 'jupyter-base)
(eval-when-compile (require 'subr-x))
(defvar jupyter-runtime-directory nil
"The Jupyter runtime directory.
When a new kernel is started through `jupyter-start-kernel', this
directory is where kernel connection files are written to.
This variable should not be used. To obtain the runtime directory
call the function `jupyter-runtime-directory'.")
(defun jupyter-command (&rest args)
"Run a Jupyter shell command synchronously, return its output.
The shell command run is
jupyter ARGS...
If the command fails or the jupyter shell command doesn't exist,
return nil."
(with-temp-buffer
(when (zerop (apply #'process-file "jupyter" nil t nil args))
(string-trim-right (buffer-string)))))
(defun jupyter-runtime-directory ()
"Return the runtime directory used by Jupyter.
Create the directory if necessary. If `default-directory' is a
remote directory, return the runtime directory on that remote.
As a side effect, the variable `jupyter-runtime-directory' is set
to the local runtime directory if it is nil."
(unless jupyter-runtime-directory
(setq jupyter-runtime-directory
(let ((default-directory (expand-file-name "~" user-emacs-directory)))
(file-name-as-directory (jupyter-command "--runtime-dir")))))
(let ((dir (if (file-remote-p default-directory)
(jupyter-command "--runtime-dir")
jupyter-runtime-directory)))
(unless dir
(error "Can't obtain runtime directory from jupyter shell command"))
(setq dir (concat (file-remote-p default-directory) dir))
(make-directory dir 'parents)
(file-name-as-directory dir)))
(defun jupyter-locate-python ()
"Return the path to the python executable in use by Jupyter.
If the `default-directory' is a remote directory, search on that
remote. Raise an error if the executable could not be found.
The paths examined are the data paths of \"jupyter --paths\" in
the order specified.
This function always returns the `file-local-name' of the path."
(let* ((remote (file-remote-p default-directory))
(paths (mapcar (lambda (x) (concat remote x))
(or (plist-get
(jupyter-read-plist-from-string
(jupyter-command "--paths" "--json"))
:data)
(error "Can't get search paths"))))
(path nil))
(cl-loop
with programs = '("bin/python3" "bin/python"
;; Need to also check Windows since paths can be
;; pointing to local or remote files.
"python3.exe" "python.exe")
with pred = (lambda (dir)
(cl-loop
for program in programs
for spath = (expand-file-name program dir)
thereis (setq path (and (file-exists-p spath) spath))))
for path in paths
thereis (locate-dominating-file path pred)
finally (error "No `python' found in search paths"))
(file-local-name path)))
(defun jupyter-write-connection-file (session)
"Write a connection file based on SESSION to `jupyter-runtime-directory'.
Return the path to the connection file."
(cl-check-type session jupyter-session)
(let* ((temporary-file-directory (jupyter-runtime-directory))
(json-encoding-pretty-print t)
(file (make-temp-file "emacs-kernel-" nil ".json")))
(prog1 file
(with-temp-file file
(insert (json-encode-plist
(jupyter-session-conn-info session)))))))
(defun jupyter-session-with-random-ports ()
"Return a `jupyter-session' with random channel ports.
The session can be used to write a connection file, see
`jupyter-write-connection-file'."
;; The actual work of making the connection file is left up to the
;; `jupyter kernel` shell command. This is done to support
;; launching remote kernels via TRAMP. The Jupyter suite of shell
;; commands probably exist on the remote system, so we rely on them
;; to figure out a set of open ports on the remote.
(with-temp-buffer
;; NOTE: On Windows, apparently the "jupyter kernel" command uses something
;; like an exec shell command to start the process which launches the kernel,
;; but exec like commands on Windows start a new process instead of replacing
;; the current one which results in the process we start here exiting after
;; the new process is launched. We call python directly to avoid this.
(let ((process (start-file-process
"jupyter-session-with-random-ports" (current-buffer)
(jupyter-locate-python) "-c"
"from jupyter_client.kernelapp import main; main()")))
(set-process-query-on-exit-flag process nil)
(jupyter-with-timeout
(nil jupyter-long-timeout
(error "`jupyter kernel` failed to show connection file path"))
(and (process-live-p process)
(goto-char (point-min))
(re-search-forward "Connection file: \\(.+\\)\n" nil t)))
(let* ((conn-file (concat
(save-match-data
(file-remote-p default-directory))
(match-string 1)))
(conn-info (jupyter-read-connection conn-file)))
;; Tell the `jupyter kernel` process to shutdown itself and
;; the launched kernel.
(interrupt-process process)
;; Wait until the connection file is cleaned up before
;; forgetting about the process completely.
(jupyter-with-timeout
(nil jupyter-default-timeout
(delete-file conn-file))
(file-exists-p conn-file))
(delete-process process)
(let ((new-key (jupyter-new-uuid)))
(plist-put conn-info :key new-key)
(jupyter-session
:conn-info conn-info
:key new-key))))))
(provide 'jupyter-env)
;;; jupyter-env.el ends here