Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Sources/CodexerCore/CodexLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,7 @@ public actor CodexInstanceController {
private let closeTimeout: Duration
private let closePollInterval: Duration
private let launchValidationTimeout: Duration
private let windowPresentationTimeout: Duration
private let processTreeProvider: any ProcessTreeSnapshotProviding
private let processIdentitySignaler: any ProcessIdentitySignaling
private let kernelStartKeyProvider: @Sendable (Int32) -> String?
Expand All @@ -849,6 +850,7 @@ public actor CodexInstanceController {
closeTimeout: Duration = .seconds(5),
closePollInterval: Duration = .milliseconds(100),
launchValidationTimeout: Duration = .seconds(2),
windowPresentationTimeout: Duration = .seconds(10),
processTreeProvider: any ProcessTreeSnapshotProviding = SystemProcessTreeSnapshotProvider(),
processIdentitySignaler: (any ProcessIdentitySignaling)? = nil,
kernelStartKeyProvider: (@Sendable (Int32) -> String?)? = nil
Expand All @@ -861,6 +863,7 @@ public actor CodexInstanceController {
self.closeTimeout = closeTimeout
self.closePollInterval = closePollInterval
self.launchValidationTimeout = launchValidationTimeout
self.windowPresentationTimeout = windowPresentationTimeout
self.processTreeProvider = processTreeProvider
self.processIdentitySignaler = processIdentitySignaler
?? SystemProcessIdentitySignaler(snapshotProvider: processTreeProvider)
Expand Down Expand Up @@ -1266,7 +1269,7 @@ public actor CodexInstanceController {

private func waitForPresentedWindow(processID: Int32) async throws -> Bool {
let clock = ContinuousClock()
let deadline = clock.now.advanced(by: launchValidationTimeout)
let deadline = clock.now.advanced(by: windowPresentationTimeout)
while clock.now < deadline {
try Task.checkCancellation()
if lifecycleController.isPresentingWindow(processID: processID) {
Expand Down
48 changes: 42 additions & 6 deletions Tests/CodexerCoreTests/CodexLauncherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,13 +358,14 @@ final class CodexLauncherTests: XCTestCase {
func testOpenStopsFreshInstanceThatDoesNotPresentAWindow() async throws {
let profile = makeProfile(slug: "windowless")
try prepareIsolationLayout(for: profile)
let lifecycle = WindowlessLifecycleController(processID: 742)
let lifecycle = PresentationLifecycleController(processID: 742)
let controller = CodexInstanceController(
validator: AcceptingValidator(),
processSnapshotProvider: FixedProcessSnapshotProvider(snapshot: ""),
workspaceLauncher: RecordingWorkspaceLauncher(processID: 742),
lifecycleController: lifecycle,
launchValidationTimeout: .milliseconds(1)
launchValidationTimeout: .milliseconds(1),
windowPresentationTimeout: .milliseconds(1)
)

do {
Expand All @@ -383,11 +384,37 @@ final class CodexLauncherTests: XCTestCase {
XCTAssertEqual(lifecycle.terminatedProcessIDs, [742])
}

func testOpenAllowsWindowPresentationToOutlastLaunchValidation() async throws {
let profile = makeProfile(slug: "slow-window")
try prepareIsolationLayout(for: profile)
let lifecycle = PresentationLifecycleController(
processID: 744,
hiddenWindowChecks: 2
)
let controller = CodexInstanceController(
validator: AcceptingValidator(),
processSnapshotProvider: FixedProcessSnapshotProvider(snapshot: ""),
workspaceLauncher: RecordingWorkspaceLauncher(processID: 744),
lifecycleController: lifecycle,
launchValidationTimeout: .milliseconds(1),
windowPresentationTimeout: .milliseconds(250)
)

let outcome = try await controller.open(
profile: profile,
codexAppURL: configuration(for: profile).codexAppURL
)

XCTAssertEqual(outcome, .launched(processID: 744))
XCTAssertGreaterThanOrEqual(lifecycle.windowCheckCount, 3)
XCTAssertTrue(lifecycle.terminatedProcessIDs.isEmpty)
}

func testCancellingWindowPresentationStopsFreshInstance() async throws {
let profile = makeProfile(slug: "cancelled-window")
try prepareIsolationLayout(for: profile)
let presentationRequested = expectation(description: "presentation requested")
let lifecycle = WindowlessLifecycleController(processID: 743) {
let lifecycle = PresentationLifecycleController(processID: 743) {
presentationRequested.fulfill()
}
let controller = CodexInstanceController(
Expand Down Expand Up @@ -1364,14 +1391,21 @@ private final class RejectingLaunchedProcessController: CodexApplicationLifecycl
}
}

private final class WindowlessLifecycleController: CodexApplicationLifecycleControlling, @unchecked Sendable {
private final class PresentationLifecycleController: CodexApplicationLifecycleControlling, @unchecked Sendable {
let processID: Int32
private(set) var presentationRequests: [Int32] = []
private(set) var terminatedProcessIDs: [Int32] = []
private(set) var windowCheckCount = 0
private let hiddenWindowChecks: Int?
private let onPresentationRequested: () -> Void

init(processID: Int32, onPresentationRequested: @escaping () -> Void = {}) {
init(
processID: Int32,
hiddenWindowChecks: Int? = nil,
onPresentationRequested: @escaping () -> Void = {}
) {
self.processID = processID
self.hiddenWindowChecks = hiddenWindowChecks
self.onPresentationRequested = onPresentationRequested
}

Expand All @@ -1392,7 +1426,9 @@ private final class WindowlessLifecycleController: CodexApplicationLifecycleCont
}

func isPresentingWindow(processID _: Int32) -> Bool {
false
windowCheckCount += 1
guard let hiddenWindowChecks else { return false }
return windowCheckCount > hiddenWindowChecks
}

func terminate(
Expand Down
Loading