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:
parent
e15825826f
commit
8d8fadbb27
5 changed files with 549 additions and 442 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue