Put Eshell's bookkeeping data for external processes on the process object

This allows tracking this information for process objects not recorded
in 'eshell-process-list', which will be useful for pipe processes for
stderr output.

* lisp/eshell/esh-proc.el (eshell-process-list): Add docstring.
(eshell-record-process-object): Only record the process object and
whether it's a subjob.
(eshell-remove-process-entry): Adapt to changes in
'eshell-record-process-object'.
(eshell-record-process-properties): New function...
(eshell-gather-process-output): ... call it.
(eshell-insertion-filter, eshell-sentinel): Use new process
properties, don't require process to be in 'eshell-process-list'.

* test/lisp/eshell/esh-proc-tests.el (esh-proc-test--output-cmd): New
variable.
(esh-proc-test--detect-pty-cmd): Add docstring.
(esh-proc-test/output/to-screen)
(esh-proc-test/output/stdout-and-stderr-to-buffer)
(esh-proc-test/exit-status/success, esh-proc-test/exit-status/failure)
(esh-proc-test/kill-process/foreground-only): New tests.
(esh-proc-test/kill-background-process): Rename to...
(esh-proc-test/kill-process/background-prompt): ... this, and use
'eshell-wait-for-subprocess' instead of 'sit-for'.
This commit is contained in:
Jim Porter 2022-08-28 11:19:30 -07:00
parent ab7e94fb1d
commit a87c7aff55
2 changed files with 159 additions and 80 deletions

View file

