diff --git a/emacs/flan-dev.el b/emacs/flan-dev.el index 75b3723..e1005e7 100644 --- a/emacs/flan-dev.el +++ b/emacs/flan-dev.el @@ -345,6 +345,11 @@ socket at all and this buffer is the only account of why." It builds the program first, which for a cold project is most of this." :type 'number) +(defvar flan-dev--file nil + "The program the daemon this Emacs started was started on, or nil. +Kept so that it can be started again on the same program and the same +socket, which is what `flan-dev-restart-program' is.") + (defvar flan-dev--daemon nil "The `flan dev' process this Emacs started, or nil. A daemon started in a terminal is not here, and `flan-connect' still works @@ -432,6 +437,7 @@ which is the one thing a dev loop exists to avoid doing by accident." (file-name-directory file))))) (unless (file-exists-p file) (user-error "flan dev: no such file: %s" file)) + (setq flan-dev--file file) (setq flan-dev--daemon (flan-dev--start-daemon file socket)) (flan-dev--connect-when-ready socket flan-dev--daemon) ;; Connected by now, so the rest is what `flan-connect' does after opening: @@ -474,11 +480,44 @@ socket; the process is killed only if it does not take it." (while (and (process-live-p proc) (< (float-time) deadline)) (accept-process-output proc 0.05))) (when (process-live-p proc) - (delete-process proc))) + ;; Killed rather than asked, so its own cleanup never runs — and that + ;; cleanup is what signals the program. The program is therefore + ;; probably still running, reparented, holding its agent socket, and + ;; saying "stopped" here would be the one place in this client where + ;; a success message meant "probably". + (delete-process proc) + (setq flan-dev--daemon nil) + (force-mode-line-update t) + (user-error + "flan dev: the daemon would not close and was killed; its program may still be running"))) (setq flan-dev--daemon nil) (force-mode-line-update t) (message "flan dev: stopped"))) +;;;###autoload +(defun flan-dev-restart-program () + "Stop the program this Emacs started and start it again from source. + +For a change the running program cannot take: a struct whose layout moved, a +function whose signature changed, anything the daemon refuses by telling you +to restart. There is no smaller version of this. A session's struct layouts +and global types describe the memory of a process only if that session +compiled it, so a new layout means a new build, which means a new process and +the session that made it — and the program's state goes with it, which is the +whole cost and the reason this is a separate command rather than something +`C-c C-c' falls back to. + +The old daemon is waited for before the new one starts, because it unlinks +the socket on its way out and would otherwise unlink the one its successor +had just bound." + (interactive) + (unless (process-live-p flan-dev--daemon) + (user-error "flan dev: no daemon started from Emacs to restart")) + (let ((file flan-dev--file) + (socket flan-dev--socket)) + (flan-dev-quit) ; returns only once it is really gone + (flan-dev file socket))) + ;;; The modeline ;; Whether there is a program on the other end is the one thing worth a diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el index 4b04f60..2daab24 100644 --- a/emacs/flan-mode.el +++ b/emacs/flan-mode.el @@ -121,8 +121,10 @@ line is off screen." (define-key map (kbd "C-c C-b") #'flan-break) ;; Help on the name at point. C-c C-d is taken by `flan-describe', which ;; is about the session rather than about a name, and renaming a key that - ;; is already documented costs more than it is worth. - (define-key map (kbd "C-c C-h") #'flan-doc) + ;; is already documented costs more than it is worth. Not C-c C-h either: + ;; C-h after a prefix is how anyone finds out what is under C-c, and a + ;; binding there takes that away. + (define-key map (kbd "C-c C-v") #'flan-doc) map) "Keymap for `flan-mode'.") diff --git a/emacs/test-flan-dev.el b/emacs/test-flan-dev.el index 6639d7f..cd5cfa9 100644 --- a/emacs/test-flan-dev.el +++ b/emacs/test-flan-dev.el @@ -537,6 +537,25 @@ is written instead — the real `message' call the real command makes." (and raised (string-match-p "already running" raised)))) ;; The daemon owns the program's lifetime, so quitting has to actually end ;; the process — not just drop the socket and leave it running. + ;; Restarting the program: for a change the running one cannot take — a + ;; struct whose layout moved — where the answer is a new build, a new + ;; process and the session that compiled it. Proved by what it throws + ;; away: a name installed into the old program is not in the new one. + (flan-dev--eval "(defn only-in-the-old-program [] i64 1)" "form") + (test-flan--check "a name installed into the running program is there" + (assoc "only-in-the-old-program" flan-dev--defs)) + (let ((old flan-dev--daemon)) + (flan-dev-restart-program) + (test-flan--check "restarting gives a different daemon, connected" + (and (not (process-live-p old)) + (process-live-p flan-dev--daemon) + (not (eq old flan-dev--daemon)) + (eq (flan-dev-state) 'live)))) + (test-flan--check "and a program built from source, without the addition" + (and (assoc "step" flan-dev--defs) + (null (assoc "only-in-the-old-program" + flan-dev--defs)))) + (let ((proc flan-dev--daemon)) (flan-dev-quit) ;; The process, not the variable: forgetting a daemon is not stopping