diff --git a/Sources/NestKit/Utils/ProcessExecutor.swift b/Sources/NestKit/Utils/ProcessExecutor.swift index 42722d8..6c08b0e 100644 --- a/Sources/NestKit/Utils/ProcessExecutor.swift +++ b/Sources/NestKit/Utils/ProcessExecutor.swift @@ -4,6 +4,12 @@ import os public protocol ProcessExecutor: Sendable { func execute(command: String, _ arguments: [String]) async throws -> String + + /// Executes the given command with the given arguments. + /// All inputs, outputs and errors are exposed to users unlike ``execute(command:_:)``. + /// So user can input texts if the command requires. + /// The returned value indicates the status of the results of the command. + func executeInteractively(command: String, _ arguments: [String]) async throws -> Int32 } extension ProcessExecutor { @@ -11,6 +17,10 @@ extension ProcessExecutor { try await execute(command: command, arguments) } + public func executeInteractively(command: String, _ arguments: String...) async throws -> Int32 { + try await executeInteractively(command: command, arguments) + } + public func which(_ command: String) async throws -> String { try await execute(command: "/usr/bin/which", command) } @@ -18,11 +28,18 @@ extension ProcessExecutor { public struct NestProcessExecutor: ProcessExecutor { let currentDirectoryURL: URL? + let environment: [String: String] let logger: Logging.Logger let logLevel: Logging.Logger.Level - public init(currentDirectory: URL? = nil, logger: Logging.Logger, logLevel: Logging.Logger.Level = .debug) { + public init( + currentDirectory: URL? = nil, + environment: [String: String] = ProcessInfo.processInfo.environment, + logger: Logging.Logger, + logLevel: Logging.Logger.Level = .debug + ) { self.currentDirectoryURL = currentDirectory + self.environment = environment self.logger = logger self.logLevel = logLevel } @@ -48,6 +65,7 @@ public struct NestProcessExecutor: ProcessExecutor { process.currentDirectoryURL = currentDirectoryURL process.executableURL = executableURL process.arguments = arguments + process.environment = environment let outputPipe = Pipe() process.standardOutput = outputPipe @@ -96,6 +114,28 @@ public struct NestProcessExecutor: ProcessExecutor { } } } + + public func executeInteractively(command: String, _ arguments: [String]) async throws -> Int32 { + logger.debug("$ \(command) \(arguments.joined(separator: " "))") + + let process = Process() + process.executableURL = URL(fileURLWithPath: command) + process.arguments = arguments + process.environment = environment + + if let currentDirectoryURL { + process.currentDirectoryURL = currentDirectoryURL + } + + try process.run() + + // Need to support standard input + // https://forums.swift.org/t/how-to-allow-process-to-receive-user-input-when-run-as-part-of-an-executable-e-g-to-enabled-sudo-commands/34357/7 + tcsetpgrp(STDIN_FILENO, process.processIdentifier) + + process.waitUntilExit() + return process.terminationStatus + } } enum StreamElement { diff --git a/Sources/NestTestHelpers/MockExecutorBuilder.swift b/Sources/NestTestHelpers/MockExecutorBuilder.swift index 1f5a650..ca7532d 100644 --- a/Sources/NestTestHelpers/MockExecutorBuilder.swift +++ b/Sources/NestTestHelpers/MockExecutorBuilder.swift @@ -46,4 +46,10 @@ public struct MockProcessExecutor: ProcessExecutor { public func execute(command: String, _ arguments: [String]) async throws -> String { try await executorClosure(command, arguments) } + + public func executeInteractively(command: String, _ arguments: [String]) async throws -> Int32 { + // For testing, just call execute and exit + _ = try await executorClosure(command, arguments) + return 0 + } } diff --git a/Sources/nest/Commands/RunCommand.swift b/Sources/nest/Commands/RunCommand.swift index b3c44e3..9b643c1 100644 --- a/Sources/nest/Commands/RunCommand.swift +++ b/Sources/nest/Commands/RunCommand.swift @@ -83,12 +83,16 @@ struct RunCommand: AsyncParsableCommand { return } - let binaryRelativePath = executables[0].binaryPath // FIXME: Needs to address multiple commands in the same artifact bundle. - _ = try? await NestProcessExecutor(logger: logger, logLevel: .info) - .execute( - command: nestDirectory.rootDirectory.appending(path: binaryRelativePath.path(percentEncoded: false)).path(percentEncoded: false), - subcommand.arguments - ) + // FIXME: Needs to address multiple commands in the same artifact bundle. + let binaryRelativePath = executables[0].binaryPath.path(percentEncoded: false) + let command = nestDirectory.rootDirectory.appending(path: binaryRelativePath).path(percentEncoded: false) + var environment = ProcessInfo.processInfo.environment + environment["RESOURCE_PATH"] = "" + let result = try await NestProcessExecutor(environment: environment, logger: logger, logLevel: .info) + .executeInteractively(command: command, subcommand.arguments) + if result != 0 { + Foundation.exit(result) + } } }