@ -99,7 +99,13 @@ information, for example."
(defvar eshell-current-subjob-p nil)
(defvar eshell-process-list nil
"A list of the current status of subprocesses.")
"A list of the current status of subprocesses.
Each element has the form (PROC . SUBJOB-P), where PROC is the
process object and SUBJOB-P is non-nil if the process is a
subjob.
To add or remove elements of this list, see
`eshell-record-process-object' and `eshell-remove-process-entry'.")
(declare-function eshell-send-eof-to-process "esh-mode")
(declare-function eshell-tail-process "esh-cmd")
@ -229,21 +235,26 @@ The prompt will be set to PROMPT."
(declare-function eshell-interactive-print "esh-mode" (string))
(eshell-interactive-print
(format "[%s] %d\n" (process-name object) (process-id object))))
(setq eshell-process-list
(cons (list object eshell-current-handles
eshell-current-subjob-p nil nil)
eshell-process-list)))
(push (cons object eshell-current-subjob-p) eshell-process-list))
(defun eshell-remove-process-entry (entry)
"Record the process ENTRY as fully completed."
(if (and (eshell-processp (car entry))
(nth 2 entry)
(cdr entry)
eshell-done-messages-in-minibuffer)
(message "[%s]+ Done %s" (process-name (car entry))
(process-command (car entry))))
(setq eshell-process-list
(delq entry eshell-process-list)))
(defun eshell-record-process-properties (process)
"Record Eshell bookkeeping properties for PROCESS.
`eshell-insertion-filter' and `eshell-sentinel' will use these to
do their jobs."
(process-put process :eshell-handles eshell-current-handles)
(process-put process :eshell-pending nil)
(process-put process :eshell-busy nil))
(defvar eshell-scratch-buffer " *eshell-scratch*"
"Scratch buffer for holding Eshell's input/output.")
(defvar eshell-last-sync-output-start nil
@ -283,6 +294,7 @@ Used only on systems which do not support async subprocesses.")
:connection-type conn-type
:file-handler t)))
(eshell-record-process-object proc)
(eshell-record-process-properties proc)
(run-hook-with-args 'eshell-exec-hook proc)
(when (fboundp 'process-coding-system)
(let ((coding-systems (process-coding-system proc)))
@ -363,36 +375,35 @@ PROC is the process for which we're inserting output. STRING is the
output."
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(let ((entry (assq proc eshell-process-list)))
(when entry
(setcar (nthcdr 3 entry)
(concat (nth 3 entry) string))
(unless (nth 4 entry) ; already being handled?
(while (nth 3 entry)
(let ((data (nth 3 entry)))
(setcar (nthcdr 3 entry) nil)
(setcar (nthcdr 4 entry) t)
(unwind-protect
(condition-case nil
(eshell-output-object data nil (cadr entry))
;; FIXME: We want to send SIGPIPE to the process
;; here. However, remote processes don't
;; currently support that, and not all systems
;; have SIGPIPE in the first place (e.g. MS
;; Windows). In these cases, just delete the
;; process; this is reasonably close to the
;; right behavior, since the default action for
;; SIGPIPE is to terminate the process. For use
;; cases where SIGPIPE is truly needed, using an
;; external pipe operator (`*|') may work
;; instead (e.g. when working with remote
;; processes).
(eshell-pipe-broken
(if (or (process-get proc 'remote-pid)
(eq system-type 'windows-nt))
(delete-process proc)
(signal-process proc 'SIGPIPE))))
(setcar (nthcdr 4 entry) nil))))))))))
(process-put proc :eshell-pending
(concat (process-get proc :eshell-pending)
string))
(unless (process-get proc :eshell-busy) ; Already being handled?
(while (process-get proc :eshell-pending)
(let ((handles (process-get proc :eshell-handles))
(data (process-get proc :eshell-pending)))
(process-put proc :eshell-pending nil)
(process-put proc :eshell-busy t)
(unwind-protect
(condition-case nil
(eshell-output-object data nil handles)
;; FIXME: We want to send SIGPIPE to the process
;; here. However, remote processes don't currently
;; support that, and not all systems have SIGPIPE in
;; the first place (e.g. MS Windows). In these
;; cases, just delete the process; this is
;; reasonably close to the right behavior, since the
;; default action for SIGPIPE is to terminate the
;; process. For use cases where SIGPIPE is truly
;; needed, using an external pipe operator (`*|')
;; may work instead (e.g. when working with remote
;; processes).
(eshell-pipe-broken
(if (or (process-get proc 'remote-pid)
(eq system-type 'windows-nt))
(delete-process proc)
(signal-process proc 'SIGPIPE))))
(process-put proc :eshell-busy nil))))))))
(defun eshell-sentinel (proc string)
"Generic sentinel for command processes. Reports only signals.
@ -400,37 +411,34 @@ PROC is the process that's exiting. STRING is the exit message."
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(unwind-protect
(when-let ((entry (assq proc eshell-process-list)))
(unwind-protect
(unless (string= string "run")
;; Write the exit message if the status is
;; abnormal and the process is already writing
;; to the terminal.
(when (and (eq proc (eshell-tail-process))
(not (string-match "^\\(finished\\|exited\\)"
string)))
(funcall (process-filter proc) proc string))
(let ((handles (nth 1 entry))
(str (prog1 (nth 3 entry)
(setf (nth 3 entry) nil)))
(status (process-exit-status proc)))
;; If we're in the middle of handling output
;; from this process then schedule the EOF for
;; later.
(letrec ((finish-io
(lambda ()
(if (nth 4 entry)
(run-at-time 0 nil finish-io)
(when str
(ignore-error 'eshell-pipe-broken
(eshell-output-object
str nil handles)))
(eshell-close-handles
status (list 'quote (= status 0))
handles)))))
(funcall finish-io))))
(eshell-remove-process-entry entry)))
(eshell-kill-process-function proc string)))))
(unless (string= string "run")
;; Write the exit message if the status is abnormal and
;; the process is already writing to the terminal.
(when (and (eq proc (eshell-tail-process))
(not (string-match "^\\(finished\\|exited\\)"
string)))
(funcall (process-filter proc) proc string))
(let ((handles (process-get proc :eshell-handles))
(data (process-get proc :eshell-pending))
(status (process-exit-status proc)))
(process-put proc :eshell-pending nil)
;; If we're in the middle of handling output from this
;; process then schedule the EOF for later.
(letrec ((finish-io
(lambda ()
(if (process-get proc :eshell-busy)
(run-at-time 0 nil finish-io)
(when data
(ignore-error 'eshell-pipe-broken
(eshell-output-object
data nil handles)))
(eshell-close-handles
status (list 'quote (= status 0))
handles)))))
(funcall finish-io))))
(when-let ((entry (assq proc eshell-process-list)))
(eshell-remove-process-entry entry))
(eshell-kill-process-function proc string)))))
(defun eshell-process-interact (func &optional all query)
"Interact with a process, using PROMPT if more than one, via FUNC.
@ -441,7 +449,7 @@ If QUERY is non-nil, query the user with QUERY before calling FUNC."
(if (and (memq (process-status (car entry))
'(run stop open closed))
(or all
(not (nth 2 entry)))
(not (cdr entry)))
(or (not query)
(y-or-n-p (format-message query
(process-name (car entry))))))

View file

@ -28,15 +28,67 @@
(file-name-directory (or load-file-name
default-directory))))
(defvar esh-proc-test--output-cmd
(concat "sh -c '"
"echo stdout; "
"echo stderr >&2"
"'")
"A shell command that prints to both stdout and stderr.")
(defvar esh-proc-test--detect-pty-cmd
(concat "sh -c '"
"if [ -t 0 ]; then echo stdin; fi; "
"if [ -t 1 ]; then echo stdout; fi; "
"if [ -t 2 ]; then echo stderr; fi"
"'"))
"'")
"A shell command that prints the standard streams connected as TTYs.")
;;; Tests:
;; Output and redirection
(ert-deftest esh-proc-test/output/to-screen ()
"Check that outputting stdout and stderr to the screen works."
(skip-unless (executable-find "sh"))
(with-temp-eshell
(eshell-match-command-output esh-proc-test--output-cmd
"stdout\nstderr\n")))
(ert-deftest esh-proc-test/output/stdout-and-stderr-to-buffer ()
"Check that redirecting stdout and stderr works."
(skip-unless (executable-find "sh"))
(eshell-with-temp-buffer bufname "old"
(with-temp-eshell
(eshell-match-command-output
(format "%s &> #<%s>" esh-proc-test--output-cmd bufname)
"\\`\\'"))
(should (equal (buffer-string) "stdout\nstderr\n"))))
;; Exit status
(ert-deftest esh-proc-test/exit-status/success ()
"Check that successful execution is properly recorded."
(skip-unless (executable-find "sh"))
(with-temp-eshell
(eshell-insert-command "sh -c 'exit 0'")
(eshell-wait-for-subprocess)
(should (= eshell-last-command-status 0))
(should (eq eshell-last-command-result t))))
(ert-deftest esh-proc-test/exit-status/failure ()
"Check that failed execution is properly recorded."
(skip-unless (executable-find "sh"))
(with-temp-eshell
(eshell-insert-command "sh -c 'exit 1'")
(eshell-wait-for-subprocess)
(should (= eshell-last-command-status 1))
(should (eq eshell-last-command-result nil))))
;; Pipelines
(ert-deftest esh-proc-test/sigpipe-exits-process ()
"Test that a SIGPIPE is properly sent to a process if a pipe closes"
(skip-unless (and (executable-find "sh")
@ -94,6 +146,35 @@ pipeline."
(unless (eq system-type 'windows-nt)
"stdout\nstderr\n"))))
;; Killing processes
(ert-deftest esh-proc-test/kill-process/foreground-only ()
"Test that `eshell-kill-process' only kills foreground processes."
(with-temp-eshell
(eshell-insert-command "sleep 100 &")
(eshell-insert-command "sleep 100")
(should (equal (length eshell-process-list) 2))
;; This should kill only the foreground process.
(eshell-kill-process)
(eshell-wait-for-subprocess)
(should (equal (length eshell-process-list) 1))
;; Now kill everything, including the background process.
(eshell-process-interact 'kill-process t)
(eshell-wait-for-subprocess t)
(should (equal (length eshell-process-list) 0))))
(ert-deftest esh-proc-test/kill-process/background-prompt ()
"Test that killing a background process doesn't emit a new
prompt. See bug#54136."
(skip-unless (and (executable-find "sh")
(executable-find "sleep")))
(with-temp-eshell
(eshell-insert-command "sh -c 'while true; do sleep 1; done' &")
(kill-process (caar eshell-process-list))
(eshell-wait-for-subprocess)
(should (eshell-match-output "\\[sh\\(\\.exe\\)?\\] [[:digit:]]+\n"))))
(ert-deftest esh-proc-test/kill-pipeline ()
"Test that killing a pipeline of processes only emits a single
prompt. See bug#54136."
@ -133,14 +214,4 @@ write the exit status to the pipe. See bug#54136."
output-start (eshell-end-of-output))
"")))))
(ert-deftest esh-proc-test/kill-background-process ()
"Test that killing a background process doesn't emit a new
prompt. See bug#54136."
(skip-unless (and (executable-find "sh")
(executable-find "sleep")))
(with-temp-eshell
(eshell-insert-command "sh -c 'while true; do sleep 1; done' &")
(kill-process (caar eshell-process-list))
;; Give `eshell-sentinel' a chance to run.
(sit-for 0.1)
(should (eshell-match-output "\\[sh\\(\\.exe\\)?\\] [[:digit:]]+\n"))))
;;; esh-proc-tests.el ends here