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