Repository navigation
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
mitchellh
left a comment
There was a problem hiding this comment.
Thanks for this. This is a real problem, but I don't love that we dig directly into the IO backend from embedded.zig. The intention of deinit is that we should be waiting for the process to exit, it shouldn't require a separate call and wait.
The ghostty/macos/Sources/Ghostty/Ghostty.Surface.swift Lines 26 to 36 in c45a762 But further, If you combine those two things (detached/async
I initially implemented a version of (1) in 2c46952ef198fc41036a7b3b4d6721919862ca57 because I also wanted to (re)use the There are some other related concerns with the a. We need (I think?) to sequentially free the surfaces due to the main thread requirement, rather than quickly signalling all of the child processes and then waiting on the batch. I then implemented a version of (2) in 75953d7ca761ce17f9a06e5197e0b81dad7c1855, by way of the new a. Add a "stop" function to And of course any other ideas that folks might have. |
0bce07d to
6a5f9ec
Compare
6a5f9ec to
f69782b
Compare
|
I'm sorry for asking but, is there any update on this? |
f69782b to
67f3b91
Compare
I just freshened it so it will cleanly merge with |
67f3b91 to
d864375
Compare
d864375 to
8110b65
Compare
8110b65 to
8da6010
Compare
|
@mitchellh what do you think of the
|
eda6cb5 to
9a44fc0
Compare
|
@mitchellh I reworked this around a new @bo2themax This also simplifies the macOS-specific changes. GTK and others could adopt something similar to |
9a44fc0 to
0bf1f75
Compare
|
@bo2themax rebased on top of #12742 |
|
@mitchellh I iterated on this some more, and I believe we can address the original issue by explicitly sending I verified this with both a delayed |
|
Would it work to simplify this and just send the hangup before the |
The child process termination in this case isn't initiated through our The root problem is that this deinit path is not reliably called when the macOS application terminates. Swift object finalizers are not guaranteed to run, and when My latest approach here ( |
|
If That's close to what I had originally on this branch, except I was more focused on cleaning up the surface resources then too. Or, maybe |
|
Okay okay I think I get it. Sorry I know you've been working on this a while so forgive the catch-up again.
Okay, alternate idea. Would it be cleaner to make surfaces have a free-path that isn't through deinit (but is idempotent so when deinit is called its safe). Then on quite we can iterate through all open surfaces and force the free? |
|
To add-on here, my motivation is just to try to bring together all the "close" paths through one path rather than more APIs. |
That would be similar to where I started in c7bde6a0cdf0b77e831d10021b7520eda6e997fe. The main problem is that our "surface free" operation destroys the underlying (C) allocation, so we'd need to make all Swift-level surface access safe, probably by making the private var surface: ghostty_surface_t?
@MainActor
func free() {
guard let surface else { return }
self.surface = nil
ghostty_surface_free(surface)
}
deinit {
// call free()
}There was some other complication I ran into back then, too, I think related to in-flight callbacks reentering and during the destruction, but maybe that's just a matter of guarding |
7f9452c to
90056c9
Compare
I implemented this approach, and it works well across all of my local tests. It became possible once I realized we don't need to explicitly wait on signal delivery (the previous commit), so a better outcome overall! |
8ef4d46 to
55ca178
Compare
55ca178 to
b7772b0
Compare
b7772b0 to
6907a8a
Compare
|
@mitchellh give this another look, potentially for 1.4? |
6907a8a to
c321752
Compare
Our previous application termination path (e.g. Cmd-Q) ultimately led to
Surface.deinit, which uses a detached Task to free its Ghostty surface.
That didn't give the child processes (e.g. bash) a chance to receive a
SIGHUP before the app completely exits, potentially resulting in list
shell history and un-run SIGHUP handlers.
This change implements a more graceful termination sequence that
synchronously frees all of our surfaces (via ghostty_surface_free) and
then delays application termination by a short amount (currently 100ms)
to give the child processes some time to perform their cleanup work.
All of the existing close/undo/termination behaviors remain unchanged
(e.g. confirmation, quit-on-last-window-closed, etc.).
Verified using:
#!/bin/bash
trap 'echo "SIGHUP at $(date)" >> /tmp/ghostty-sigup.log' SIGHUP
echo "Waiting for SIGHUP... "
read -p "Press Cmd+Q to quit Ghostty..."
This exported function allows the app to stop a surface's child process. ghostty_surface_process_exited() can be used to check whether it's done stopping.
Freeing the surfaces directly was a flawed approach because its a fundamental structure within the application. Instead, we now gather all of the surfaces and stop their processes. The surfaces remain alive which allows us to poll them and delay application termination (up to a time limit) until they've exited.
This keeps the IO backend interaction inside of the owning Surface code.
Replaces the per-surface stop+poll dance with a single libghostty entry point that hangs up all child processes in parallel and waits (up to a caller-provided timeout) for them to exit. The per-surface Subprocess.stop now bounds its own wait time, too, so a stuck child can't stall application teardown.
SIGHUP delivery occurs before PTY teardown and does not require Ghostty to remain alive while shells handle it. Signal all child process groups and terminate immediately rather than draining the mailbox and waiting for each process to exit. Restore the existing subprocess stop behavior so this termination path does not change normal surface teardown.
Replace the app-level quit API with an explicit, idempotent free path on
the Swift Surface model. That object clears its `ghostty_surface_t`
handle before freeing so reentrant access and its eventual `deinit`
cannot free the surface twice. `Surface` is now main actor-isolated so
access to the surface handle is guaranteed to be serialized.
We also now free all surfaces from applicationWillTerminate. This
centralizes the operation after AppKit has committed to termination and
runs normal surface teardown before application teardown closes the
PTYs. The existing deinit path remains as a fallback for surfaces freed
during normal application use.
Because we free the surfaces serially and synchronously, this approach
does add a shutdown time cost. For example, if there are 10 open tabs
and each of their shell processes takes 20ms to exit, quitting the app
takes 200ms longer. This should be acceptable because the reason for the
delay is probably obvious to the user (lots of tabs), and quitting the
whole app with many tabs still active is probably uncommon.
Verified automatic bash history writing without prompt-time history
updates or a custom signal handler using:
shopt -u histappend
HISTCONTROL=
HISTIGNORE=
history -c
echo HISTORY_MARKER
Verified explicit SIGHUP handling with an isolated interactive shell:
trap 'echo "HUP $$ $(date +%s)" >> /tmp/ghostty-hup; exit 0' HUP
Verified that a process deliberately ignoring SIGHUP does not block
macOS application termination using:
trap '' HUP
while :; do sleep 1; done
As with existing surface teardown, the ignoring bash process survived
and was reparented to PID 1 after Ghostty exited.
Verified an undo-retained surface with a 30-second undo timeout by
opening two tabs, closing the second, confirming its shell remained
alive, and quitting before the undo entry expired. Its automatic history
marker was saved successfully.
c321752 to
275b614
Compare
Our previous application termination path (e.g. Cmd-Q) ultimately led to Surface.deinit, which uses a detached Task to free its Ghostty surface. That didn't give the child processes (e.g. bash) a chance to receive a SIGHUP before the app completely exits, potentially resulting in lost shell history and un-run SIGHUP handlers.
This change implements a more graceful termination sequence that explicitly signals the child processes to hang up before PTY teardown.
All of the existing close/undo/termination behaviors remain unchanged (e.g. confirmation, quit-on-last-window-closed, etc.).
Verified using:
Fixes: #9773