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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AppKit
import Foundation
import CmuxFoundation

Expand All @@ -6,6 +7,29 @@ struct ComputerUseHelperLaunchConfiguration: Equatable, Sendable {
let arguments: [String]
let environment: [String: String]

@MainActor
func workspaceConfiguration(
helperURL: URL,
fileManager: FileManager = .default
) -> NSWorkspace.OpenConfiguration? {
let executable = helperURL.appendingPathComponent("Contents/MacOS/cmux-cua")
guard
let attributes = try? fileManager.attributesOfItem(atPath: executable.path),
attributes[.type] as? FileAttributeType == .typeRegular,
fileManager.isExecutableFile(atPath: executable.path)
else { return nil }

let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = false
configuration.createsNewApplicationInstance = true
// Health checks retry in the background. Launch failures must reach the
// completion handler without waiting on another Finder error dialog.
configuration.promptsUserIfNeeded = false
configuration.arguments = arguments
configuration.environment = environment
return configuration
}

init?(
paths: ComputerUseRuntimePaths,
profile: ComputerUseDaemonProfile = .native,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1146,11 +1146,9 @@ public final class ComputerUseRuntimeService {
return false
}

let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = false
configuration.createsNewApplicationInstance = true
configuration.arguments = launch.arguments
configuration.environment = launch.environment
guard let configuration = launch.workspaceConfiguration(helperURL: helperURL) else {
return false
}
let launchedProcessIdentifier: pid_t? = await withCheckedContinuation { continuation in
NSWorkspace.shared.openApplication(
at: helperURL,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import AppKit
import Foundation
import Testing
@testable import CmuxComputerUse

@MainActor
struct ComputerUseHelperLaunchConfigurationTests {
@Test(arguments: ComputerUseDaemonProfile.allCases)
func backgroundLaunchDoesNotWaitForUserToDismissErrors(
profile: ComputerUseDaemonProfile
) throws {
let fixture = try HelperBundleFixture()
defer { fixture.remove() }
let launch = try makeLaunch(fixture: fixture, profile: profile)
let configuration = try #require(launch.workspaceConfiguration(helperURL: fixture.bundle))

#expect(!configuration.promptsUserIfNeeded)
#expect(!configuration.activates)
#expect(configuration.createsNewApplicationInstance)
#expect(configuration.arguments == launch.arguments)
#expect(configuration.environment == launch.environment)
}

@Test func nonExecutableHelperIsRejectedUntilPermissionsAreRepaired() throws {
let fixture = try HelperBundleFixture()
defer { fixture.remove() }
let launch = try makeLaunch(fixture: fixture)
try FileManager.default.setAttributes(
[.posixPermissions: 0o644], ofItemAtPath: fixture.executable.path
)

#expect(launch.workspaceConfiguration(helperURL: fixture.bundle) == nil)
#expect(!FileManager.default.isExecutableFile(atPath: fixture.executable.path))

try FileManager.default.setAttributes(
[.posixPermissions: 0o755], ofItemAtPath: fixture.executable.path
)
#expect(launch.workspaceConfiguration(helperURL: fixture.bundle) != nil)
}

@Test func missingExecutableAndDirectoryAreRejected() throws {
let fixture = try HelperBundleFixture()
defer { fixture.remove() }
let launch = try makeLaunch(fixture: fixture)
try FileManager.default.removeItem(at: fixture.executable)
#expect(launch.workspaceConfiguration(helperURL: fixture.bundle) == nil)

try FileManager.default.createDirectory(
at: fixture.executable, withIntermediateDirectories: false
)
#expect(launch.workspaceConfiguration(helperURL: fixture.bundle) == nil)
}

private func makeLaunch(
fixture: HelperBundleFixture,
profile: ComputerUseDaemonProfile = .native
) throws -> ComputerUseHelperLaunchConfiguration {
try #require(ComputerUseHelperLaunchConfiguration(
paths: ComputerUseRuntimePaths(
homeDirectoryURL: fixture.root,
socketRootDirectoryURL: fixture.root,
environment: [:],
bundleIdentifier: "com.cmuxterm.tests.helper-launch",
authenticationToken: "test-token",
hostAuthenticationToken: "test-host-token"
),
profile: profile
))
}
}
Loading