Skip to content

Introduce PlatformHandles API #101

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: main
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
27 changes: 5 additions & 22 deletions Sources/Subprocess/Execution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,19 @@ public struct Execution: Sendable {
/// The process identifier of the current execution
public let processIdentifier: ProcessIdentifier

#if os(Windows)
internal nonisolated(unsafe) let processInformation: PROCESS_INFORMATION
internal let consoleBehavior: PlatformOptions.ConsoleBehavior
/// The collection of platform-specific handles of the current execution.
public let platformHandles: PlatformHandles

init(
processIdentifier: ProcessIdentifier,
processInformation: PROCESS_INFORMATION,
consoleBehavior: PlatformOptions.ConsoleBehavior
platformHandles: PlatformHandles
) {
self.processIdentifier = processIdentifier
self.processInformation = processInformation
self.consoleBehavior = consoleBehavior
self.platformHandles = platformHandles
}
#else
init(
processIdentifier: ProcessIdentifier
) {
self.processIdentifier = processIdentifier
}
#endif // os(Windows)

internal func release() {
#if os(Windows)
guard CloseHandle(processInformation.hThread) else {
fatalError("Failed to close thread HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
}
guard CloseHandle(processInformation.hProcess) else {
fatalError("Failed to close process HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
}
#endif
platformHandles.release()
}
}

Expand Down
11 changes: 10 additions & 1 deletion Sources/Subprocess/Platforms/Subprocess+Darwin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ import FoundationEssentials

#endif // SubprocessFoundation

// MARK: - PlatformExecution

/// The collection of platform-specific handles used to control the subprocess when running.
public struct PlatformHandles: Sendable {
public init() {}
internal func release() {}
}

// MARK: - PlatformOptions

/// The collection of platform-specific settings
Expand Down Expand Up @@ -438,7 +446,8 @@ extension Configuration {
)

let execution = Execution(
processIdentifier: .init(value: pid)
processIdentifier: .init(value: pid),
platformHandles: .init()
)
return SpawnResult(
execution: execution,
Expand Down
11 changes: 10 additions & 1 deletion Sources/Subprocess/Platforms/Subprocess+Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ extension Configuration {
)

let execution = Execution(
processIdentifier: .init(value: pid)
processIdentifier: .init(value: pid),
platformHandles: .init()
)
return SpawnResult(
execution: execution,
Expand Down Expand Up @@ -189,6 +190,14 @@ extension Configuration {
}
}

// MARK: - PlatformExecution

/// The collection of platform-specific handles used to control the subprocess when running.
public struct PlatformHandles: Sendable {
public init() {}
internal func release() {}
}

// MARK: - Platform Specific Options

/// The collection of platform-specific settings
Expand Down
36 changes: 27 additions & 9 deletions Sources/Subprocess/Platforms/Subprocess+Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ extension Configuration {
)
let execution = Execution(
processIdentifier: pid,
processInformation: processInfo,
consoleBehavior: self.platformOptions.consoleBehavior
platformHandles: .init(processInformation: processInfo)
)

do {
Expand Down Expand Up @@ -288,8 +287,7 @@ extension Configuration {
)
let execution = Execution(
processIdentifier: pid,
processInformation: processInfo,
consoleBehavior: self.platformOptions.consoleBehavior
platformHandles: .init(processInformation: processInfo)
)

do {
Expand Down Expand Up @@ -318,6 +316,26 @@ extension Configuration {
}
}

// MARK: - PlatformExecution

/// The collection of platform-specific handles used to control the subprocess when running.
public struct PlatformHandles: Sendable {
public nonisolated(unsafe) let processInformation: PROCESS_INFORMATION

internal init(processInformation: PROCESS_INFORMATION) {
self.processInformation = processInformation
}

internal func release() {
guard CloseHandle(processInformation.hThread) else {
fatalError("Failed to close thread HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
}
guard CloseHandle(processInformation.hProcess) else {
fatalError("Failed to close process HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
}
}
}

// MARK: - Platform Specific Options

/// The collection of platform-specific settings
Expand Down Expand Up @@ -494,7 +512,7 @@ internal func monitorProcessTermination(
guard
RegisterWaitForSingleObject(
&waitHandle,
execution.processInformation.hProcess,
execution.platformHandles.processInformation.hProcess,
callback,
context,
INFINITE,
Expand All @@ -512,7 +530,7 @@ internal func monitorProcessTermination(
}

var status: DWORD = 0
guard GetExitCodeProcess(execution.processInformation.hProcess, &status) else {
guard GetExitCodeProcess(execution.platformHandles.processInformation.hProcess, &status) else {
// The child process terminated but we couldn't get its status back.
// Assume generic failure.
return .exited(1)
Expand All @@ -530,7 +548,7 @@ extension Execution {
/// Terminate the current subprocess with the given exit code
/// - Parameter exitCode: The exit code to use for the subprocess.
public func terminate(withExitCode exitCode: DWORD) throws {
guard TerminateProcess(processInformation.hProcess, exitCode) else {
guard TerminateProcess(platformHandles.processInformation.hProcess, exitCode) else {
throw SubprocessError(
code: .init(.failedToTerminate),
underlyingError: .init(rawValue: GetLastError())
Expand All @@ -554,7 +572,7 @@ extension Execution {
underlyingError: .init(rawValue: GetLastError())
)
}
guard NTSuspendProcess(processInformation.hProcess) >= 0 else {
guard NTSuspendProcess(platformHandles.processInformation.hProcess) >= 0 else {
throw SubprocessError(
code: .init(.failedToSuspend),
underlyingError: .init(rawValue: GetLastError())
Expand All @@ -578,7 +596,7 @@ extension Execution {
underlyingError: .init(rawValue: GetLastError())
)
}
guard NTResumeProcess(processInformation.hProcess) >= 0 else {
guard NTResumeProcess(platformHandles.processInformation.hProcess) >= 0 else {
throw SubprocessError(
code: .init(.failedToResume),
underlyingError: .init(rawValue: GetLastError())
Expand Down
30 changes: 30 additions & 0 deletions Tests/SubprocessTests/SubprocessTests+Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,36 @@ extension SubprocessWindowsTests {
#expect(stuckProcess.terminationStatus.isSuccess)
}

/// Tests a use case for Windows platform handles by assigning the newly created process to a Job Object
/// - see: https://devblogs.microsoft.com/oldnewthing/20131209-00/
@Test func testPlatformHandles() async throws {
let hJob = CreateJobObjectW(nil, nil)
defer { #expect(CloseHandle(hJob)) }
var info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
info.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size))

var platformOptions = PlatformOptions()
platformOptions.preSpawnProcessConfigurator = { (createProcessFlags, startupInfo) in
createProcessFlags |= DWORD(CREATE_SUSPENDED)
}

let result = try await Subprocess.run(
self.cmdExe,
arguments: ["/c", "echo"],
platformOptions: platformOptions,
output: .discarded
) { execution, _ in
guard AssignProcessToJobObject(hJob, execution.platformHandles.processInformation.hProcess) else {
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
}
guard ResumeThread(execution.platformHandles.processInformation.hThread) != DWORD(bitPattern: -1) else {
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
}
}
#expect(result.terminationStatus.isSuccess)
}

@Test func testRunDetached() async throws {
let (readFd, writeFd) = try FileDescriptor.ssp_pipe()
SetHandleInformation(
Expand Down
Loading