Skip to content

Commit e2d8d01

Browse files
committed
fix: harden macOS Ghostty teardown
1 parent b2725cf commit e2d8d01

3 files changed

Lines changed: 136 additions & 29 deletions

File tree

macos/TaskersMac/Sources/TaskersGhosttyHost.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ final class TaskersGhosttyHost: NSObject {
6161
confirm_read_clipboard_cb: { _, _, _, _ in },
6262
write_clipboard_cb: { _, _, _, _, _ in },
6363
close_surface_cb: { userdata, _ in
64-
TaskersTerminalView.from(userdata: userdata)?.handleSurfaceClosed()
64+
TaskersGhosttySurfaceContext.from(userdata: userdata)?.handleSurfaceClosed()
6565
}
6666
)
6767

@@ -144,13 +144,13 @@ final class TaskersGhosttyHost: NSObject {
144144
return false
145145
}
146146

147-
guard let view = TaskersTerminalView.from(surface: surface) else {
147+
guard let context = TaskersGhosttySurfaceContext.from(surface: surface) else {
148148
return false
149149
}
150150

151151
switch action.tag {
152152
case GHOSTTY_ACTION_SHOW_CHILD_EXITED:
153-
view.handleChildExited(exitCode: action.action.child_exited.exit_code)
153+
context.handleChildExited(exitCode: action.action.child_exited.exit_code)
154154
return true
155155
default:
156156
return false

macos/TaskersMac/Sources/TaskersTerminalView.swift

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,73 @@
11
import AppKit
22
import Foundation
33

4+
final class TaskersGhosttySurfaceContext {
5+
private weak var host: TaskersGhosttyHost?
6+
private(set) var isClosing = false
7+
8+
let workspaceID: String
9+
let paneID: String
10+
let surfaceID: String
11+
12+
init(host: TaskersGhosttyHost, workspaceID: String, paneID: String, surfaceID: String) {
13+
self.host = host
14+
self.workspaceID = workspaceID
15+
self.paneID = paneID
16+
self.surfaceID = surfaceID
17+
}
18+
19+
func retainForUserdata() -> UnsafeMutableRawPointer {
20+
Unmanaged.passRetained(self).toOpaque()
21+
}
22+
23+
func beginTeardown() {
24+
isClosing = true
25+
}
26+
27+
func handleChildExited(exitCode: UInt32) {
28+
_ = exitCode
29+
closeSurfaceIfNeeded()
30+
}
31+
32+
func handleSurfaceClosed() {
33+
closeSurfaceIfNeeded()
34+
}
35+
36+
private func closeSurfaceIfNeeded() {
37+
guard !isClosing else {
38+
return
39+
}
40+
41+
isClosing = true
42+
host?.surfaceDidClose(workspaceID: workspaceID, paneID: paneID, surfaceID: surfaceID)
43+
}
44+
45+
static func from(surface: ghostty_surface_t) -> TaskersGhosttySurfaceContext? {
46+
from(userdata: ghostty_surface_userdata(surface))
47+
}
48+
49+
static func from(userdata: UnsafeMutableRawPointer?) -> TaskersGhosttySurfaceContext? {
50+
guard let userdata else {
51+
return nil
52+
}
53+
54+
return Unmanaged<TaskersGhosttySurfaceContext>.fromOpaque(userdata).takeUnretainedValue()
55+
}
56+
57+
static func releaseUserdata(_ userdata: UnsafeMutableRawPointer) {
58+
Unmanaged<TaskersGhosttySurfaceContext>.fromOpaque(userdata).release()
59+
}
60+
}
61+
462
final class TaskersTerminalView: NSView {
563
let workspaceID: String
664
let paneID: String
765
let surfaceID: String
866

967
private weak var host: TaskersGhosttyHost?
68+
private let callbackContext: TaskersGhosttySurfaceContext
69+
private var callbackContextHandle: UnsafeMutableRawPointer?
70+
private var isDisposed = false
1071
private var surface: ghostty_surface_t?
1172
private var commandString: String
1273

@@ -26,6 +87,13 @@ final class TaskersTerminalView: NSView {
2687
self.workspaceID = workspaceID
2788
self.paneID = paneID
2889
self.surfaceID = surfaceID
90+
self.callbackContext = TaskersGhosttySurfaceContext(
91+
host: host,
92+
workspaceID: workspaceID,
93+
paneID: paneID,
94+
surfaceID: surfaceID
95+
)
96+
self.callbackContextHandle = callbackContext.retainForUserdata()
2997
self.commandString = Self.commandString(for: descriptor.commandArgv)
3098

3199
super.init(frame: NSRect(x: 0, y: 0, width: 640, height: 420))
@@ -36,7 +104,8 @@ final class TaskersTerminalView: NSView {
36104
view: self,
37105
app: app,
38106
descriptor: descriptor,
39-
commandString: commandString
107+
commandString: commandString,
108+
userdata: self.callbackContextHandle!
40109
)
41110
updateSurfaceMetrics()
42111
}
@@ -46,10 +115,7 @@ final class TaskersTerminalView: NSView {
46115
}
47116

48117
deinit {
49-
if let surface {
50-
ghostty_surface_free(surface)
51-
}
52-
host?.unregisterSurface(self)
118+
dispose()
53119
}
54120

55121
override func becomeFirstResponder() -> Bool {
@@ -135,13 +201,38 @@ final class TaskersTerminalView: NSView {
135201
needsDisplay = true
136202
}
137203

138-
func handleChildExited(exitCode: UInt32) {
139-
_ = exitCode
140-
host?.surfaceDidClose(workspaceID: workspaceID, paneID: paneID, surfaceID: surfaceID)
204+
func beginTeardown() {
205+
callbackContext.beginTeardown()
141206
}
142207

143-
func handleSurfaceClosed() {
144-
host?.surfaceDidClose(workspaceID: workspaceID, paneID: paneID, surfaceID: surfaceID)
208+
func dispose() {
209+
guard !isDisposed else {
210+
return
211+
}
212+
213+
isDisposed = true
214+
callbackContext.beginTeardown()
215+
host?.unregisterSurface(self)
216+
217+
let surface = self.surface
218+
self.surface = nil
219+
220+
guard let callbackContextHandle else {
221+
return
222+
}
223+
self.callbackContextHandle = nil
224+
225+
let cleanup = {
226+
if let surface {
227+
ghostty_surface_free(surface)
228+
}
229+
TaskersGhosttySurfaceContext.releaseUserdata(callbackContextHandle)
230+
}
231+
if Thread.isMainThread {
232+
cleanup()
233+
} else {
234+
DispatchQueue.main.async(execute: cleanup)
235+
}
145236
}
146237

147238
private func setFocused(_ focused: Bool) {
@@ -254,15 +345,16 @@ final class TaskersTerminalView: NSView {
254345
view: TaskersTerminalView,
255346
app: ghostty_app_t,
256347
descriptor: TaskersSurfaceDescriptor,
257-
commandString: String
348+
commandString: String,
349+
userdata: UnsafeMutableRawPointer
258350
) throws -> ghostty_surface_t {
259351
let scale = NSScreen.main?.backingScaleFactor ?? 2.0
260352
var config = ghostty_surface_config_new()
261353
config.platform_tag = GHOSTTY_PLATFORM_MACOS
262354
config.platform = ghostty_platform_u(
263355
macos: ghostty_platform_macos_s(nsview: Unmanaged.passUnretained(view).toOpaque())
264356
)
265-
config.userdata = Unmanaged.passUnretained(view).toOpaque()
357+
config.userdata = userdata
266358
config.scale_factor = scale
267359
config.context = GHOSTTY_SURFACE_CONTEXT_SPLIT
268360

@@ -394,17 +486,6 @@ final class TaskersTerminalView: NSView {
394486
return flags
395487
}
396488

397-
static func from(surface: ghostty_surface_t) -> TaskersTerminalView? {
398-
from(userdata: ghostty_surface_userdata(surface))
399-
}
400-
401-
static func from(userdata: UnsafeMutableRawPointer?) -> TaskersTerminalView? {
402-
guard let userdata else {
403-
return nil
404-
}
405-
406-
return Unmanaged<TaskersTerminalView>.fromOpaque(userdata).takeUnretainedValue()
407-
}
408489
}
409490

410491
private extension NSEvent {

macos/TaskersMac/Sources/TaskersWorkspaceController.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ final class TaskersWorkspaceController: NSWindowController {
4040
private var surfaceRegistry: [String: TaskersTerminalView] = [:]
4141
private var pollTimer: Timer?
4242
private var lastRevision: UInt64?
43+
private var didShutdown = false
4344

4445
var surfaceCount: Int {
4546
surfaceRegistry.count
@@ -71,7 +72,12 @@ final class TaskersWorkspaceController: NSWindowController {
7172
}
7273

7374
deinit {
74-
pollTimer?.invalidate()
75+
shutdown()
76+
}
77+
78+
override func close() {
79+
shutdown()
80+
super.close()
7581
}
7682

7783
func start() throws {
@@ -208,8 +214,11 @@ final class TaskersWorkspaceController: NSWindowController {
208214

209215
private func pruneSurfaceRegistry(keeping liveSurfaceIDs: Set<String>) {
210216
for surfaceID in Array(surfaceRegistry.keys) where !liveSurfaceIDs.contains(surfaceID) {
211-
surfaceRegistry[surfaceID]?.removeFromSuperview()
212-
surfaceRegistry.removeValue(forKey: surfaceID)
217+
guard let surface = surfaceRegistry.removeValue(forKey: surfaceID) else {
218+
continue
219+
}
220+
surface.dispose()
221+
surface.removeFromSuperview()
213222
}
214223
}
215224

@@ -238,4 +247,21 @@ final class TaskersWorkspaceController: NSWindowController {
238247
])
239248
return view
240249
}
250+
251+
private func shutdown() {
252+
guard !didShutdown else {
253+
return
254+
}
255+
256+
didShutdown = true
257+
pollTimer?.invalidate()
258+
pollTimer = nil
259+
ghosttyHost.onSurfaceClosed = nil
260+
for surface in surfaceRegistry.values {
261+
surface.dispose()
262+
surface.removeFromSuperview()
263+
}
264+
surfaceRegistry.removeAll()
265+
window?.contentView = nil
266+
}
241267
}

0 commit comments

Comments
 (0)