]> git.eshelyaron.com Git - emacs.git/commitdiff
Use 'unwind-protect' to ensure that Eshell always closes I/O handles
authorJim Porter <jporterbugs@gmail.com>
Sat, 20 Jul 2024 01:02:16 +0000 (18:02 -0700)
committerEshel Yaron <me@eshelyaron.com>
Mon, 29 Jul 2024 17:16:19 +0000 (19:16 +0200)
See bug#72220.

* lisp/eshell/esh-cmd.el (eshell-with-handles): New macro...
(eshell-commands): ... use it.
(eshell-with-copied-handles): Remove STEAL-P and allow multiple body
forms (this is an incompatible change, but the macro is currently
internal despite the name).
(eshell-parse-command, eshell-do-pipelines)
(eshell-do-pipelines-synchronously, eshell--invoke-command-directly-p):
Remove handle stealing.
(eshell-structure-basic-command, eshell-do-command)
(eshell-lisp-command): Remove 'eshell-close-handles'.
(eshell-protect): Make obsolete.
(eshell-rewrite-for-command, eshell-rewrite-while-command)
(eshell-rewrite-if-command, (eshell-parse-pipeline): Remove
'eshell-protect'.

* lisp/eshell/esh-io.el (eshell-duplicate-handles): Make STEAL-P
obsolete.

* lisp/eshell/esh-proc.el (eshell-gather-process-output): Call
'eshell-protect-handles' one more time.  Remove 'eshell-close-handles'.

* lisp/eshell/esh-var.el (eshell-parse-variable-ref): Reimplement
$<COMMAND> form using 'eshell-with-handles'.

* test/lisp/eshell/esh-cmd-tests.el
(esh-cmd-test/command-not-found/pipeline): New test.

* test/lisp/eshell/em-tramp-tests.el
(em-tramp-test/should-replace-command): Adjust check for
'eshell-with-copied-handles'.

(cherry picked from commit 50339b38fdef31982744bdb8b51838012dd4ef47)

lisp/eshell/esh-cmd.el
lisp/eshell/esh-io.el
lisp/eshell/esh-proc.el
lisp/eshell/esh-var.el
test/lisp/eshell/em-tramp-tests.el
test/lisp/eshell/esh-cmd-tests.el

index 43d536ac463d623b285a9450910bf59445ac8945..0a68859fc0a71a8da4fd6d0cb8cc7c6f93c0cd0d 100644 (file)
@@ -415,7 +415,6 @@ command hooks should be run before and after the command."
      ;; The last command (first in our reversed list) is implicitly
      ;; terminated by ";".
      (sep-terms (cons ";" sep-terms))
-     (steal-handles t)
      (commands
       (nreverse
        (mapcan
@@ -428,11 +427,8 @@ command hooks should be run before and after the command."
               (unless eshell-in-pipeline-p
                 (setq cmd `(eshell-do-command ,cmd)))
               ;; Copy I/O handles so each full statement can manipulate
-              ;; them if they like.  Steal the handles for the last
-              ;; command (first in our reversed list); we won't use the
-              ;; originals again anyway.
-              (setq cmd `(eshell-with-copied-handles ,cmd ,steal-handles)
-                    steal-handles nil)
+              ;; them if they like.
+              (setq cmd `(eshell-with-copied-handles ,cmd))
               (when (equal sep "&")
                 (setq cmd `(eshell-do-subjob ,cmd)))
               (list cmd))))
@@ -547,10 +543,8 @@ implemented via rewriting, rather than as a function."
              (let ((,(intern (cadr terms)) (car ,for-items))
                   (eshell--local-vars (cons ',(intern (cadr terms))
                                              eshell--local-vars)))
-              (eshell-protect
-               ,(eshell-invokify-arg body t)))
-             (setq ,for-items (cdr ,for-items)))
-           (eshell-close-handles)))))
+              ,(eshell-invokify-arg body t))
+             (setq ,for-items (cdr ,for-items)))))))
 
 (defun eshell-structure-basic-command (func names keyword test body
                                            &optional else)
@@ -577,11 +571,8 @@ function."
               (string= keyword (cadr names))))
       (setq test `(not ,test)))
 
-  ;; finally, create the form that represents this structured
-  ;; command
-  `(progn
-     (,func ,test ,body ,else)
-     (eshell-close-handles)))
+  ;; Finally, create the form that represents this structured command.
+  `(,func ,test ,body ,else))
 
 (defun eshell-rewrite-while-command (terms)
   "Rewrite a `while' command into its equivalent Eshell command form.
