Subject: bug#79079: 31.0.50; Piped command output is sometimes lost in Eshell

From: Jim Porter <[email-masked]>
Date: Fri, 15 Aug 2025 13:55:27 -0700

Attached is a patch for this with some regression tests (unfortunately, 
we can only run them in an interactive session, since Emacs will 
terminate itself in batch mode upon receiving a SIGPIPE). I've also 
called out the change in NEWS so that users are aware.

On 7/26/2025 2:12 PM, Jim Porter wrote:
> After examining this more closely, there are three problems with the 
> current Emacs code, of varying importance:
> 
> 1. When we receive an EPIPE, we don't call our process-filter one last 
> time (this is the original bug report). You can see this by running the 
> following Eshell command repeatedly until it produces no output:
> 
>    git log | head -n 10 | cat
> 
> 2. When we receive an EPIPE, we set the exit status to 256; that's not 
> right, since the exit status should be 0. You can see this by running 
> the following Eshell command until the prompt shows a non-zero status:
> 
>    git log | head -n 10 > #<my-buffer>
> 
> 3. If the child process closes stdin and wants to keep doing some work 
> (like write to stdout), it can't, since we called 'deactivate_process'. 
> You can see this with the attached script by running (it's supposed to 
> print the first line of "git log"):
> 
>    git log | ./test-script.sh
> 
> I've also attached two WIP patches for the different implementations. 
> "bad-call-filter.diff" uses 'read_process_output' to call our process 
> filter. It only solves issue #1 above. "good-close-pipe.diff" (which I 
> posted previously) solves all three issues. While the latter is a 
> slightly bigger change and requires some more testing on my end to make 
> sure I've covered 100% of the cases, I think it's the more-correct 
> solution. If we make the bigger change, it's probably worth calling it 
> out in NEWS and on emacs-devel so that people can be on the lookout for 
> any edge cases I haven't identified.

From d9eb9199e3f0c7f8bf1dc445047266fb48cb8678 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Fri, 15 Aug 2025 13:44:03 -0700
Subject: [PATCH] Allow child processes to continue after EPIPE

