Add hidden CLI command for live terminal debugging (#1599)

* Add hidden terminal debug CLI command

* Expand orphan terminal debug metadata

* Remove stray CLIProcessRunner test target wiring

* Tighten debug terminal diagnostics handling

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-17 04:03:49 -07:00 committed by GitHub
parent e15825826f
commit 8d8fadbb27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 549 additions and 442 deletions

View file

@ -2489,6 +2489,30 @@ final class GhosttyMetalLayer: CAMetalLayer {
}
}
final class TerminalSurfaceRegistry {
static let shared = TerminalSurfaceRegistry()
private let lock = NSLock()
private let surfaces = NSHashTable<AnyObject>.weakObjects()
private init() {}
func register(_ surface: TerminalSurface) {
lock.lock()
defer { lock.unlock() }
surfaces.add(surface)
}
func allSurfaces() -> [TerminalSurface] {
lock.lock()
let objects = surfaces.allObjects.compactMap { $0 as? TerminalSurface }
lock.unlock()
return objects.sorted { lhs, rhs in
lhs.id.uuidString < rhs.id.uuidString
}
}
}
// MARK: - Terminal Surface (owns the ghostty_surface_t lifecycle)
final class TerminalSurface: Identifiable, ObservableObject {
@ -2538,6 +2562,11 @@ final class TerminalSurface: Identifiable, ObservableObject {
private var lastPixelHeight: UInt32 = 0
private var lastXScale: CGFloat = 0
private var lastYScale: CGFloat = 0
private let debugMetadataLock = NSLock()
private let createdAt: Date = Date()
private var runtimeSurfaceCreatedAt: Date?
private var teardownRequestedAt: Date?
private var teardownRequestReason: String?
private var pendingTextQueue: [Data] = []
private var pendingTextBytes: Int = 0
private let maxPendingTextBytes = 1_048_576
@ -2623,6 +2652,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
self.hostedView = GhosttySurfaceScrollView(surfaceView: view)
// Surface is created when attached to a view
hostedView.attachSurface(self)
TerminalSurfaceRegistry.shared.register(self)
}
@ -2679,6 +2709,47 @@ final class TerminalSurface: Identifiable, ObservableObject {
portalLifecycleState.rawValue
}
private func withDebugMetadataLock<T>(_ body: () -> T) -> T {
debugMetadataLock.lock()
defer { debugMetadataLock.unlock() }
return body()
}
func debugCreatedAt() -> Date {
withDebugMetadataLock { createdAt }
}
func debugRuntimeSurfaceCreatedAt() -> Date? {
withDebugMetadataLock { runtimeSurfaceCreatedAt }
}
func debugTeardownRequest() -> (requestedAt: Date?, reason: String?) {
withDebugMetadataLock { (teardownRequestedAt, teardownRequestReason) }
}
func debugLastKnownWorkspaceId() -> UUID {
tabId
}
func debugSurfaceContextLabel() -> String {
cmuxSurfaceContextName(surfaceContext)
}
func debugInitialCommand() -> String? {
initialCommand
}
func debugPortalHostLease() -> (hostId: String?, inWindow: Bool?, area: CGFloat?) {
guard let activePortalHostLease else {
return (nil, nil, nil)
}
return (
hostId: String(describing: activePortalHostLease.hostId),
inWindow: activePortalHostLease.inWindow,
area: activePortalHostLease.area
)
}
func canAcceptPortalBinding(expectedSurfaceId: UUID?, expectedGeneration: UInt64?) -> Bool {
guard portalLifecycleState == .live else { return false }
if let expectedSurfaceId, expectedSurfaceId != id {
@ -2774,9 +2845,28 @@ final class TerminalSurface: Identifiable, ObservableObject {
#endif
}
private func recordTeardownRequest(reason: String) {
withDebugMetadataLock {
if teardownRequestedAt == nil {
teardownRequestedAt = Date()
}
if let existing = teardownRequestReason, !existing.isEmpty {
return
}
teardownRequestReason = reason
}
}
private func recordRuntimeSurfaceCreation() {
withDebugMetadataLock {
runtimeSurfaceCreatedAt = Date()
}
}
func beginPortalCloseLifecycle(reason: String) {
guard portalLifecycleState != .closed else { return }
guard portalLifecycleState != .closing else { return }
recordTeardownRequest(reason: reason)
portalLifecycleState = .closing
portalLifecycleGeneration &+= 1
#if DEBUG
@ -2805,6 +2895,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
/// before deinit; deinit will skip the free if already torn down.
@MainActor
func teardownSurface() {
recordTeardownRequest(reason: "surface.teardown")
markPortalLifecycleClosed(reason: "teardown")
let callbackContext = surfaceCallbackContext
@ -3198,6 +3289,7 @@ final class TerminalSurface: Identifiable, ObservableObject {
return
}
guard let createdSurface = surface else { return }
recordRuntimeSurfaceCreation()
// Session scrollback replay must be one-shot. Reusing it on a later runtime
// surface recreation would inject stale restored output into a live shell.