aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-08-25 05:28:25 -0600
committerCraig Jennings <c@cjennings.net>2026-08-25 05:28:25 -0600
commit638310601a39e7f9ade59ea85794a21167905f31 (patch)
tree59ebb07880634aaf2084cf4917b09aadc1cd7068
parentac1d034d09dd37305e84d08438fef268ed8173bc (diff)
downloaddotemacs-638310601a39e7f9ade59ea85794a21167905f31.tar.gz
dotemacs-638310601a39e7f9ade59ea85794a21167905f31.zip
fix(startup): keep *Warnings* alive so 31.1's first frame is Wayland
Emacs 31.1's warnings.el defers daemon-startup warnings into a one-shot after-make-frame-functions closure that holds the *Warnings* buffer object and displays it on the first client frame. The startup sweep in cj/dashboard-only killed that buffer first. On the first emacsclient -c of every session, display-buffer then signaled inside make-frame and server.el answered -window-system-unsupported. emacsclient silently retried on $DISPLAY, and the frame opened on XWayland behind the pgtk "unsupported under X" dialog. Only that first frame was affected, which made it look like a display problem rather than a config one. I added *Warnings* to cj/undead-buffer-list so the sweep buries it instead, the same choice desktop.el makes in desktop-clear-preserve-buffers. Two things follow. C-x k on *Warnings* now buries it. A startup that produced warnings shows them on the first frame, because the deferred closure finally has a live buffer. As a backstop, cj/warning--display-buffer-if-live wraps warning--display-buffer so a dead buffer (or a name that resolves to none) is skipped rather than passed on. That function only exists from Emacs 31, and advising the undefined symbol on 30.2 is harmless. The new tests pin *Warnings* to the undead list and check the sweep leaves it live. The guard tests cover live and dead buffers plus names that do and don't resolve, and run the real 31.x function on a dead buffer.
-rw-r--r--modules/system-defaults.el24
-rw-r--r--modules/undead-buffers.el11
-rw-r--r--tests/test-system-defaults--warning-display-dead-buffer.el103
-rw-r--r--tests/test-undead-buffers--warnings-undead.el63
4 files changed, 200 insertions, 1 deletions
diff --git a/modules/system-defaults.el b/modules/system-defaults.el
index d9ec1878..47bd1505 100644
--- a/modules/system-defaults.el
+++ b/modules/system-defaults.el
@@ -89,6 +89,30 @@ indicate the warning was handled."
(advice-add 'display-warning :before-until #'cj/log-comp-warning)
+;; ------------------ Deferred Daemon Warnings vs. Frame Creation -----------------
+
+;; Emacs 31's warnings.el defers warnings raised during daemon startup: it puts
+;; a one-shot closure on `after-make-frame-functions' holding the *Warnings*
+;; buffer object and calls `warning--display-buffer' on it when the first
+;; client frame is made. If that buffer died in between, `display-buffer'
+;; signals inside `make-frame', server.el reports "-window-system-unsupported",
+;; and emacsclient retries on $DISPLAY -- the first frame of the session
+;; silently opens on XWayland. Keeping *Warnings* alive is the root fix
+;; (undead-buffers.el); this guard is the backstop, so no future buffer sweep
+;; can break frame creation the same way. The function only exists from
+;; Emacs 31; advising an undefined symbol is harmless and takes effect once
+;; warnings.el defines it.
+
+(defun cj/warning--display-buffer-if-live (orig buffer)
+ "Call ORIG with BUFFER only when it names or is a live buffer.
+Around advice for `warning--display-buffer'. BUFFER may be a buffer object
+or a buffer name, like `display-buffer' accepts. Return nil when skipped."
+ (let ((buf (and buffer (get-buffer buffer))))
+ (when (buffer-live-p buf)
+ (funcall orig buf))))
+
+(advice-add 'warning--display-buffer :around #'cj/warning--display-buffer-if-live)
+
;; ---------------------------------- Unicode ----------------------------------
(set-locale-environment "en_US.UTF-8")
diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el
index e5b8dc00..21232e50 100644
--- a/modules/undead-buffers.el
+++ b/modules/undead-buffers.el
@@ -31,7 +31,16 @@
(defvar cj/undead-buffer-list
'("*scratch*" "*EMMS-Playlist*" "*Messages*" "*ert*"
- "*AI-Assistant*")
+ "*AI-Assistant*"
+ ;; *Warnings* stays alive because Emacs 31's warnings.el defers daemon
+ ;; startup warnings into an `after-make-frame-functions' closure that
+ ;; holds this buffer object until the first client frame. The startup
+ ;; sweep in `cj/dashboard-only' used to kill it; the closure then failed
+ ;; inside `make-frame', server.el reported the window system as
+ ;; unsupported, and emacsclient silently retried on $DISPLAY, so the first
+ ;; frame of every 31.1 session opened on XWayland. I bury it instead, the
+ ;; same choice desktop.el makes in `desktop-clear-preserve-buffers'.
+ "*Warnings*")
"Buffer names to bury instead of killing (exact match).")
(defvar cj/undead-buffer-regexps nil
diff --git a/tests/test-system-defaults--warning-display-dead-buffer.el b/tests/test-system-defaults--warning-display-dead-buffer.el
new file mode 100644
index 00000000..48c7797e
--- /dev/null
+++ b/tests/test-system-defaults--warning-display-dead-buffer.el
@@ -0,0 +1,103 @@
+;;; test-system-defaults--warning-display-dead-buffer.el --- Dead-buffer guard on deferred warnings -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Emacs 31.1's warnings.el defers daemon-startup warnings into a one-shot
+;; `after-make-frame-functions' closure that calls `warning--display-buffer'
+;; on the first client frame with the *Warnings* buffer object it captured at
+;; warning time. If anything killed that buffer in between, `display-buffer'
+;; signals inside `make-frame', server.el swallows the error as
+;; "-window-system-unsupported", and emacsclient silently retries on $DISPLAY:
+;; the session's first frame lands on XWayland.
+;;
+;; The root fix keeps *Warnings* alive (undead-buffers.el). This is the
+;; defense in depth: `cj/warning--display-buffer-if-live' wraps
+;; `warning--display-buffer' so a dead buffer is skipped rather than passed
+;; on. Load happens once in the shared sandbox (testutil-system-defaults.el);
+;; `warning--display-buffer' only exists from Emacs 31, so the end-to-end case
+;; skips on older builds while the pure-function cases always run.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(require 'testutil-system-defaults)
+
+(test-system-defaults--with-load-environment
+ (test-system-defaults--load))
+
+(defun test-system-defaults--recording-orig ()
+ "Return (ORIG . CALLS) where ORIG records every argument into CALLS."
+ (let ((calls (list nil)))
+ (cons (lambda (buffer)
+ (push buffer (car calls))
+ 'displayed)
+ calls)))
+
+;;; Normal Cases
+
+(ert-deftest test-system-defaults-warning-guard-passes-live-buffer-through ()
+ "Normal: a live buffer reaches the original and its value is returned."
+ (let* ((rec (test-system-defaults--recording-orig))
+ (buf (generate-new-buffer " *warning-guard-live*")))
+ (unwind-protect
+ (progn
+ (should (eq 'displayed
+ (cj/warning--display-buffer-if-live (car rec) buf)))
+ (should (equal (list buf) (car (cdr rec)))))
+ (kill-buffer buf))))
+
+(ert-deftest test-system-defaults-warning-guard-is-installed ()
+ "Normal: loading system-defaults installs the guard on the deferred display.
+From Emacs 31 the advised symbol must actually be defined: pending advice on
+an undefined symbol would still count as installed, so a rename upstream
+would otherwise silently disable the backstop."
+ (should (advice-member-p #'cj/warning--display-buffer-if-live
+ 'warning--display-buffer))
+ (when (>= emacs-major-version 31)
+ (should (fboundp 'warning--display-buffer))))
+
+(ert-deftest test-system-defaults-warning-guard-resolves-live-buffer-name ()
+ "Normal: a live buffer's name is resolved and passed through as the buffer."
+ (let* ((rec (test-system-defaults--recording-orig))
+ (buf (generate-new-buffer " *warning-guard-named*")))
+ (unwind-protect
+ (progn
+ (should (eq 'displayed
+ (cj/warning--display-buffer-if-live
+ (car rec) (buffer-name buf))))
+ (should (equal (list buf) (car (cdr rec)))))
+ (kill-buffer buf))))
+
+;;; Boundary Cases
+
+(ert-deftest test-system-defaults-warning-guard-skips-killed-buffer ()
+ "Boundary: a killed buffer never reaches the original; result is nil."
+ (let* ((rec (test-system-defaults--recording-orig))
+ (buf (generate-new-buffer " *warning-guard-dead*")))
+ (kill-buffer buf)
+ (should-not (cj/warning--display-buffer-if-live (car rec) buf))
+ (should-not (car (cdr rec)))))
+
+(ert-deftest test-system-defaults-warning-guard-end-to-end-dead-buffer-does-not-signal ()
+ "Boundary: the real deferred display survives a dead buffer.
+Mirrors the live failure: a string condition in `display-buffer-alist' is
+what `buffer-match-p' tripped over when the buffer name came back nil."
+ (skip-unless (fboundp 'warning--display-buffer))
+ (let ((buf (generate-new-buffer "*Warnings*"))
+ (display-buffer-alist '(("^ \\*test-guard\\*" display-buffer-no-window))))
+ (kill-buffer buf)
+ (should-not (warning--display-buffer buf))))
+
+;;; Error Cases
+
+(ert-deftest test-system-defaults-warning-guard-rejects-non-buffer ()
+ "Error: nil, or a name that resolves to no buffer, is skipped without a signal."
+ (let ((rec (test-system-defaults--recording-orig))
+ (missing " *warning-guard-no-such-buffer*"))
+ (when (get-buffer missing) (kill-buffer missing))
+ (should-not (cj/warning--display-buffer-if-live (car rec) nil))
+ (should-not (cj/warning--display-buffer-if-live (car rec) missing))
+ (should-not (car (cdr rec)))))
+
+(provide 'test-system-defaults--warning-display-dead-buffer)
+;;; test-system-defaults--warning-display-dead-buffer.el ends here
diff --git a/tests/test-undead-buffers--warnings-undead.el b/tests/test-undead-buffers--warnings-undead.el
new file mode 100644
index 00000000..858dbf76
--- /dev/null
+++ b/tests/test-undead-buffers--warnings-undead.el
@@ -0,0 +1,63 @@
+;;; test-undead-buffers--warnings-undead.el --- *Warnings* survives the buffer sweep -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Emacs 31.1's warnings.el defers daemon-startup warnings into a one-shot
+;; `after-make-frame-functions' closure that holds the *Warnings* buffer
+;; object and displays it on the first client frame. Killing that buffer
+;; during startup leaves the closure holding a dead buffer, `display-buffer'
+;; then signals inside `make-frame', server.el reports the window system as
+;; unsupported, and emacsclient silently retries on $DISPLAY -- the first
+;; frame of the session lands on XWayland instead of Wayland.
+;;
+;; `cj/dashboard-only' on `emacs-startup-hook' runs
+;; `cj/kill-all-other-buffers-and-windows', which is exactly such a sweep.
+;; These tests pin *Warnings* to the undead list so the sweep buries it
+;; instead of killing it. Error-path coverage of the predicate itself (a nil
+;; or non-string name) lives in test-undead-buffers--buffer-undead-p.el.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'undead-buffers)
+
+;;; Normal Cases
+
+(ert-deftest test-undead-buffers-warnings-is-undead-by-default ()
+ "Normal: the module's default list makes *Warnings* bury-only."
+ (should (member "*Warnings*" cj/undead-buffer-list))
+ (should (cj/--buffer-undead-p "*Warnings*")))
+
+(ert-deftest test-undead-buffers-warnings-survives-kill-all-other-buffers ()
+ "Normal: the startup sweep buries *Warnings* rather than killing it.
+This is the sweep `cj/dashboard-only' runs from `emacs-startup-hook'."
+ (delete-other-windows)
+ (unwind-protect
+ (let* ((main (current-buffer))
+ (existing (get-buffer "*Warnings*"))
+ (warnings (or existing (get-buffer-create "*Warnings*")))
+ (victim (generate-new-buffer "*test-sweep-victim*")))
+ (unwind-protect
+ (progn
+ (cj/kill-all-other-buffers-and-windows)
+ (should (buffer-live-p main))
+ (should (buffer-live-p warnings))
+ (should-not (buffer-live-p victim)))
+ (when (buffer-live-p victim) (kill-buffer victim))
+ ;; Only remove what this test created. `kill-buffer' the function
+ ;; is not the remapped command, so the undead list doesn't apply.
+ (when (and (not existing) (buffer-live-p warnings))
+ (kill-buffer warnings))))
+ (delete-other-windows)))
+
+;;; Boundary Cases
+
+(ert-deftest test-undead-buffers-warnings-match-is-exact ()
+ "Boundary: only the exact name is undead; a uniquified *Warnings*<2> is not.
+The list matches exact names, so a second warnings buffer made by
+`generate-new-buffer' is an ordinary buffer to the sweep."
+ (should-not (cj/--buffer-undead-p "*Warnings*<2>"))
+ (should-not (cj/--buffer-undead-p " *Warnings*")))
+
+(provide 'test-undead-buffers--warnings-undead)
+;;; test-undead-buffers--warnings-undead.el ends here