Skip to content

Commit eba918b

Browse files
committed
Introduce PlatformHandles API
Imbue Execution with a new PlatformHandles structure which provides access to platform-specific handles used to control the process while it is running. This is necessary for interop with certain platform-native APIs, especially on Windows. One test has been introduced which shows how this API could be used to associate a subprocess with a Job Object using the Windows API. In future, the idea is that PlatformHandles could store a process descriptor on Linux (clone3(..., CLONE_PIDFD) / pidfd_open) and FreeBSD (pdfork), or other platforms which may have or introduce similar concepts in the future.
1 parent 4e853c3 commit eba918b

File tree

5 files changed

+82
-33
lines changed

5 files changed

+82
-33
lines changed

Sources/Subprocess/Execution.swift

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,19 @@ public struct Execution: Sendable {
3434
/// The process identifier of the current execution
3535
public let processIdentifier: ProcessIdentifier
3636

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

4140
init(
4241
processIdentifier: ProcessIdentifier,
43-
processInformation: PROCESS_INFORMATION,
44-
consoleBehavior: PlatformOptions.ConsoleBehavior
42+
platformHandles: PlatformHandles
4543
) {
4644
self.processIdentifier = processIdentifier
47-
self.processInformation = processInformation
48-
self.consoleBehavior = consoleBehavior
45+
self.platformHandles = platformHandles
4946
}
50-
#else
51-
init(
52-
processIdentifier: ProcessIdentifier
53-
) {
54-
self.processIdentifier = processIdentifier
55-
}
56-
#endif // os(Windows)
5747

5848
internal func release() {
59-
#if os(Windows)
60-
guard CloseHandle(processInformation.hThread) else {
61-
fatalError("Failed to close thread HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
62-
}
63-
guard CloseHandle(processInformation.hProcess) else {
64-
fatalError("Failed to close process HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
65-
}
66-
#endif
49+
platformHandles.release()
6750
}
6851
}
6952

Sources/Subprocess/Platforms/Subprocess+Darwin.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ import FoundationEssentials
3333

3434
#endif // SubprocessFoundation
3535

36+
// MARK: - PlatformExecution
37+
38+
/// The collection of platform-specific handles used to control the subprocess when running.
39+
public struct PlatformHandles: Sendable {
40+
public init() {}
41+
internal func release() {}
42+
}
43+
3644
// MARK: - PlatformOptions
3745

3846
/// The collection of platform-specific settings
@@ -438,7 +446,8 @@ extension Configuration {
438446
)
439447

440448
let execution = Execution(
441-
processIdentifier: .init(value: pid)
449+
processIdentifier: .init(value: pid),
450+
platformHandles: .init()
442451
)
443452
return SpawnResult(
444453
execution: execution,

Sources/Subprocess/Platforms/Subprocess+Linux.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ extension Configuration {
150150
)
151151

152152
let execution = Execution(
153-
processIdentifier: .init(value: pid)
153+
processIdentifier: .init(value: pid),
154+
platformHandles: .init()
154155
)
155156
return SpawnResult(
156157
execution: execution,
@@ -189,6 +190,14 @@ extension Configuration {
189190
}
190191
}
191192

193+
// MARK: - PlatformExecution
194+
195+
/// The collection of platform-specific handles used to control the subprocess when running.
196+
public struct PlatformHandles: Sendable {
197+
public init() {}
198+
internal func release() {}
199+
}
200+
192201
// MARK: - Platform Specific Options
193202

194203
/// The collection of platform-specific settings

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,7 @@ extension Configuration {
122122
)
123123
let execution = Execution(
124124
processIdentifier: pid,
125-
processInformation: processInfo,
126-
consoleBehavior: self.platformOptions.consoleBehavior
125+
platformHandles: .init(processInformation: processInfo)
127126
)
128127

129128
do {
@@ -244,8 +243,7 @@ extension Configuration {
244243
)
245244
let execution = Execution(
246245
processIdentifier: pid,
247-
processInformation: processInfo,
248-
consoleBehavior: self.platformOptions.consoleBehavior
246+
platformHandles: .init(processInformation: processInfo)
249247
)
250248

251249
do {
@@ -274,6 +272,26 @@ extension Configuration {
274272
}
275273
}
276274

275+
// MARK: - PlatformExecution
276+
277+
/// The collection of platform-specific handles used to control the subprocess when running.
278+
public struct PlatformHandles: Sendable {
279+
public nonisolated(unsafe) let processInformation: PROCESS_INFORMATION
280+
281+
internal init(processInformation: PROCESS_INFORMATION) {
282+
self.processInformation = processInformation
283+
}
284+
285+
internal func release() {
286+
guard CloseHandle(processInformation.hThread) else {
287+
fatalError("Failed to close thread HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
288+
}
289+
guard CloseHandle(processInformation.hProcess) else {
290+
fatalError("Failed to close process HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
291+
}
292+
}
293+
}
294+
277295
// MARK: - Platform Specific Options
278296

279297
/// The collection of platform-specific settings
@@ -450,7 +468,7 @@ internal func monitorProcessTermination(
450468
guard
451469
RegisterWaitForSingleObject(
452470
&waitHandle,
453-
execution.processInformation.hProcess,
471+
execution.platformHandles.processInformation.hProcess,
454472
callback,
455473
context,
456474
INFINITE,
@@ -468,7 +486,7 @@ internal func monitorProcessTermination(
468486
}
469487

470488
var status: DWORD = 0
471-
guard GetExitCodeProcess(execution.processInformation.hProcess, &status) else {
489+
guard GetExitCodeProcess(execution.platformHandles.processInformation.hProcess, &status) else {
472490
// The child process terminated but we couldn't get its status back.
473491
// Assume generic failure.
474492
return .exited(1)
@@ -486,7 +504,7 @@ extension Execution {
486504
/// Terminate the current subprocess with the given exit code
487505
/// - Parameter exitCode: The exit code to use for the subprocess.
488506
public func terminate(withExitCode exitCode: DWORD) throws {
489-
guard TerminateProcess(processInformation.hProcess, exitCode) else {
507+
guard TerminateProcess(platformHandles.processInformation.hProcess, exitCode) else {
490508
throw SubprocessError(
491509
code: .init(.failedToTerminate),
492510
underlyingError: .init(rawValue: GetLastError())
@@ -510,7 +528,7 @@ extension Execution {
510528
underlyingError: .init(rawValue: GetLastError())
511529
)
512530
}
513-
guard NTSuspendProcess(processInformation.hProcess) >= 0 else {
531+
guard NTSuspendProcess(platformHandles.processInformation.hProcess) >= 0 else {
514532
throw SubprocessError(
515533
code: .init(.failedToSuspend),
516534
underlyingError: .init(rawValue: GetLastError())
@@ -534,7 +552,7 @@ extension Execution {
534552
underlyingError: .init(rawValue: GetLastError())
535553
)
536554
}
537-
guard NTResumeProcess(processInformation.hProcess) >= 0 else {
555+
guard NTResumeProcess(platformHandles.processInformation.hProcess) >= 0 else {
538556
throw SubprocessError(
539557
code: .init(.failedToResume),
540558
underlyingError: .init(rawValue: GetLastError())

Tests/SubprocessTests/SubprocessTests+Windows.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,36 @@ extension SubprocessWindowsTests {
677677
#expect(stuckProcess.terminationStatus.isSuccess)
678678
}
679679

680+
/// Tests a use case for Windows platform handles by assigning the newly created process to a Job Object
681+
/// - see: https://devblogs.microsoft.com/oldnewthing/20131209-00/
682+
@Test func testPlatformHandles() async throws {
683+
let hJob = CreateJobObjectW(nil, nil)
684+
defer { #expect(CloseHandle(hJob)) }
685+
var info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
686+
info.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
687+
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size))
688+
689+
var platformOptions = PlatformOptions()
690+
platformOptions.preSpawnProcessConfigurator = { (createProcessFlags, startupInfo) in
691+
createProcessFlags |= DWORD(CREATE_SUSPENDED)
692+
}
693+
694+
let result = try await Subprocess.run(
695+
self.cmdExe,
696+
arguments: ["/c", "echo"],
697+
platformOptions: platformOptions,
698+
output: .discarded
699+
) { execution, _ in
700+
guard AssignProcessToJobObject(hJob, execution.platformHandles.processInformation.hProcess) else {
701+
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
702+
}
703+
guard ResumeThread(execution.platformHandles.processInformation.hThread) != DWORD(bitPattern: -1) else {
704+
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
705+
}
706+
}
707+
#expect(result.terminationStatus.isSuccess)
708+
}
709+
680710
@Test func testRunDetached() async throws {
681711
let (readFd, writeFd) = try FileDescriptor.ssp_pipe()
682712
SetHandleInformation(

0 commit comments

Comments
 (0)