@@ -593,8 +584,7 @@ must be implemented via rewriting, rather than as a function."
       (eshell-structure-basic-command
        'while '("while" "until") (car terms)
        (eshell-invokify-arg (cadr terms) nil t)
-       `(eshell-protect
-         ,(eshell-invokify-arg (car (last terms)) t)))))
+       (eshell-invokify-arg (car (last terms)) t))))
 
 (defun eshell-rewrite-if-command (terms)
   "Rewrite an `if' command into its equivalent Eshell command form.
@@ -606,12 +596,9 @@ must be implemented via rewriting, rather than as a function."
       (eshell-structure-basic-command
        'if '("if" "unless") (car terms)
        (eshell-invokify-arg (cadr terms) nil t)
-       `(eshell-protect
-         ,(eshell-invokify-arg (car (last terms (if (= (length terms) 4) 2)))
-                               t))
-       (if (= (length terms) 4)
-          `(eshell-protect
-             ,(eshell-invokify-arg (car (last terms)) t))))))
+       (eshell-invokify-arg (car (last terms (if (= (length terms) 4) 2))) t)
+       (when (= (length terms) 4)
+         (eshell-invokify-arg (car (last terms)) t)))))
 
 (defun eshell-set-exit-info (status &optional result)
   "Set the exit status and result for the last command.
@@ -662,8 +649,7 @@ This means an exit code of 0."
       (cl-assert (car sep-terms))
       (setq final (eshell-structure-basic-command
                    'if (string= (pop sep-terms) "&&") "if"
-                   `(eshell-protect ,(pop results))
-                   `(eshell-protect ,final))))
+                   (pop results) final)))
     final))
 
 (defun eshell-parse-subcommand-argument ()
@@ -762,6 +748,28 @@ if none)."
 ;; that `eshell-do-eval' will evaluated, such as command rewriting
 ;; hooks (see `eshell-rewrite-command-hook' and friends).
 
+(defmacro eshell-with-handles (handle-args &rest body)
+  "Create a new set of I/O handles and evaluate BODY.
+HANDLE-ARGS is a list of arguments to pass to `eshell-create-handles'.
+After evaluating BODY, automatically release the handles, allowing them
+to close."
+  (declare (indent 1))
+  `(let ((eshell-current-handles (eshell-create-handles ,@handle-args)))
+     (unwind-protect
+         ,(if (length= body 1) (car body) `(progn ,@body))
+       (eshell-close-handles))))
+
+(defmacro eshell-with-copied-handles (&rest body)
+  "Copy the current I/O handles and evaluate BODY.
+After evaluating BODY, automatically release the handles, allowing them
+to close."
+  (declare (indent 0))
+  `(let ((eshell-current-handles
+          (eshell-duplicate-handles eshell-current-handles)))
+     (unwind-protect
+         ,(if (length= body 1) (car body) `(progn ,@body))
+       (eshell-close-handles))))
+
 (defmacro eshell-do-subjob (object)
   "Evaluate a command OBJECT as a subjob.
 We indicate that the process was run in the background by
@@ -774,10 +782,9 @@ returning it as (:eshell-background . PROCESSES)."
 
 (defmacro eshell-commands (object &optional silent)
   "Place a valid set of handles, and context, around command OBJECT."
-  `(let ((eshell-current-handles
-         (eshell-create-handles ,(not silent) 'append))
-        eshell-current-subjob-p)
-     ,object))
+  `(let (eshell-current-subjob-p)
+     (eshell-with-handles (,(not silent) 'append)
+       ,object)))
 
 (defvar eshell-this-command-hook nil)
 
@@ -796,8 +803,7 @@ this grossness will be made to disappear by using `call/cc'..."
            (mapc #'funcall eshell-this-command-hook)))
      (error
       (eshell-errorn (error-message-string err))
-      (eshell-set-exit-info 1)
-      (eshell-close-handles))))
+      (eshell-set-exit-info 1))))
 
 (define-obsolete-function-alias 'eshell-trap-errors #'eshell-do-command "31.1")
 
@@ -806,19 +812,12 @@ this grossness will be made to disappear by using `call/cc'..."
 If the wrapped form returns a process (or list thereof), Eshell will
 wait for completion in the background for the process(es) to complete.")
 
-(defmacro eshell-with-copied-handles (object &optional steal-p)
-  "Duplicate current I/O handles, so OBJECT works with its own copy.
-If STEAL-P is non-nil, these new handles will be stolen from the
-current ones (see `eshell-duplicate-handles')."
-  `(let ((eshell-current-handles
-          (eshell-duplicate-handles eshell-current-handles ,steal-p)))
-     ,object))
-
 (define-obsolete-function-alias 'eshell-copy-handles
   #'eshell-with-copied-handles "30.1")
 
 (defmacro eshell-protect (object)
   "Protect I/O handles, so they aren't get closed after eval'ing OBJECT."
+  (declare (obsolete nil "31.1"))
   `(progn
      (eshell-protect-handles eshell-current-handles)
      ,object))
@@ -845,9 +844,7 @@ This macro calls itself recursively, with NOTFIRST non-nil."
            `(eshell-set-output-handle ,eshell-output-handle
                                       'append (car next-procs)))
         (let ((proc ,(car pipeline)))
-          (cons proc next-procs)))
-      ;; Steal handles if this is the last item in the pipeline.
-      ,(null (cdr pipeline)))))
+          (cons proc next-procs))))))
 
 (defmacro eshell-do-pipelines-synchronously (pipeline)
   "Execute the commands in PIPELINE in sequence synchronously.
@@ -869,9 +866,7 @@ supported."
                   ;; meaning for synchronous processes: it's non-nil
                   ;; only when piping *to* a process.
                   (eshell-in-pipeline-p ,(and (cdr pipeline) t)))
-              ,(car pipeline)))
-          ;; Steal handles if this is the last item in the pipeline.
-          ,(null (cdr pipeline)))
+              ,(car pipeline))))
        ,(when (cdr pipeline)
           `(eshell-do-pipelines-synchronously (quote ,(cdr pipeline)))))))
 
