From 2967cada95ce21694fdcced611a5852b2b99f96c Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Sun, 29 Jun 2025 13:02:23 -0700 Subject: [PATCH] Remove runDetached API Currently runDetached returns a ProcessIdentifier, which contains a pidfd on Linux and HANDLE on Windows. These handles are closed as soon as runDetached returns, making the values effectively useless to the caller. Similar to #92, there is a Windows-specific race condition here. On Unix the pid will be valid until someone waitid's it, so a caller _could_ use pidfd_open to get a new pidfd. But on Windows the ProcessIdentifier may already be invalid by the time runDetached returns and therefore OpenProcess can't be used to recover a handle. On FreeBSD one can't get a process descriptor from a PID at all (PDs are only for child processes). In order to avoid these race conditions, runDetached would need to either return a type which manages the lifetime of the various platform-specific handles, or leak the handles and leave them to be managed by the user. Due to questionable utility of these APIs in the first place (the closure based API can be used in conjunction with a Task to do pretty much everything runDetached can, without the downsides), we simply remove them. Closes #94 --- README.md | 15 -- Sources/Subprocess/API.swift | 172 ------------------ .../Platforms/Subprocess+Windows.swift | 4 +- .../SubprocessTests+Unix.swift | 19 -- .../SubprocessTests+Windows.swift | 39 ---- 5 files changed, 2 insertions(+), 247 deletions(-) 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 0121bcc..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.processIdentifier.close() - return execution.processIdentifier -} - diff --git a/Sources/Subprocess/Platforms/Subprocess+Windows.swift b/Sources/Subprocess/Platforms/Subprocess+Windows.swift index 2d1fa3f..779e073 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. pid.close() 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. pid.close() throw error diff --git a/Tests/SubprocessTests/SubprocessTests+Unix.swift b/Tests/SubprocessTests/SubprocessTests+Unix.swift index d151911..e23a246 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 ad4f3cf..ac31891 100644 --- a/Tests/SubprocessTests/SubprocessTests+Windows.swift +++ b/Tests/SubprocessTests/SubprocessTests+Windows.swift @@ -727,45 +727,6 @@ extension SubprocessWindowsTests { } #expect(result.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