Subject: 31.0.50; Synchronous man does not handle error cases properly

From: Jens Schmidt <[email-masked]>
Date: Sat, 12 Apr 2025 00:19:27 +0200

This is about some deficiencies with the user option
`Man-prefer-synchronous-call', which got added in Emacs 30.  So it is in
fact also a bug in Emacs 30.  (Thanks to Sebastian for adding that new
option, prior to Emacs 30 I have been using my private hacks to get
synchronous man page invocations.)

Anyway, when using a non-nil value for that option, some edge/error
cases behave awkwardly:

1. ./src/emacs -Q --eval '(setq Man-prefer-synchronous-call t)'

   M-x man RET foobarbaz RET

   Results in the error "Can't find the foobarbaz manpage" (OK) with the
   minibuffer window being selected (not OK).

   The minibuffer being selected is IMO a result of the following form
   from function `Man-bgproc-sentinel':

     (let ((old-window (old-selected-window)))
       (when (window-live-p old-window)
         (select-window old-window)))

   IIUC, that form (or its predecessor) got added by Juri in bug#38164,
   commit 611b85b6 to keep the minibuffer selected when an async `man'
   fails at the time when the user is operating in the minibuffer.

   Unfortunately, that hack ensures that the minibuffer is *always*
   selected if a synchronous `man' fails.

