-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Fix Claude process leaks when closing tabs #9782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
5be8761
test: reproduce Claude close teardown leak
austinywang 85db658
fix: unblock Claude teardown when tabs close
austinywang 6163126
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang 5d21acb
test: reproduce stranded terminal close teardown
austinywang cebfba4
fix: isolate blocked terminal close teardowns
austinywang ef9c634
fix: respect Claude SessionEnd budget overrides
austinywang aedc1f7
fix: clear inherited Swift warning regressions
austinywang 0e4bb1c
ci: canonicalize app-host config evidence paths
austinywang f998d9d
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang b1cd4d8
test: cover teardown beyond blocked close slots
austinywang aee1f97
fix: start process teardown before free admission
austinywang 2de6814
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang b962644
test: cover source-aware network command fixtures
austinywang e50e33d
fix: make network fixture masking source-aware
austinywang db465d9
build: pin GhosttyKit for bounded teardown
austinywang 16b499f
revert: drop incomplete determinism lexer change
austinywang 586a39b
fix: reap process-group descendants after child exit
austinywang 46fa5c0
build: pin GhosttyKit for descendant teardown
austinywang 35dbb8c
test: cover teardown racing native surface reads
austinywang 3e05822
test: unblock native access regression execution
austinywang c9b543f
fix: gate native reads before surface teardown
austinywang be48e85
build: pin GhosttyKit for stable process groups
austinywang d31933e
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang 6677811
refactor: tighten native access gate ownership
austinywang f4e112f
refactor: make native access gates runtime-owned
austinywang 8fc0ece
test: cover one-shot native teardown admission
austinywang 230a8e8
build: pin stable process-group GhosttyKit
austinywang db12524
fix: expose native borrow release within package
austinywang 4db0fa3
fix: gate sharing conformance for Xcode 16
austinywang 4f4b389
fix: keep native reads off teardown actor
austinywang c54d9d5
build: pin foreground process-group GhosttyKit
austinywang 476f272
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang 0810cc8
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang 22a37de
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang 1d2334a
Update Ghostty to main-integrated process reaping
austinywang bd266e2
Pin merged GhosttyKit archive
austinywang a348c70
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang 52b914b
test: reproduce concurrent screen-tail admission
austinywang 35b87c0
fix: serialize native screen-tail reads
austinywang c3d4e12
Update Ghostty process-group reuse safety
austinywang cb95bbb
test: keep queued reads from delaying teardown
austinywang 0c6f524
fix: acquire native borrow after read admission
austinywang 01720c7
build: pin process-group safety GhosttyKit
austinywang 742d82e
ci: restore app-host validation from main
austinywang 3fff7e4
Merge origin/main into issue-9573-claude-process-leak
austinywang 81911f4
Merge origin/main into issue-9573-claude-process-leak
austinywang eace2af
Merge origin/main into issue-9573-claude-process-leak
austinywang deee27d
fix: pair terminal native teardown operations
austinywang 75d6a0c
Merge remote-tracking branch 'origin/main' into issue-9573-claude-pro…
austinywang 9ac0ef5
test: cover native read cleanup and hook timeout
austinywang f1335e6
test: count every native text free invocation
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/Concurrency/AtomicRawPointerValue.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| internal import CmuxFoundationAtomicsC | ||
|
|
||
| /// A macOS 14-compatible atomic raw pointer that does not own its pointee. | ||
| /// | ||
| /// C11 owns every storage access, so the wrapper is safe to send between | ||
| /// isolation domains. Callers remain responsible for retaining any object | ||
| /// represented by the pointer until a successful exchange removes it. | ||
| public final class AtomicRawPointerValue: @unchecked Sendable { | ||
| // The storage address never changes, and all pointee access occurs through | ||
| // the C11 atomic API rather than overlapping Swift `inout` accesses. | ||
| nonisolated(unsafe) private let storage: | ||
| UnsafeMutablePointer<CmuxAtomicRawPointerStorage> | ||
|
|
||
| /// Creates an atomic pointer value. | ||
| /// | ||
| /// - Parameter initialValue: The unowned pointer returned until replaced. | ||
| public init(_ initialValue: UnsafeRawPointer? = nil) { | ||
| storage = .allocate(capacity: 1) | ||
| CmuxAtomicRawPointerInitialize(storage, initialValue) | ||
| } | ||
|
|
||
| deinit { | ||
| storage.deallocate() | ||
| } | ||
|
|
||
| /// Returns the current unowned pointer with acquire ordering. | ||
| @inline(__always) | ||
| public func loadAcquire() -> UnsafeRawPointer? { | ||
| CmuxAtomicRawPointerLoadAcquire(storage) | ||
| } | ||
|
|
||
| /// Atomically replaces `expected` with `desired` using acquire-release ordering. | ||
| /// | ||
| /// - Parameters: | ||
| /// - expected: The pointer that must still be stored for replacement to occur. | ||
| /// - desired: The unowned replacement pointer. | ||
| /// - Returns: `true` when the replacement occurred, otherwise `false`. | ||
| @inline(__always) | ||
| public func compareExchange( | ||
| expected: UnsafeRawPointer?, | ||
| desired: UnsafeRawPointer? | ||
| ) -> Bool { | ||
| CmuxAtomicRawPointerCompareExchange(storage, expected, desired) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
...cOS/CmuxFoundation/Tests/CmuxFoundationTests/Concurrency/AtomicRawPointerValueTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import Testing | ||
| @testable import CmuxFoundation | ||
|
|
||
| @Suite | ||
| struct AtomicRawPointerValueTests { | ||
| @Test func compareExchangeOnlyReplacesTheExpectedPointer() { | ||
| let first = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 1) | ||
| let second = UnsafeMutableRawPointer.allocate(byteCount: 1, alignment: 1) | ||
| defer { | ||
| first.deallocate() | ||
| second.deallocate() | ||
| } | ||
| let value = AtomicRawPointerValue() | ||
|
|
||
| #expect(value.loadAcquire() == nil) | ||
| #expect(value.compareExchange(expected: nil, desired: first)) | ||
| #expect(value.loadAcquire() == UnsafeRawPointer(first)) | ||
| #expect(!value.compareExchange(expected: nil, desired: second)) | ||
| #expect(value.compareExchange(expected: first, desired: second)) | ||
| #expect(value.loadAcquire() == UnsafeRawPointer(second)) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
...muxTerminal/Sources/CmuxTerminal/Lifecycle/TerminalSurfaceRuntimeNativeAccessBorrow.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| internal import CmuxFoundation | ||
|
|
||
| /// A one-shot borrow that keeps one native runtime generation accessible. | ||
| final class TerminalSurfaceRuntimeNativeAccessBorrow: Sendable { | ||
| private let gate: TerminalSurfaceRuntimeNativeAccessGate | ||
| private let isActive = AtomicBooleanGate(true) | ||
|
|
||
| init(gate: TerminalSurfaceRuntimeNativeAccessGate) { | ||
| self.gate = gate | ||
| } | ||
|
|
||
| func release() { | ||
| guard isActive.compareExchange(expected: true, desired: false) else { | ||
| return | ||
| } | ||
| gate.releaseBorrow() | ||
| } | ||
|
|
||
| deinit { | ||
| release() | ||
| } | ||
| } |
118 changes: 118 additions & 0 deletions
118
.../CmuxTerminal/Sources/CmuxTerminal/Lifecycle/TerminalSurfaceRuntimeNativeAccessGate.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| internal import CmuxFoundation | ||
|
|
||
| /// Orders native surface borrows before the teardown of one runtime generation. | ||
| /// | ||
| /// The high state bit permanently closes borrow admission. Lower bits count | ||
| /// active borrows. A retained one-shot teardown action is published before the | ||
| /// close transition, so either the closer or the final borrower can claim and | ||
| /// run it synchronously without a task hop. | ||
| final class TerminalSurfaceRuntimeNativeAccessGate: Sendable { | ||
| private static let teardownRequestedMask: UInt64 = 1 << 63 | ||
| private static let borrowCountMask = teardownRequestedMask - 1 | ||
|
|
||
| private let state = AtomicUInt64Value() | ||
| private let pendingTeardownAction = AtomicRawPointerValue() | ||
|
|
||
| /// Acquires a borrow unless teardown has already claimed this generation. | ||
| func acquireBorrow() -> TerminalSurfaceRuntimeNativeAccessBorrow? { | ||
| while true { | ||
| let current = state.loadAcquire() | ||
| guard current & Self.teardownRequestedMask == 0, | ||
| current != Self.borrowCountMask else { | ||
| return nil | ||
| } | ||
| guard state.compareExchange( | ||
| expected: current, | ||
| desired: current + 1 | ||
| ) else { | ||
| continue | ||
| } | ||
| return TerminalSurfaceRuntimeNativeAccessBorrow(gate: self) | ||
| } | ||
| } | ||
|
|
||
| /// Closes admission and starts the first teardown after admitted borrows finish. | ||
| /// | ||
| /// Later teardown requests are ignored because one native runtime generation | ||
| /// has exactly one terminal teardown transition. | ||
| func requestTeardown(start: @escaping @Sendable () -> Void) { | ||
| let retainedAction = Unmanaged.passRetained( | ||
| TerminalSurfaceRuntimeTeardownAction(start: start) | ||
| ) | ||
| let actionPointer = UnsafeRawPointer(retainedAction.toOpaque()) | ||
| guard pendingTeardownAction.compareExchange( | ||
| expected: nil, | ||
| desired: actionPointer | ||
| ) else { | ||
| retainedAction.release() | ||
| return | ||
| } | ||
|
|
||
| while true { | ||
| let current = state.loadAcquire() | ||
| guard current & Self.teardownRequestedMask == 0 else { | ||
| discardPendingAction(actionPointer: actionPointer) | ||
| return | ||
| } | ||
| let closed = current | Self.teardownRequestedMask | ||
| guard state.compareExchange(expected: current, desired: closed) else { | ||
| continue | ||
| } | ||
| if closed & Self.borrowCountMask == 0 { | ||
| runPendingTeardown() | ||
| } | ||
| return | ||
| } | ||
| } | ||
|
|
||
| /// Releases one admitted borrow and starts any newly-unblocked teardown. | ||
| func releaseBorrow() { | ||
| while true { | ||
| let current = state.loadAcquire() | ||
| let borrowCount = current & Self.borrowCountMask | ||
| guard borrowCount > 0 else { return } | ||
| let released = current - 1 | ||
| guard state.compareExchange(expected: current, desired: released) else { | ||
| continue | ||
| } | ||
| if released == Self.teardownRequestedMask { | ||
| runPendingTeardown() | ||
| } | ||
| return | ||
| } | ||
| } | ||
|
|
||
| private func runPendingTeardown() { | ||
| while true { | ||
| guard let actionPointer = pendingTeardownAction.loadAcquire() else { | ||
| return | ||
| } | ||
| guard pendingTeardownAction.compareExchange( | ||
| expected: actionPointer, | ||
| desired: nil | ||
| ) else { | ||
| continue | ||
| } | ||
| takeRetainedAction(actionPointer: actionPointer).run() | ||
| return | ||
| } | ||
| } | ||
|
|
||
| private func discardPendingAction(actionPointer: UnsafeRawPointer) { | ||
| guard pendingTeardownAction.compareExchange( | ||
| expected: actionPointer, | ||
| desired: nil | ||
| ) else { | ||
| return | ||
| } | ||
| _ = takeRetainedAction(actionPointer: actionPointer) | ||
| } | ||
|
|
||
| private func takeRetainedAction( | ||
| actionPointer: UnsafeRawPointer | ||
| ) -> TerminalSurfaceRuntimeTeardownAction { | ||
| return Unmanaged<TerminalSurfaceRuntimeTeardownAction> | ||
| .fromOpaque(actionPointer) | ||
| .takeRetainedValue() | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
...OS/CmuxTerminal/Sources/CmuxTerminal/Lifecycle/TerminalSurfaceRuntimeNativeTeardown.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| internal import GhosttyKit | ||
|
|
||
| /// Couples native process shutdown with the matching final surface free. | ||
| /// | ||
| /// A runtime generation owns both operations as one value so a custom free | ||
| /// cannot accidentally invoke Ghostty's termination API on a foreign pointer. | ||
| struct TerminalSurfaceRuntimeNativeTeardown: Sendable { | ||
| let beginSurfaceTeardown: @Sendable (ghostty_surface_t) -> Void | ||
| let freeSurface: @Sendable (ghostty_surface_t) -> Void | ||
|
|
||
| static let ghostty = TerminalSurfaceRuntimeNativeTeardown( | ||
| beginSurfaceTeardown: { surface in | ||
| ghostty_surface_request_process_termination(surface) | ||
| }, | ||
| freeSurface: { surface in | ||
| ghostty_surface_free(surface) | ||
| } | ||
| ) | ||
| } |
16 changes: 16 additions & 0 deletions
16
.../CmuxTerminal/Sources/CmuxTerminal/Lifecycle/TerminalSurfaceRuntimeScreenTailReader.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /// Serializes bounded screen-tail formatting across the terminal runtime. | ||
| /// | ||
| /// Requests may wait here without borrowing their surface, so one blocked | ||
| /// formatter cannot defer another surface's process teardown. After admission, | ||
| /// the reader atomically acquires that runtime generation before dereferencing | ||
| /// its pointer. Cancelled or already-closing requests never enter Ghostty. | ||
| actor TerminalSurfaceRuntimeScreenTailReader { | ||
| func read(_ request: TerminalSurfaceRuntimeScreenTailRequest) -> String? { | ||
| guard !Task.isCancelled, | ||
| let borrow = request.nativeAccessGate.acquireBorrow() else { | ||
| return nil | ||
| } | ||
| defer { borrow.release() } | ||
| return request.read() | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.