This ensures that if the child process closed its stdin and Emacs tries
to write to it, the process can still do any remaining work and exit
normally.  In practice, this can occur with commands like "head(1)"
(bug#79079).

* src/process.c (send_process): When encountering EPIPE, only close the
fd for the pipe to the child process's stdin.

* lisp/eshell/esh-io.el (eshell-output-object-to-target): Don't check
for process liveness anymore.

* test/src/process-tests.el (process-tests/broken-pipe): New function.
(process-tests/broken-pipe/pipe, process-tests/broken-pipe/pty)
(process-tests/broken-pipe/pipe-stdin)
(process-tests/broken-pipe/pty-stdin): New tests.

* etc/NEWS: Announce this change.
---
 etc/NEWS                  |  7 ++++++
 lisp/eshell/esh-io.el     | 14 +++++-------
 src/process.c             |  6 ++---
 test/src/process-tests.el | 47 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 61 insertions(+), 13 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 5b3be259196..5cd43872649 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2735,6 +2735,13 @@ See the new argument in 'make-thread'.  The default value allows the
 thread's current buffer to be killed by another thread.  This does not
 apply to the main thread's buffer.
 
+---
+** Emacs no longer kills child processes after EPIPE.
+Previously, Emacs would immediately kill a child process and set its
+exit status to 256 if sending input to that process returned EPIPE.
+Now when this happens, Emacs closes the file descriptor to write to the
+child process, but allows it to continue execution as normal.
+
 
 * Lisp Changes in Emacs 31.1
 
diff --git a/lisp/eshell/esh-io.el b/lisp/eshell/esh-io.el
index def51b0c0cb..d063ad42eea 100644
--- a/lisp/eshell/esh-io.el
+++ b/lisp/eshell/esh-io.el
@@ -744,15 +744,11 @@ eshell-output-object-to-target
   (condition-case err
       (process-send-string target object)
     (error
-     ;; If `process-send-string' raises an error and the process has
-     ;; finished, treat it as a broken pipe.  Otherwise, just re-raise
-     ;; the signal.  NOTE: When running Emacs in batch mode
-     ;; (e.g. during regression tests), Emacs can abort due to SIGPIPE
-     ;; here.  Maybe `process-send-string' should handle SIGPIPE even
-     ;; in batch mode (bug#66186).
-     (if (process-live-p target)
-         (signal (car err) (cdr err))
-       (signal 'eshell-pipe-broken (list target)))))
+     ;; NOTE: When running Emacs in batch mode (e.g. during regression
+     ;; tests), Emacs can abort due to SIGPIPE here.  Maybe
+     ;; `process-send-string' should handle SIGPIPE even in batch mode
+     ;; (bug#66186).
+     (signal 'eshell-pipe-broken (list target))))
   object)
 
 (cl-defmethod eshell-output-object-to-target (object
diff --git a/src/process.c b/src/process.c
index e61ec425f7e..5bc32a21fcf 100644
--- a/src/process.c
+++ b/src/process.c
@@ -6873,10 +6873,8 @@ send_process (Lisp_Object proc, const char *buf, ptrdiff_t len,
 		}
 	      else if (errno == EPIPE)
 		{
-		  p->raw_status_new = 0;
-		  pset_status (p, list2 (Qexit, make_fixnum (256)));
-		  p->tick = ++process_tick;
-		  deactivate_process (proc);
+		  close_process_fd(&p->open_fd[WRITE_TO_SUBPROCESS]);
+		  p->outfd = -1;
 		  error ("Process %s no longer connected to pipe; closed it",
 			 SDATA (p->name));
 		}
diff --git a/test/src/process-tests.el b/test/src/process-tests.el
index 0693bbe8924..3b6cb2a1674 100644
--- a/test/src/process-tests.el
+++ b/test/src/process-tests.el
@@ -1044,6 +1044,53 @@ process-sentinel-interrupt-event
       ;; ...and the change description should be "interrupt".
       (should (equal '("interrupt\n") events)))))
 
+(defun process-tests/broken-pipe (connection-type)
+  "Test handling of broken pipes; see bug#79079.
+This test runs a shell script that reads a line of text and closes
+stdin.  We send two lines of text to the script; the second should
+signal an error indicating that the pipe has been closed.  The script
+should also run to completion, printing out the line of text it read."
+  (with-temp-buffer
+    (let ((saw-error nil)
+          (proc (make-process
+                 :name "test" :buffer (current-buffer)
+                 :command '("sh" "-c"
+                            "read line; exec 0<&-; sleep 1; echo $line")
+                 :connection-type 'pipe)))
+      (process-send-string proc "hello\n")
+      (sit-for 0.1)
+      (condition-case err
+          (process-send-string proc "extra\n")
+        (error
+         (setq saw-error t)
+         (should (equal (error-message-string err)
+                        (concat "Process test no longer connected to pipe; "
+                                "closed it")))))
+      (unless saw-error
+        (ert-fail "Expected error from `process-send-string'"))
+      ;; Wait for the process to finish, and check results.
+      (while (eq (process-status proc) 'run)
+        (sit-for 0.1))
+      (should (equal (buffer-string) "hello\n\nProcess test finished\n"))
+      (should (eq (process-status proc) 'exit))
+      (should (eq (process-exit-status proc) 0)))))
+
+;; These tests only works when running Emacs interactively, since we
+;; don't catch SIGPIPE in batch mode.
+(when (and (not noninteractive)
+           (executable-find "sh"))
+  (ert-deftest process-tests/broken-pipe/pipe ()
+    (process-tests/broken-pipe 'pipe))
+
+  (ert-deftest process-tests/broken-pipe/pty ()
+    (process-tests/broken-pipe 'pty))
+
+  (ert-deftest process-tests/broken-pipe/pipe-stdin ()
+    (process-tests/broken-pipe '(pipe . pty)))
+
+  (ert-deftest process-tests/broken-pipe/pty-stdin ()
+    (process-tests/broken-pipe '(pty . pipe))))
+
 (ert-deftest process-num-processors ()
   "Sanity checks for num-processors."
   (should (equal (num-processors) (num-processors)))
-- 
2.25.1