2. ./src/emacs -Q --eval '(setq Man-prefer-synchronous-call t)'

   M-x man RET -k foobarbaz RET

   Results in the error "Can't find the k foobarbaz manpage" (not OK),
   again with the minibuffer window being selected (not OK).

   With async `man' this results in the error message "-k foobarbaz: no
   matches", which I consider OK.

   Here the problem is IMO that the following branch to detect failing
   "man -k" calls in function `Man-bgproc-sentinel':

     ((and (string-match "\\(\\`\\|\\s-\\)-k\\s-" Man-arguments)
           (eq (process-status process) 'exit)
           (= (process-exit-status process) 0)
           (= (point-min) (point-max)))
      (setq message (format "%s: no matches" Man-arguments)
            delete-buff t))

   never gets selected in the synchronous case since the `(eq
   (process-status process) 'exit)' form always evaluates to nil for
   that case.

The attached tentative patch would fix both issues.

What do you think?

In GNU Emacs 31.0.50 (build 25, x86_64-pc-linux-gnu, GTK+ Version
 3.24.38, cairo version 1.16.0) of 2025-04-11 built on sappc2
Repository revision: 695edc5b55bec645fe2df8924513826b202022b6
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12201009
System Description: Debian GNU/Linux 12 (bookworm)

Configured using:
 'configure --with-native-compilation --with-mailutils'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NATIVE_COMP
NOTIFY INOTIFY PDUMPER PNG RSVG SECCOMP SOUND THREADS TIFF
TOOLKIT_SCROLL_BARS WEBP X11 XDBE XIM XINERAMA XINPUT2 XPM XRANDR GTK3
ZLIB

Important settings:
  value of $LC_COLLATE: POSIX
  value of $LC_TIME: POSIX
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  minibuffer-regexp-mode: t
  line-number-mode: t
  indent-tabs-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr comp-run bytecomp byte-compile comp-common rx
emacsbug lisp-mnt message mailcap yank-media puny dired dired-loaddefs
rfc822 mml mml-sec password-cache epa derived epg rfc6068 epg-config
gnus-util text-property-search time-date subr-x mm-decode mm-bodies
mm-encode mail-parse rfc2231 mailabbrev gmm-utils mailheader cl-loaddefs
cl-lib sendmail rfc2047 rfc2045 ietf-drums mm-util mail-prsvr mail-utils
rmc iso-transl tooltip cconv eldoc paren electric uniquify ediff-hook
vc-hooks lisp-float-type elisp-mode mwheel term/x-win x-win
term/common-win x-dnd touch-screen tool-bar dnd fontset image regexp-opt
fringe tabulated-list replace newcomment text-mode lisp-mode prog-mode
register page tab-bar menu-bar rfn-eshadow isearch easymenu timer select
scroll-bar mouse jit-lock font-lock syntax font-core term/tty-colors
frame minibuffer nadvice seq simple cl-generic indonesian philippine
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite emoji-zwj charscript
charprop case-table epa-hook jka-cmpr-hook help abbrev obarray oclosure
cl-preloaded button loaddefs theme-loaddefs faces cus-face macroexp
files window text-properties overlay sha1 md5 base64 format env
code-pages mule custom widget keymap hashtable-print-readable backquote
threads dbusbind inotify lcms2 dynamic-setting system-font-setting
font-render-setting cairo gtk x-toolkit xinput2 x multi-tty move-toolbar
make-network-process tty-child-frames native-compile emacs)

Memory information:
((conses 16 58466 9024) (symbols 48 6197 0) (strings 32 15326 1600)
 (string-bytes 1 470967) (vectors 16 9931)
 (vector-slots 8 139569 9476) (floats 8 22 13) (intervals 56 242 0)
 (buffers 984 10))
From cde6fca80beeac50112c4a9da15ca08c7b8cc211 Mon Sep 17 00:00:00 2001
From: Jens Schmidt <[email-masked]>
Date: Sat, 12 Apr 2025 00:02:56 +0200
Subject: [PATCH] Better handle errors after sync man invocations

* lisp/man.el (Man-start-calling): Declare as debuggable.
(Man-getpage-in-background): Call `Man-bgproc-sentinel' with a
cons (BUFFER . EXIT-STATUS) as PROCESS argument for synchronous calls.
(Man-bgproc-sentinel): Use that information to handle those more
similarly to asynchronous calls.  Do not employ window selection hacks
for synchronous calls.  (Bug#XXXXX)
---
 lisp/man.el | 46 +++++++++++++++++++++++++++++-----------------
 1 file changed, 29 insertions(+), 17 deletions(-)

diff --git a/lisp/man.el b/lisp/man.el
index 4d5e8e323ca..d34d9154052 100644
--- a/lisp/man.el
+++ b/lisp/man.el
@@ -1166,6 +1166,7 @@ Man-columns
 
 (defmacro Man-start-calling (&rest body)
   "Start the man command in `body' after setting up the environment."
+  (declare (debug t))
   `(let ((process-environment (copy-sequence process-environment))
 	;; The following is so Awk script gets \n intact
 	;; But don't prevent decoding of the outside.
@@ -1253,7 +1254,7 @@ Man-getpage-in-background
 				    exit-status)))
 		 (setq msg exit-status))
 	     (man--maybe-fontify-manpage)
-	     (Man-bgproc-sentinel bufname msg))))))
+	     (Man-bgproc-sentinel (cons buffer exit-status) msg))))))
     buffer))
 
 (defun Man-update-manpage ()
@@ -1541,17 +1542,26 @@ Man-bgproc-sentinel
   "Manpage background process sentinel.
 When manpage command is run asynchronously, PROCESS is the process
 object for the manpage command; when manpage command is run
-synchronously, PROCESS is the name of the buffer where the manpage
-command is run.  Second argument MSG is the exit message of the
-manpage command."
-  (let ((Man-buffer (if (stringp process) (get-buffer process)
-		      (process-buffer process)))
+synchronously, PROCESS is a cons (BUFFER . EXIT-STATUS) of the buffer
+where the manpage command has run and the exit status of the manpage
+command.  Second argument MSG is the exit message of the manpage
+command."
+  (let ((asynchronous (processp process))
+        Man-buffer process-status exit-status
 	(delete-buff nil)
 	message)
 
+    (if asynchronous
+        (setq Man-buffer     (process-buffer process)
+              process-status (process-status process)
+              exit-status    (process-exit-status process))
+      (setq Man-buffer     (car process)
+            process-status 'exit
+            exit-status    (cdr process)))
+
     (if (not (buffer-live-p Man-buffer)) ;; deleted buffer
-	(or (stringp process)
-	    (set-process-buffer process nil))
+	(and asynchronous
+	     (set-process-buffer process nil))
 
       (with-current-buffer Man-buffer
 	(save-excursion
@@ -1570,15 +1580,14 @@ Man-bgproc-sentinel
 		  ;; `Man-highlight-references'.  The \\s- bits here are
 		  ;; meant to allow for multiple options with -k among them.
 		  ((and (string-match "\\(\\`\\|\\s-\\)-k\\s-" Man-arguments)
-			(eq (process-status process) 'exit)
-			(= (process-exit-status process) 0)
+			(eq process-status 'exit)
+			(= exit-status 0)
 			(= (point-min) (point-max)))
 		   (setq message (format "%s: no matches" Man-arguments)
 			 delete-buff t))
 
-		  ((or (stringp process)
-		       (not (and (eq (process-status process) 'exit)
-				 (= (process-exit-status process) 0))))
+		  ((not (and (eq process-status 'exit)
+			     (= exit-status 0)))
 		   (or (zerop (length msg))
 		       (progn
 			 (setq message
@@ -1630,10 +1639,13 @@ Man-bgproc-sentinel
             (progn
               (quit-restore-window
                (get-buffer-window Man-buffer t) 'kill)
-              ;; Ensure that we end up in the correct window.
-              (let ((old-window (old-selected-window)))
-                (when (window-live-p old-window)
-                  (select-window old-window))))
+              ;; Ensure that we end up in the correct window.  Which is
+              ;; only relevant in rather special cases and if we have
+              ;; been called in an asynchronous fashion, see bug#38164.
+              (and asynchronous
+                   (let ((old-window (old-selected-window)))
+                     (when (window-live-p old-window)
+                       (select-window old-window)))))
           (kill-buffer Man-buffer)))
 
       (when message
-- 
2.39.5