Skip to content

Pass the child's exit status to ghostel-exit-functions - #746

Closed
a-min-7 wants to merge 1 commit into
dakra:mainfrom
a-min-7:feat/exit-status-hook-arg
Closed

a-min-7 wants to merge 1 commit into
dakra:mainfrom
a-min-7:feat/exit-status-hook-arg

Conversation

@a-min-7

@a-min-7 a-min-7 commented Oct 18, 2026 •

Copy link
Copy Markdown

ghostel-exit-functions runs before the buffer is reaped, which is the right place for a client to react to its own child dying — but on the native-PTY path the child's exit status never reached it. The native reaper writes the status as a bare number into the event pipe; ghostel--events-filter deletes the pipe without publishing it, so the sentinel surfaces "finished" and an rc=0 exit and an rc=1 exit are indistinguishable. This is the API gap behind herdr.el's takeover prompt, which currently re-reads the event stream through :around advice on ghostel--events-filter.

This records the status before the deletion and passes it to ghostel-exit-functions as a third argument, as agreed in #744.

New contract: (BUFFER EVENT STATUS).

  • STATUS is an integer, or nil when it cannot be determined.
  • A child killed by a signal reports 128 plus the signal number on both backends. process-exit-status alone reports the bare signal number on the Emacs-PTY path, so the sentinel normalizes it; the native reaper already used 128 plus the signal number.
  • Handlers that declare two arguments keep receiving (BUFFER EVENT): the runner checks func-arity per handler, so existing hooks (ghostel-eshell--visual-exit included) do not change.

Measured against a real refusal on herdr 0.9.1, native-PTY path, module built from this branch:

case EVENT STATUS
second herdr agent attach to a held pane (refused; stderr: terminal … already has an attached client; retry with --takeover) "finished\n" 1
clean herdr client exit "finished\n" 0
an attached client detaching normally "finished\n" 0

EVENT is identical in all three; STATUS is the distinguishing value.

This fixes the status half of #744 only; the stderr half remains open. The child's stderr still does not render into the VT — re-measured empty at hook time in the same run. The takeover prompt no longer needs it now that the status is authoritative, but it is a separate gap and is not addressed here.

Version floor for consumers. This adds a third hook argument, so a consumer that gates on the status should require a ghostel release containing this change. herdr.el's fork keeps its :around workaround on ghostel--events-filter until such a release exists; its Package-Requires then needs the corresponding ghostel minimum.

Test: ghostel-test-exec-exit-hook-receives-status runs rc=0, rc=1 and SIGTERM cases on both local PTY backends, pins the three-argument contract, and asserts that a two-argument handler is still called. It fails against the pre-change code, where the three-argument handler never receives a status.

Refs #744.

The native reaper already writes the child's exit status into the event
pipe, but ghostel--events-filter discarded it before deleting the pipe,
so the sentinel only saw "finished" and an rc=0 exit and an rc=1 exit
were indistinguishable to exit hooks.

Record the status as a process property before the deletion and pass it
to ghostel-exit-functions as a third argument. The status is nil when it
cannot be determined, and a signal death reports 128 plus the signal
number on both the native and Emacs-PTY paths, where process-exit-status
alone reports the bare signal number.

Handlers that declare two arguments keep the old (BUFFER EVENT)
contract; the sender checks func-arity per handler.

Closes dakra#744.

Session: ef458615-ef53-4c61-a763-6f1d0fdc2e4c
@dakra

dakra commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@a-min-7 thanks. Would you also be ok with PR #748 ?
I'm not strictly against a numeric 3rd argument (which I personally find cleaner too),
but Emacs process sentinels, compilation-finish-functions, and vterm-exit-functions all do the buffer+event args only and you have to compare the strings in the event argument.
PR #748 is therefore a lot smaller than this here (9 added lines vs 48).
wdyt?

@a-min-7

a-min-7 commented Oct 19, 2026

Copy link
Copy Markdown
Author

Fair enough on the convention — (BUFFER EVENT) is what sentinels and compilation-finish-functions do, and keeping the hook surface unchanged is the smaller change. I ran #748 on both backends with herdr 0.9.1 (current stable is 0.9.5): a refused attach via its nested-herdr guard (rc=1) gives "exited abnormally with code 1\n" and a clean exit gives "finished\n" on both, so the advice on ghostel--events-filter in herdr.el can go, and the split-status case reassembles correctly.

One thing before merging: the newline framing makes the Elisp require the new module. With a pre-#748 module the filter holds the bare number forever and never calls delete-process, so the sentinel never runs — no exit hook fires, and the buffer still looks live indefinitely (measured). MELPA users can get the new Elisp against the released 0.58.0 module, and the minimum-version check will not reject it. Could you either bump ghostel--minimum-module-version in the same change, or make the status path tolerate the un-terminated form?

One documentation point: "same strings on both backends" holds for normal exits only. SIGTERM gives "terminated: 15\n" on the Emacs PTY and "exited abnormally with code 143\n" on native (measured); SIGKILL/SIGINT follow from the same 128+N path (137/130), where Emacs says "killed: 9\n" / "interrupt: 2\n". Still distinguishable, but a consumer written against the Emacs vocabulary silently misses — worth a docstring or commit-message note, and a signal-case test would be ideal. A CHANGELOG entry would also match the recent commits.

Minor, though the newline framing makes it load-bearing: ghostel--event-buf is buffer-local but filters run in the caller's buffer, so holding the status there assumes the same buffer across reads; process-put on the pipe would be sturdier.

If #748 goes in with those addressed, I'll close #746 — the two-argument hook plus the synthesized strings already covers what prompted it (herdr telling a refused attach from a clean exit without reaching into internals), and the convention argument wins.

Environment:

Emacs    GNU Emacs 31.1 (build prefix /Users/amonecke/opt/emacs-31.1; invoked via /Users/amonecke/.local/bin/emacs)
Host     macOS 26.7.1, arm64, MacOSX27.0 SDK
Ghostel  19ff12b (PR head)
Module   Zig 0.16.0; zig build --prefix . -Dtarget=aarch64-macos.13.0 -Doptimize=ReleaseFast -Dcpu=baseline
         (the default native target fails identically on the base branch against this SDK; not #748)
Herdr    0.9.1 at test time

@dakra

dakra commented Oct 19, 2026

Copy link
Copy Markdown
Owner

OK, it's merged and I also triggered a new release.
Closing this PR then.

@dakra dakra closed this Oct 19, 2026
@a-min-7

a-min-7 commented Oct 19, 2026

Copy link
Copy Markdown
Author

Thanks — that was quick. Merged plus a release is more than I expected today; the two-argument hook is the smaller and more idiomatic change, and I'm happy to have lost that argument.

I'll close out my branch here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants