From 4ab195df11afb66253aea8825e9185c25e90c428 Mon Sep 17 00:00:00 2001 From: Thierry Volpiatto Date: Wed, 19 Jun 2024 12:02:59 +0200 Subject: [PATCH] Allow using multiple buffers in 'eshell-command' Provide the same functionality as 'async-shell-command-buffer' but for 'eshell-command'. Co-Authored-By: Jim Porter * lisp/eshell/eshell.el (eshell-command-async-buffer): New option... (eshell-command): ... use it. * lisp/eshell/esh-proc.el (eshell-sentinel): Check for buffer liveness in 'finish-io'. * test/lisp/eshell/eshell-tests.el (eshell-test/eshell-command/output-buffer/async-kill): New test. * etc/NEWS: Announce this change (bug#71554). (cherry picked from commit 7f631a3e2aca97e95b8659c902c25ab21f084e08) --- etc/NEWS | 11 ++++ lisp/eshell/esh-proc.el | 50 +++++++++--------- lisp/eshell/eshell.el | 88 +++++++++++++++++++++++++++----- test/lisp/eshell/eshell-tests.el | 21 ++++++++ 4 files changed, 135 insertions(+), 35 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 1af252e8a8f..3d2b86cfb6a 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -43,6 +43,17 @@ applies, and please also update docstrings as needed. If 'whitespace-style' includes 'missing-newline-at-eof (which is the default), the 'whitespace-cleanup' function will now add the newline. +** Eshell + +--- +*** New option 'eshell-command-async-buffer'. +This option lets you tell 'eshell-command' how to respond if its output +buffer is already in use by another invocation of 'eshell-command', much +like 'async-shell-command-buffer' does for 'shell-command'. By default, +this will prompt for confirmation before creating a new buffer when +necessary. To restore the previous behavior, set this option to +'confirm-kill-process'. + ** SHR +++ diff --git a/lisp/eshell/esh-proc.el b/lisp/eshell/esh-proc.el index 2ff41c3d409..a5e9de79907 100644 --- a/lisp/eshell/esh-proc.el +++ b/lisp/eshell/esh-proc.el @@ -530,30 +530,34 @@ PROC is the process that's exiting. STRING is the exit message." (not (process-live-p proc)))) (finish-io (lambda () - (with-current-buffer (process-buffer proc) - (if (or (process-get proc :eshell-busy) - (and wait-for-stderr (car stderr-live))) - (progn + (if (buffer-live-p (process-buffer proc)) + (with-current-buffer (process-buffer proc) + (if (or (process-get proc :eshell-busy) + (and wait-for-stderr (car stderr-live))) + (progn + (eshell-debug-command 'process + "i/o busy for process `%s'" proc) + (run-at-time 0 nil finish-io)) + (when data + (ignore-error eshell-pipe-broken + (eshell-output-object + data index handles))) + (eshell-close-handles + status + (when status (list 'quote (= status 0))) + handles) + ;; Clear the handles to mark that we're 100% + ;; finished with the I/O for this process. + (process-put proc :eshell-handles nil) (eshell-debug-command 'process - "i/o busy for process `%s'" proc) - (run-at-time 0 nil finish-io)) - (when data - (ignore-error eshell-pipe-broken - (eshell-output-object - data index handles))) - (eshell-close-handles - status - (when status (list 'quote (= status 0))) - handles) - ;; Clear the handles to mark that we're 100% - ;; finished with the I/O for this process. - (process-put proc :eshell-handles nil) - (eshell-debug-command 'process - "finished external process `%s'" proc) - (if primary - (run-hook-with-args 'eshell-kill-hook - proc string) - (setcar stderr-live nil))))))) + "finished external process `%s'" proc) + (if primary + (run-hook-with-args 'eshell-kill-hook + proc string) + (setcar stderr-live nil)))) + (eshell-debug-command 'process + "buffer for external process `%s' already killed" + proc))))) (funcall finish-io))) (when-let ((entry (assq proc eshell-process-list))) (eshell-remove-process-entry entry)))))) diff --git a/lisp/eshell/eshell.el b/lisp/eshell/eshell.el index 18e05a371a4..568f6745067 100644 --- a/lisp/eshell/eshell.el +++ b/lisp/eshell/eshell.el @@ -216,6 +216,34 @@ named \"*eshell*<2>\"." :type 'string :group 'eshell) +(defcustom eshell-command-async-buffer 'confirm-new-buffer + "What to do when the output buffer is used by another shell command. +This option specifies how to resolve the conflict where a new command +wants to direct its output to the buffer whose name is stored +in `eshell-command-buffer-name-async', but that buffer is already +taken by another running shell command. + +The value `confirm-kill-process' is used to ask for confirmation before +killing the already running process and running a new process in the +same buffer, `confirm-new-buffer' for confirmation before running the +command in a new buffer with a name other than the default buffer name, +`new-buffer' for doing the same without confirmation, +`confirm-rename-buffer' for confirmation before renaming the existing +output buffer and running a new command in the default buffer, +`rename-buffer' for doing the same without confirmation." + :type '(choice (const :tag "Confirm killing of running command" + confirm-kill-process) + (const :tag "Confirm creation of a new buffer" + confirm-new-buffer) + (const :tag "Create a new buffer" + new-buffer) + (const :tag "Confirm renaming of existing buffer" + confirm-rename-buffer) + (const :tag "Rename the existing buffer" + rename-buffer)) + :group 'eshell + :version "31.1") + ;;;_* Running Eshell ;; ;; There are only three commands used to invoke Eshell. The first two @@ -283,11 +311,19 @@ information on Eshell, see Info node `(eshell)Top'." (eshell-command-mode +1)) (read-from-minibuffer prompt)))) +(defvar eshell-command-buffer-name-async "*Eshell Async Command Output*") +(defvar eshell-command-buffer-name-sync "*Eshell Command Output*") + ;;;###autoload (defun eshell-command (command &optional to-current-buffer) "Execute the Eshell command string COMMAND. If TO-CURRENT-BUFFER is non-nil (interactively, with the prefix -argument), then insert output into the current buffer at point." +argument), then insert output into the current buffer at point. + +When \"&\" is added at end of command, the command is async and its output +appears in a specific buffer. You can customize +`eshell-command-async-buffer' to specify what to do when this output +buffer is already taken by another running shell command." (interactive (list (eshell-read-command) current-prefix-arg)) (save-excursion @@ -301,18 +337,46 @@ argument), then insert output into the current buffer at point." (eshell-current-subjob-p)) ,(eshell-parse-command command)) command)) - intr - (bufname (if (eq (car-safe proc) :eshell-background) - "*Eshell Async Command Output*" - (setq intr t) - "*Eshell Command Output*"))) - (if (buffer-live-p (get-buffer bufname)) - (kill-buffer bufname)) - (rename-buffer bufname) + (async (eq (car-safe proc) :eshell-background)) + (bufname (cond + (to-current-buffer nil) + (async eshell-command-buffer-name-async) + (t eshell-command-buffer-name-sync))) + unique) + (when bufname + (when (buffer-live-p (get-buffer bufname)) + (cond + ((with-current-buffer bufname + (and (null eshell-foreground-command) + (null eshell-background-commands))) + ;; The old buffer is done executing; kill it so we can + ;; take its place. + (kill-buffer bufname)) + ((eq eshell-command-async-buffer 'confirm-kill-process) + (shell-command--same-buffer-confirm "Kill it") + (with-current-buffer bufname + ;; Stop all the processes in the old buffer (there may + ;; be several). + (eshell-process-interact #'interrupt-process t)) + (accept-process-output) + (kill-buffer bufname)) + ((eq eshell-command-async-buffer 'confirm-new-buffer) + (shell-command--same-buffer-confirm "Use a new buffer") + (setq unique t)) + ((eq eshell-command-async-buffer 'new-buffer) + (setq unique t)) + ((eq eshell-command-async-buffer 'confirm-rename-buffer) + (shell-command--same-buffer-confirm "Rename it") + (with-current-buffer bufname + (rename-uniquely))) + ((eq eshell-command-async-buffer 'rename-buffer) + (with-current-buffer bufname + (rename-uniquely))))) + (rename-buffer bufname unique)) ;; things get a little coarse here, since the desire is to ;; make the output as attractive as possible, with no ;; extraneous newlines - (when intr + (unless async (apply #'eshell-wait-for-process (cadr eshell-foreground-command)) (cl-assert (not eshell-foreground-command)) (goto-char (point-max)) @@ -320,7 +384,7 @@ argument), then insert output into the current buffer at point." (delete-char -1))) (cl-assert (and buf (buffer-live-p buf))) (unless to-current-buffer - (let ((len (if (not intr) 2 + (let ((len (if async 2 (count-lines (point-min) (point-max))))) (cond ((= len 0) @@ -336,7 +400,7 @@ argument), then insert output into the current buffer at point." ;; cause the output buffer to take up as little screen ;; real-estate as possible, if temp buffer resizing is ;; enabled - (and intr temp-buffer-resize-mode + (and (not async) temp-buffer-resize-mode (resize-temp-buffer-window))))))))))) ;;;###autoload diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el index e58b5a14ed9..f16c28cd1ae 100644 --- a/test/lisp/eshell/eshell-tests.el +++ b/test/lisp/eshell/eshell-tests.el @@ -117,6 +117,27 @@ This test uses a pipeline for the command." (forward-line) (should (looking-at "hi\n")))))) +(ert-deftest eshell-test/eshell-command/output-buffer/async-kill () + "Test that the `eshell-command' function kills the old process when told to." + (skip-unless (executable-find "echo")) + (ert-with-temp-directory eshell-directory-name + (let ((orig-processes (process-list)) + (eshell-history-file-name nil) + (eshell-command-async-buffer 'confirm-kill-process)) + (eshell-command "sleep 5 | *echo hi &") + (cl-letf* ((result t) + ;; Say "yes" only once: for the `confirm-kill-process' + ;; prompt. If there are any other prompts (e.g. from + ;; `kill-buffer'), say "no" to make the test fail. + ((symbol-function 'yes-or-no-p) + (lambda (_prompt) (prog1 result (setq result nil))))) + (eshell-command "*echo bye &")) + (eshell-wait-for (lambda () (equal (process-list) orig-processes))) + (with-current-buffer "*Eshell Async Command Output*" + (goto-char (point-min)) + (forward-line) + (should (looking-at "bye\n")))))) + (ert-deftest eshell-test/command-running-p () "Modeline should show no command running" (with-temp-eshell -- 2.39.2