From 994b6e9afe5aec8fae82731f7addf72f84772bbb Mon Sep 17 00:00:00 2001 From: Chris Hodapp Date: Mon, 7 Sep 2026 22:08:43 -0700 Subject: [PATCH] fix: give each daemon child a private Emacs server name A :daemon child ran with a bare --fg-daemon, so its server took the default name "server". Emacs starts a daemon's server after the command line is processed and refuses to start at all when that name is already held, so the child said hello over the parenting socket and then exited wherever an Emacs daemon was already running under the default name, which is the usual desktop setup. The same collision kept two daemon children from coexisting anywhere. CI never saw it because no daemon runs there. parenting-spawn now passes --fg-daemon=NAME, with NAME from make-temp-name on the child's process name, and accepts a string for :daemon to choose the name outright. Two tests cover it: two daemon children spawned side by side with distinct server names, neither of them "server", and a string :daemon reaching server-name. Co-Authored-By: Claude Fable 5.1 --- README.md | 5 ++++- parenting-parent.el | 18 +++++++++++++----- test/parenting-test.el | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9aa2938..6fc9238 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,10 @@ one), `:batch`/`:daemon` (see below), `:quick` (default t; nil drops Emacs (event loop, timers, frame machinery, real init with `:quick nil`) with no visible frame. Right for driving a new build off-screen. Promote it to visible later by evaluating a - `make-frame` form in it. + `make-frame` form in it. The child's Emacs server gets a private + name (an Emacs daemon refuses to start while another server holds + its name, so this keeps it clear of a daemon you already run); + pass a string instead of t to choose the name. - both nil — the child starts normally and opens a frame on the parent's display: trying the new build as a user, or watching a sandboxed agent's Emacs. diff --git a/parenting-parent.el b/parenting-parent.el index 65b2695..30e4590 100644 --- a/parenting-parent.el +++ b/parenting-parent.el @@ -349,10 +349,13 @@ connection closes; with DAEMON non-nil it runs with --fg-daemon instead — a full interactive Emacs with no visible frame, which you can promote later by evaluating a `make-frame' form in it; with both nil the child starts normally and shows a frame on the parent's -display. With QUICK nil, -Q is dropped so the child starts with its -normal init files. NAME names the child process. Signal -`parenting-timeout' if the child has not connected after TIMEOUT -seconds. +display. A daemon child gets its own Emacs server name, so it does +not collide with any daemon already running under the default name +\(a daemon refuses to start when its server name is taken); DAEMON +may also be a string naming that server explicitly. With QUICK +nil, -Q is dropped so the child starts with its normal init files. +NAME names the child process. Signal `parenting-timeout' if the +child has not connected after TIMEOUT seconds. The remaining keywords exist for launching the child inside a sandbox or on another machine. COMMAND-WRAPPER is either a list of @@ -421,7 +424,12 @@ host network. See `parenting--sandbox-command-wrapper'." (list emacs) (and quick '("-Q")) (and batch '("--batch")) - (and daemon '("--fg-daemon")) + (and daemon + (list (concat "--fg-daemon=" + (if (stringp daemon) + daemon + (make-temp-name + (concat name "-")))))) args (cl-mapcan (lambda (dir) (list "-L" dir)) extra-load-path) diff --git a/test/parenting-test.el b/test/parenting-test.el index 1511416..5eca0ad 100644 --- a/test/parenting-test.el +++ b/test/parenting-test.el @@ -431,6 +431,30 @@ (should (> (parenting-eval conn '(length (frame-list))) 0))) (parenting-shutdown conn)))) +(ert-deftest parenting-spawn-daemon-children-do-not-collide () + ;; Each daemon child names its own Emacs server: an Emacs daemon + ;; refuses to start when its server name is already taken, so two + ;; children under the default name (or one beside the user's own + ;; daemon) could not coexist. + (let ((a (parenting-spawn :daemon t :timeout 60))) + (unwind-protect + (let ((b (parenting-spawn :daemon t :timeout 60))) + (unwind-protect + (progn + (should (eq t (parenting-eval b '(and (daemonp) t)))) + (should-not (equal "server" (parenting-eval a 'server-name))) + (should-not (equal (parenting-eval a 'server-name) + (parenting-eval b 'server-name)))) + (parenting-shutdown b))) + (parenting-shutdown a)))) + +(ert-deftest parenting-spawn-daemon-name-is-honored () + (let ((conn (parenting-spawn :daemon "parenting-test-named" :timeout 60))) + (unwind-protect + (should (equal "parenting-test-named" + (parenting-eval conn 'server-name))) + (parenting-shutdown conn)))) + (ert-deftest parenting-spawn-rejects-batch-plus-daemon () (should-error (parenting-spawn :batch t :daemon t)))