@@ -946,7 +941,7 @@ A command can be invoked directly if all of the following are true:
 
 * The command is of the form
   (eshell-with-copied-handles
-   (eshell-do-command (eshell-named-command NAME [ARGS])) _).
+   (eshell-do-command (eshell-named-command NAME [ARGS]))).
 
 * NAME is a string referring to an alias function and isn't a
   complex command (see `eshell-complex-commands').
@@ -954,8 +949,7 @@ A command can be invoked directly if all of the following are true:
 * Any subcommands in ARGS can also be invoked directly."
   (pcase command
     (`(eshell-with-copied-handles
-       (eshell-do-command (eshell-named-command ,name . ,args))
-       ,_)
+       (eshell-do-command (eshell-named-command ,name . ,args)))
      (and name (stringp name)
          (not (member name eshell-complex-commands))
          (catch 'simple
@@ -1557,7 +1551,7 @@ a string naming a Lisp function."
                     (not result))
            2)
          result))
-      (eshell-close-handles))))
+      nil)))
 
 (define-obsolete-function-alias 'eshell-lisp-command* #'eshell-lisp-command
   "31.1")
index 5fccc4fe82f08e8020db67b75f61ef22429fed03..a6df75e86e941afbadb53d663de7ded4136305c6 100644 (file)
@@ -353,14 +353,14 @@ calling this function)."
 (defun eshell-duplicate-handles (handles &optional steal-p)
   "Create a duplicate of the file handles in HANDLES.
 This uses the targets of each handle in HANDLES, incrementing its
-reference count by one (unless STEAL-P is non-nil).  These
-targets are shared between the original set of handles and the
-new one, so the targets are only closed when the reference count
-drops to 0 (see `eshell-close-handles').
+reference count by one.  These targets are shared between the original
+set of handles and the new one, so the targets are only closed when the
+reference count drops to 0 (see `eshell-close-handles').
 
 This function also sets the DEFAULT field for each handle to
 t (see `eshell-create-handles').  Unlike the targets, this value
 is not shared with the original handles."
+  (declare (advertised-calling-convention (handles) "31.1"))
   (let ((dup-handles (make-vector eshell-number-of-handles nil)))
     (dotimes (idx eshell-number-of-handles)
       (when-let ((handle (aref handles idx)))
index dc7b497666be4b86dbe0a147f65e9834e58b06da..b579a93e14c5a64d0c8db020ae3283e31f38f980 100644 (file)
@@ -371,6 +371,7 @@ Used only on systems which do not support async subprocesses.")
                          #'eshell-insertion-filter)
                :sentinel #'eshell-sentinel))
         (eshell-record-process-properties stderr-proc eshell-error-handle))
+      (eshell-protect-handles eshell-current-handles)
       (setq proc
             (let ((command (file-local-name (expand-file-name command)))
                   (conn-type (pcase (bound-and-true-p eshell-in-pipeline-p)
@@ -468,7 +469,6 @@ Used only on systems which do not support async subprocesses.")
         (eshell-set-exit-info
          (if (numberp exit-status) exit-status -1)
          (and (numberp exit-status) (= exit-status 0)))
-        (eshell-close-handles)
        (run-hook-with-args 'eshell-kill-hook command exit-status)
        (or (bound-and-true-p eshell-in-pipeline-p)
            (setq eshell-last-sync-output-start nil))
index f0270aca92c853d7663e898a56b71e5e4ba00af1..0e6c01d27741e4949f8ec87e30c6d17c496dd642 100644 (file)
@@ -553,24 +553,22 @@ Possible variable references are:
              (subcmd (or (eshell-unescape-inner-double-quote end)
                          (cons (point) end))))
         (prog1
-            `(let ((eshell-current-handles
-                    (eshell-create-handles ,temp 'overwrite)))
-               (progn
-                 (eshell-as-subcommand
-                  ,(let ((eshell-current-quoted nil))
-                     (eshell-parse-command subcmd)))
-                 (ignore
-                  (nconc eshell-this-command-hook
-                         ;; Quote this lambda; it will be evaluated by
-                         ;; `eshell-do-eval', which requires very
-                         ;; particular forms in order to work
-                         ;; properly.  See bug#54190.
-                         (list (function
-                                (lambda ()
-                                  (delete-file ,temp)
-                                  (when-let ((buffer (get-file-buffer ,temp)))
-                                    (kill-buffer buffer)))))))
-                 (eshell-apply-indices ,temp indices ,eshell-current-quoted)))
+            `(eshell-with-handles (,temp 'overwrite)
+               (eshell-as-subcommand
+                ,(let ((eshell-current-quoted nil))
+                   (eshell-parse-command subcmd)))
+               (ignore
+                (nconc eshell-this-command-hook
+                       ;; Quote this lambda; it will be evaluated by
+                       ;; `eshell-do-eval', which requires very
+                       ;; particular forms in order to work
+                       ;; properly.  See bug#54190.
+                       (list (function
+                              (lambda ()
+                                (delete-file ,temp)
+                                (when-let ((buffer (get-file-buffer ,temp)))
+                                  (kill-buffer buffer)))))))
+               (eshell-apply-indices ,temp indices ,eshell-current-quoted))
           (goto-char (1+ end))))))
    ((eq (char-after) ?\()
     (condition-case nil
index 49dd5a78c3d67d5ee700236c010822d699734a4d..b85dfc61580cf886584c8da24729b84e802d6d5b 100644 (file)
@@ -29,8 +29,7 @@
   `(should (equal
             (catch 'eshell-replace-command ,form)
             (list 'eshell-with-copied-handles
-                  (list 'eshell-do-command ,replacement)
-                  t))))
+                  (list 'eshell-do-command ,replacement)))))
 
 (ert-deftest em-tramp-test/su-default ()
   "Test Eshell `su' command with no arguments."
index 18ea1f9a9d66bd952214d2c00d3f5be23cc77d26..cac349a2616a08a07d1fd9bf3cb7985ac7eeaeb3 100644 (file)
@@ -558,6 +558,17 @@ NAME is the name of the test case."
    ;; Make sure we can call another command after throwing.
    (eshell-match-command-output "echo again" "\\`again\n")))
 
+(ert-deftest esh-cmd-test/command-not-found/pipeline ()
+  "Ensure that processes are stopped if a command in a pipeline is not found."
+  (skip-when (or (not (executable-find "cat"))
+                 (executable-find "nonexist")))
+  (with-temp-eshell
+    (let ((starting-process-list (process-list)))
+      (eshell-match-command-output "nonexist | *cat"
+                                   "\\`nonexist: command not found\n")
+      (eshell-wait-for-subprocess t)
+      (should (equal (process-list) starting-process-list)))))
+
 \f
 ;; `which' command