Skip to content

Change runDetached to return a ProcessHandle instead of a ProcessIdentifier #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: eng/PR-platform-handles
Choose a base branch
from
Open
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
15 changes: 0 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
172 changes: 0 additions & 172 deletions Sources/Subprocess/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -585,175 +585,3 @@ public func run<Result>(
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
}

4 changes: 2 additions & 2 deletions Sources/Subprocess/Platforms/Subprocess+Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 0 additions & 19 deletions Tests/SubprocessTests/SubprocessTests+Unix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -774,25 +774,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
Expand Down
39 changes: 0 additions & 39 deletions Tests/SubprocessTests/SubprocessTests+Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -724,45 +724,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
Expand Down