Skip to content

Commit e0559da

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 0e853a9 commit e0559da

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
@@ -144,8 +144,7 @@ extension Configuration {
144144
)
145145
let execution = Execution(
146146
processIdentifier: pid,
147-
processInformation: processInfo,
148-
consoleBehavior: self.platformOptions.consoleBehavior
147+
platformHandles: .init(processInformation: processInfo)
149148
)
150149

151150
do {
@@ -288,8 +287,7 @@ extension Configuration {
288287
)
289288
let execution = Execution(
290289
processIdentifier: pid,
291-
processInformation: processInfo,
292-
consoleBehavior: self.platformOptions.consoleBehavior
290+
platformHandles: .init(processInformation: processInfo)
293291
)
294292

295293
do {
@@ -318,6 +316,26 @@ extension Configuration {
318316
}
319317
}
320318

319+
// MARK: - PlatformExecution
320+
321+
/// The collection of platform-specific handles used to control the subprocess when running.
322+
public struct PlatformHandles: Sendable {
323+
public nonisolated(unsafe) let processInformation: PROCESS_INFORMATION
324+
325+
internal init(processInformation: PROCESS_INFORMATION) {
326+
self.processInformation = processInformation
327+
}
328+
329+
internal func release() {
330+
guard CloseHandle(processInformation.hThread) else {
331+
fatalError("Failed to close thread HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
332+
}
333+
guard CloseHandle(processInformation.hProcess) else {
334+
fatalError("Failed to close process HANDLE: \(SubprocessError.UnderlyingError(rawValue: GetLastError()))")
335+
}
336+
}
337+
}
338+
321339
// MARK: - Platform Specific Options
322340

323341
/// The collection of platform-specific settings
@@ -494,7 +512,7 @@ internal func monitorProcessTermination(
494512
guard
495513
RegisterWaitForSingleObject(
496514
&waitHandle,
497-
execution.processInformation.hProcess,
515+
execution.platformHandles.processInformation.hProcess,
498516
callback,
499517
context,
500518
INFINITE,
@@ -512,7 +530,7 @@ internal func monitorProcessTermination(
512530
}
513531

514532
var status: DWORD = 0
515-
guard GetExitCodeProcess(execution.processInformation.hProcess, &status) else {
533+
guard GetExitCodeProcess(execution.platformHandles.processInformation.hProcess, &status) else {
516534
// The child process terminated but we couldn't get its status back.
517535
// Assume generic failure.
518536
return .exited(1)
@@ -530,7 +548,7 @@ extension Execution {
530548
/// Terminate the current subprocess with the given exit code
531549
/// - Parameter exitCode: The exit code to use for the subprocess.
532550
public func terminate(withExitCode exitCode: DWORD) throws {
533-
guard TerminateProcess(processInformation.hProcess, exitCode) else {
551+
guard TerminateProcess(platformHandles.processInformation.hProcess, exitCode) else {
534552
throw SubprocessError(
535553
code: .init(.failedToTerminate),
536554
underlyingError: .init(rawValue: GetLastError())
@@ -554,7 +572,7 @@ extension Execution {
554572
underlyingError: .init(rawValue: GetLastError())
555573
)
556574
}
557-
guard NTSuspendProcess(processInformation.hProcess) >= 0 else {
575+
guard NTSuspendProcess(platformHandles.processInformation.hProcess) >= 0 else {
558576
throw SubprocessError(
559577
code: .init(.failedToSuspend),
560578
underlyingError: .init(rawValue: GetLastError())
@@ -578,7 +596,7 @@ extension Execution {
578596
underlyingError: .init(rawValue: GetLastError())
579597
)
580598
}
581-
guard NTResumeProcess(processInformation.hProcess) >= 0 else {
599+
guard NTResumeProcess(platformHandles.processInformation.hProcess) >= 0 else {
582600
throw SubprocessError(
583601
code: .init(.failedToResume),
584602
underlyingError: .init(rawValue: GetLastError())

Tests/SubprocessTests/SubprocessTests+Windows.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,36 @@ extension SubprocessWindowsTests {
692692
#expect(stuckProcess.terminationStatus.isSuccess)
693693
}
694694

695+
/// Tests a use case for Windows platform handles by assigning the newly created process to a Job Object
696+
/// - see: https://devblogs.microsoft.com/oldnewthing/20131209-00/
697+
@Test func testPlatformHandles() async throws {
698+
let hJob = CreateJobObjectW(nil, nil)
699+
defer { #expect(CloseHandle(hJob)) }
700+
var info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
701+
info.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
702+
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size))
703+
704+
var platformOptions = PlatformOptions()
705+
platformOptions.preSpawnProcessConfigurator = { (createProcessFlags, startupInfo) in
706+
createProcessFlags |= DWORD(CREATE_SUSPENDED)
707+
}
708+
709+
let result = try await Subprocess.run(
710+
self.cmdExe,
711+
arguments: ["/c", "echo"],
712+
platformOptions: platformOptions,
713+
output: .discarded
714+
) { execution, _ in
715+
guard AssignProcessToJobObject(hJob, execution.platformHandles.processInformation.hProcess) else {
716+
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
717+
}
718+
guard ResumeThread(execution.platformHandles.processInformation.hThread) != DWORD(bitPattern: -1) else {
719+
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
720+
}
721+
}
722+
#expect(result.terminationStatus.isSuccess)
723+
}
724+
695725
@Test func testRunDetached() async throws {
696726
let (readFd, writeFd) = try FileDescriptor.ssp_pipe()
697727
SetHandleInformation(

0 commit comments

Comments
 (0)