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
16 changes: 3 additions & 13 deletions Sources/Services/ProcessManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,9 @@ class ProcessManager {
proc.standardOutput = logHandle
proc.standardError = logHandle

// Configure environment for headless mode (remove GUI access)
if !enableGUI {
var env = ProcessInfo.processInfo.environment
// Remove GUI-related environment variables
env.removeValue(forKey: "DISPLAY")
env.removeValue(forKey: "WAYLAND_DISPLAY")
env.removeValue(forKey: "XDG_SESSION_TYPE")
env.removeValue(forKey: "XDG_RUNTIME_DIR")
// Explicitly mark as headless
env["CI"] = "true"
env["HEADLESS"] = "true"
proc.environment = env
}
let env = RunnerEnvironment.environment(enableGUI: enableGUI)
try RunnerEnvironment.writePathSnapshot(in: workingDirectory, environment: env)
proc.environment = env

do {
try proc.run()
Expand Down
58 changes: 58 additions & 0 deletions Sources/Services/RunnerEnvironment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation

enum RunnerEnvironment {
private static let preferredPathEntries = [
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
"/usr/local/sbin",
]

private static let fallbackPath = "/usr/bin:/bin:/usr/sbin:/sbin"

static func environment(
from base: [String: String] = ProcessInfo.processInfo.environment,
enableGUI: Bool
) -> [String: String] {
var environment = base
environment["PATH"] = normalizedPath(base["PATH"])

if !enableGUI {
environment.removeValue(forKey: "DISPLAY")
environment.removeValue(forKey: "WAYLAND_DISPLAY")
environment.removeValue(forKey: "XDG_SESSION_TYPE")
environment.removeValue(forKey: "XDG_RUNTIME_DIR")
environment["CI"] = "true"
environment["HEADLESS"] = "true"
}

return environment
}

static func normalizedPath(_ path: String?) -> String {
let sourcePath = path.flatMap { $0.isEmpty ? nil : $0 } ?? fallbackPath
let existingEntries = sourcePath
.split(separator: ":", omittingEmptySubsequences: true)
.map(String.init)

var seen = Set<String>()
var entries: [String] = []

for entry in preferredPathEntries + existingEntries where seen.insert(entry).inserted {
entries.append(entry)
}

return entries.joined(separator: ":")
}

static func writePathSnapshot(
in runnerDirectory: String,
environment: [String: String] = Self.environment(enableGUI: false)
) throws {
let path = normalizedPath(environment["PATH"])
let pathFile = URL(fileURLWithPath: runnerDirectory)
.appendingPathComponent(".path")

try (path + "\n").write(to: pathFile, atomically: true, encoding: .utf8)
}
}
8 changes: 8 additions & 0 deletions Sources/Services/RunnerInstaller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class RunnerInstaller {
process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", configCommand]
process.environment = RunnerEnvironment.environment(enableGUI: false)

case .dedicatedUser(let username):
// Run config.sh as the service user
Expand All @@ -115,6 +116,13 @@ class RunnerInstaller {
throw InstallerError.configurationFailed(output)
}

if isolation == .none || isolation == .container {
try RunnerEnvironment.writePathSnapshot(
in: directory,
environment: RunnerEnvironment.environment(enableGUI: false)
)
}

print("Runner configured successfully")
}

Expand Down
63 changes: 63 additions & 0 deletions Tests/MacRunnerTests/MacRunnerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,69 @@ final class MacRunnerTests: XCTestCase {
XCTAssertEqual(ResourceLimits.shellCommand("echo test", openFileLimit: 0), "echo test")
}

func testRunnerEnvironmentPrependsHomebrewPathsToLaunchServicesPath() {
let path = RunnerEnvironment.normalizedPath("/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin")

XCTAssertTrue(path.hasPrefix("/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:"))
XCTAssertEqual(path.components(separatedBy: ":").filter { $0 == "/usr/local/bin" }.count, 1)
}

func testRunnerEnvironmentMarksHeadlessRuns() {
let environment = RunnerEnvironment.environment(
from: [
"PATH": "/usr/bin:/bin",
"DISPLAY": ":0",
"WAYLAND_DISPLAY": "wayland-0",
"XDG_SESSION_TYPE": "x11",
"XDG_RUNTIME_DIR": "/tmp/runtime",
],
enableGUI: false
)

XCTAssertNil(environment["DISPLAY"])
XCTAssertNil(environment["WAYLAND_DISPLAY"])
XCTAssertNil(environment["XDG_SESSION_TYPE"])
XCTAssertNil(environment["XDG_RUNTIME_DIR"])
XCTAssertEqual(environment["CI"], "true")
XCTAssertEqual(environment["HEADLESS"], "true")
XCTAssertTrue(environment["PATH"]?.contains("/opt/homebrew/bin") == true)
}

func testRunnerEnvironmentPreservesGUIVariablesWhenEnabled() {
let environment = RunnerEnvironment.environment(
from: [
"PATH": "/usr/bin:/bin",
"DISPLAY": ":0",
],
enableGUI: true
)

XCTAssertEqual(environment["DISPLAY"], ":0")
XCTAssertNil(environment["CI"])
XCTAssertNil(environment["HEADLESS"])
}

func testRunnerEnvironmentWritesPathSnapshot() throws {
let temporaryDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }

try RunnerEnvironment.writePathSnapshot(
in: temporaryDirectory.path,
environment: ["PATH": "/usr/bin:/bin"]
)

let written = try String(
contentsOf: temporaryDirectory.appendingPathComponent(".path"),
encoding: .utf8
)
XCTAssertEqual(
written,
"/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin\n"
)
}

func testAdministratorAuthenticationProcessUsesInteractiveTerminalHandles() {
let process = UserIsolationService.makeAdministratorAuthenticationProcess()

Expand Down
Loading