From 6191c8036ba4f67a2d52f90a4bb7b419bd31259f Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Sun, 29 Jun 2025 15:08:52 -0700 Subject: [PATCH] Expose platform-specific process file descriptors Expose the pidfd on Linux and process/thread HANDLEs on Windows. This is necessary for interop with certain platform-native APIs, especially on Windows. pidfds are also gaining the ability to be used for more and more with each Linux kernel release. 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, and for Linux, a "test" that shows one use case for access to the raw fd, which is to read the globally unique (to the boot session) identifier for the process, allowing processes sharing the same PID to be distinguished. FreeBSD also has the concept of process file descriptors, and would expect to expose the same ProcessIdentifier API as for Linux. This may spread to other platforms in the future. --- .../Platforms/Subprocess+Linux.swift | 2 +- .../Platforms/Subprocess+Windows.swift | 5 ++-- .../SubprocessTests/PlatformConformance.swift | 9 ++++++ .../SubprocessTests+Linux.swift | 16 ++++++++++ .../SubprocessTests+Windows.swift | 30 +++++++++++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/Sources/Subprocess/Platforms/Subprocess+Linux.swift b/Sources/Subprocess/Platforms/Subprocess+Linux.swift index f731981..4b18dd4 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Linux.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Linux.swift @@ -191,7 +191,7 @@ extension Configuration { public struct ProcessIdentifier: Sendable, Hashable { /// The platform specific process identifier value public let value: pid_t - internal let processDescriptor: PlatformFileDescriptor + public let processDescriptor: CInt internal init(value: pid_t, processDescriptor: PlatformFileDescriptor) { self.value = value diff --git a/Sources/Subprocess/Platforms/Subprocess+Windows.swift b/Sources/Subprocess/Platforms/Subprocess+Windows.swift index 5b46933..541cc7e 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Windows.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Windows.swift @@ -677,9 +677,8 @@ extension Environment { public struct ProcessIdentifier: Sendable, Hashable { /// Windows specific process identifier value public let value: DWORD - internal nonisolated(unsafe) let processDescriptor: HANDLE - internal nonisolated(unsafe) let threadHandle: HANDLE - + public nonisolated(unsafe) let processDescriptor: HANDLE + public nonisolated(unsafe) let threadHandle: HANDLE internal init(value: DWORD, processDescriptor: HANDLE, threadHandle: HANDLE) { self.value = value diff --git a/Tests/SubprocessTests/PlatformConformance.swift b/Tests/SubprocessTests/PlatformConformance.swift index 8cbbacf..3c619d2 100644 --- a/Tests/SubprocessTests/PlatformConformance.swift +++ b/Tests/SubprocessTests/PlatformConformance.swift @@ -35,6 +35,15 @@ protocol ProcessIdentifierProtocol: Sendable, Hashable, CustomStringConvertible, #else var value: pid_t { get } #endif + + #if os(Linux) || os(Android) + var processDescriptor: PlatformFileDescriptor { get } + #endif + + #if os(Windows) + nonisolated(unsafe) var processDescriptor: PlatformFileDescriptor { get } + nonisolated(unsafe) var threadHandle: PlatformFileDescriptor { get } + #endif } extension ProcessIdentifier : ProcessIdentifierProtocol {} diff --git a/Tests/SubprocessTests/SubprocessTests+Linux.swift b/Tests/SubprocessTests/SubprocessTests+Linux.swift index 2e2e7d2..f4e87f2 100644 --- a/Tests/SubprocessTests/SubprocessTests+Linux.swift +++ b/Tests/SubprocessTests/SubprocessTests+Linux.swift @@ -114,6 +114,22 @@ struct SubprocessLinuxTests { #expect(result.terminationStatus == .unhandledException(SIGTERM)) } } + + @Test func testUniqueProcessIdentifier() async throws { + _ = try await Subprocess.run( + .path("/bin/echo"), + output: .discarded, + error: .discarded + ) { subprocess in + if subprocess.processIdentifier.processDescriptor > 0 { + var statinfo = stat() + try #require(fstat(subprocess.processIdentifier.processDescriptor, &statinfo) == 0) + + // In kernel 6.9+, st_ino globally uniquely identifies the process + #expect(statinfo.st_ino > 0) + } + } + } } fileprivate enum ProcessState: String { diff --git a/Tests/SubprocessTests/SubprocessTests+Windows.swift b/Tests/SubprocessTests/SubprocessTests+Windows.swift index b01301c..363fb70 100644 --- a/Tests/SubprocessTests/SubprocessTests+Windows.swift +++ b/Tests/SubprocessTests/SubprocessTests+Windows.swift @@ -697,6 +697,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) + #expect(SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout.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.processIdentifier.processDescriptor) else { + throw SubprocessError.UnderlyingError(rawValue: GetLastError()) + } + guard ResumeThread(execution.processIdentifier.threadHandle) != DWORD(bitPattern: -1) else { + throw SubprocessError.UnderlyingError(rawValue: GetLastError()) + } + } + #expect(result.terminationStatus.isSuccess) + } } // MARK: - User Utils