diff --git a/README.md b/README.md index 41f02bf..f1f311d 100644 --- a/README.md +++ b/README.md @@ -78,21 +78,6 @@ async let monitorResult = run( } ``` -### Running Unmonitored Processes - -While `Subprocess` is designed with Swift’s structural concurrency in mind, it also provides a lower level, synchronous method for launching child processes. However, since `Subprocess` can’t synchronously monitor child process’s state or handle cleanup, you’ll need to attach a FileDescriptor to each I/O directly. Remember to close the `FileDescriptor` once you’re finished. - -```swift -import Subprocess - -let input: FileDescriptor = ... - -input.closeAfter { - let pid = try runDetached(.path("/bin/daemon"), input: input) - // ... other opeartions -} -``` - ### Customizable Execution You can set various parameters when running the child process, such as `Arguments`, `Environment`, and working directory: diff --git a/Sources/Subprocess/API.swift b/Sources/Subprocess/API.swift index c2f2f9f..ee7ac0f 100644 --- a/Sources/Subprocess/API.swift +++ b/Sources/Subprocess/API.swift @@ -585,175 +585,3 @@ public func run( return try await body(execution, writer, outputSequence, errorSequence) } } - -// MARK: - Detached - -/// Run an executable with given parameters and return its process -/// identifier immediately without monitoring the state of the -/// subprocess nor waiting until it exits. -/// -/// This method is useful for launching subprocesses that outlive their -/// parents (for example, daemons and trampolines). -/// -/// - Parameters: -/// - executable: The executable to run. -/// - arguments: The arguments to pass to the executable. -/// - environment: The environment to use for the process. -/// - workingDirectory: The working directory for the process. -/// - platformOptions: The platform specific options to use for the process. -/// - input: A file descriptor to bind to the subprocess' standard input. -/// - output: A file descriptor to bind to the subprocess' standard output. -/// - error: A file descriptor to bind to the subprocess' standard error. -/// - Returns: the process identifier for the subprocess. -public func runDetached( - _ executable: Executable, - arguments: Arguments = [], - environment: Environment = .inherit, - workingDirectory: FilePath? = nil, - platformOptions: PlatformOptions = PlatformOptions(), - input: FileDescriptor? = nil, - output: FileDescriptor? = nil, - error: FileDescriptor? = nil -) throws -> ProcessIdentifier { - let config: Configuration = Configuration( - executable: executable, - arguments: arguments, - environment: environment, - workingDirectory: workingDirectory, - platformOptions: platformOptions - ) - return try runDetached(config, input: input, output: output, error: error) -} - -/// Run an executable with given configuration and return its process -/// identifier immediately without monitoring the state of the -/// subprocess nor waiting until it exits. -/// -/// This method is useful for launching subprocesses that outlive their -/// parents (for example, daemons and trampolines). -/// -/// - Parameters: -/// - configuration: The `Subprocess` configuration to run. -/// - input: A file descriptor to bind to the subprocess' standard input. -/// - output: A file descriptor to bind to the subprocess' standard output. -/// - error: A file descriptor to bind to the subprocess' standard error. -/// - Returns: the process identifier for the subprocess. -public func runDetached( - _ configuration: Configuration, - input: FileDescriptor? = nil, - output: FileDescriptor? = nil, - error: FileDescriptor? = nil -) throws -> ProcessIdentifier { - let execution: Execution - switch (input, output, error) { - case (.none, .none, .none): - let processInput = NoInput() - let processOutput = DiscardedOutput() - let processError = DiscardedOutput() - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.none, .none, .some(let errorFd)): - let processInput = NoInput() - let processOutput = DiscardedOutput() - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.none, .some(let outputFd), .none): - let processInput = NoInput() - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, closeAfterSpawningProcess: false - ) - let processError = DiscardedOutput() - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.none, .some(let outputFd), .some(let errorFd)): - let processInput = NoInput() - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, - closeAfterSpawningProcess: false - ) - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.some(let inputFd), .none, .none): - let processInput = FileDescriptorInput( - fileDescriptor: inputFd, - closeAfterSpawningProcess: false - ) - let processOutput = DiscardedOutput() - let processError = DiscardedOutput() - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.some(let inputFd), .none, .some(let errorFd)): - let processInput = FileDescriptorInput( - fileDescriptor: inputFd, closeAfterSpawningProcess: false - ) - let processOutput = DiscardedOutput() - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.some(let inputFd), .some(let outputFd), .none): - let processInput = FileDescriptorInput( - fileDescriptor: inputFd, - closeAfterSpawningProcess: false - ) - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, - closeAfterSpawningProcess: false - ) - let processError = DiscardedOutput() - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - case (.some(let inputFd), .some(let outputFd), .some(let errorFd)): - let processInput = FileDescriptorInput( - fileDescriptor: inputFd, - closeAfterSpawningProcess: false - ) - let processOutput = FileDescriptorOutput( - fileDescriptor: outputFd, - closeAfterSpawningProcess: false - ) - let processError = FileDescriptorOutput( - fileDescriptor: errorFd, - closeAfterSpawningProcess: false - ) - execution = try configuration.spawn( - withInput: try processInput.createPipe(), - outputPipe: try processOutput.createPipe(), - errorPipe: try processError.createPipe() - ).execution - } - execution.release() - return execution.processIdentifier -} - diff --git a/Sources/Subprocess/Platforms/Subprocess+Windows.swift b/Sources/Subprocess/Platforms/Subprocess+Windows.swift index fe3c54f..47ed990 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Windows.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Windows.swift @@ -154,7 +154,7 @@ extension Configuration { errorWrite: errorWriteFileDescriptor ) } catch { - // If spawn() throws, monitorProcessTermination or runDetached + // If spawn() throws, monitorProcessTermination // won't have an opportunity to call release, so do it here to avoid leaking the handles. execution.release() throw error @@ -298,7 +298,7 @@ extension Configuration { errorWrite: errorWriteFileDescriptor ) } catch { - // If spawn() throws, monitorProcessTermination or runDetached + // If spawn() throws, monitorProcessTermination // won't have an opportunity to call release, so do it here to avoid leaking the handles. execution.release() throw error diff --git a/Tests/SubprocessTests/SubprocessTests+Unix.swift b/Tests/SubprocessTests/SubprocessTests+Unix.swift index b9f78cd..2a317c6 100644 --- a/Tests/SubprocessTests/SubprocessTests+Unix.swift +++ b/Tests/SubprocessTests/SubprocessTests+Unix.swift @@ -776,25 +776,6 @@ extension SubprocessUnixTests { // MARK: - Misc extension SubprocessUnixTests { - @Test func testRunDetached() async throws { - let (readFd, writeFd) = try FileDescriptor.pipe() - let pid = try runDetached( - .path("/bin/sh"), - arguments: ["-c", "echo $$"], - output: writeFd - ) - var status: Int32 = 0 - waitpid(pid.value, &status, 0) - #expect(_was_process_exited(status) > 0) - try writeFd.close() - let data = try await readFd.readUntilEOF(upToLength: 10) - let resultPID = try #require( - String(data: Data(data), encoding: .utf8) - ).trimmingCharacters(in: .whitespacesAndNewlines) - #expect("\(pid.value)" == resultPID) - try readFd.close() - } - @Test func testTerminateProcess() async throws { let stuckResult = try await Subprocess.run( // This will intentionally hang diff --git a/Tests/SubprocessTests/SubprocessTests+Windows.swift b/Tests/SubprocessTests/SubprocessTests+Windows.swift index 683dcc3..dc799e5 100644 --- a/Tests/SubprocessTests/SubprocessTests+Windows.swift +++ b/Tests/SubprocessTests/SubprocessTests+Windows.swift @@ -688,45 +688,6 @@ extension SubprocessWindowsTests { } #expect(stuckProcess.terminationStatus.isSuccess) } - - @Test func testRunDetached() async throws { - let (readFd, writeFd) = try FileDescriptor.ssp_pipe() - SetHandleInformation( - readFd.platformDescriptor, - DWORD(HANDLE_FLAG_INHERIT), - 0 - ) - let pid = try Subprocess.runDetached( - .name("powershell.exe"), - arguments: [ - "-Command", "Write-Host $PID", - ], - output: writeFd - ) - try writeFd.close() - // Wait for process to finish - guard - let processHandle = OpenProcess( - DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE), - false, - pid.value - ) - else { - Issue.record("Failed to get process handle") - return - } - - // Wait for the process to finish - WaitForSingleObject(processHandle, INFINITE) - - // Up to 10 characters because Windows process IDs are DWORDs (UInt32), whose max value is 10 digits. - let data = try await readFd.readUntilEOF(upToLength: 10) - let resultPID = try #require( - String(data: data, encoding: .utf8) - ).trimmingCharacters(in: .whitespacesAndNewlines) - #expect("\(pid.value)" == resultPID) - try readFd.close() - } } // MARK: - User Utils