Skip to content

Commit e65a537

Browse files
committed
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.
1 parent a2de33f commit e65a537

File tree

5 files changed

+58
-3
lines changed

5 files changed

+58
-3
lines changed

Sources/Subprocess/Platforms/Subprocess+Linux.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ extension Configuration {
191191
public struct ProcessIdentifier: Sendable, Hashable {
192192
/// The platform specific process identifier value
193193
public let value: pid_t
194-
internal let processFileDescriptor: PlatformFileDescriptor
194+
public let processFileDescriptor: CInt
195195

196196
internal init(value: pid_t, processFileDescriptor: PlatformFileDescriptor) {
197197
self.value = value

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,8 +677,8 @@ extension Environment {
677677
public struct ProcessIdentifier: Sendable, Hashable {
678678
/// Windows specific process identifier value
679679
public let value: DWORD
680-
internal nonisolated(unsafe) let processHandle: HANDLE
681-
internal nonisolated(unsafe) let threadHandle: HANDLE
680+
public nonisolated(unsafe) let processHandle: HANDLE
681+
public nonisolated(unsafe) let threadHandle: HANDLE
682682

683683

684684
internal init(value: DWORD, processHandle: HANDLE, threadHandle: HANDLE) {

Tests/SubprocessTests/PlatformConformance.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ protocol ProcessIdentifierProtocol: Sendable, Hashable, CustomStringConvertible,
3535
#else
3636
var value: pid_t { get }
3737
#endif
38+
39+
#if os(Linux) || os(Android)
40+
var processFileDescriptor: PlatformFileDescriptor { get }
41+
#endif
42+
43+
#if os(Windows)
44+
nonisolated(unsafe) var processHandle: PlatformFileDescriptor { get }
45+
nonisolated(unsafe) var threadHandle: PlatformFileDescriptor { get }
46+
#endif
3847
}
3948

4049
extension ProcessIdentifier : ProcessIdentifierProtocol {}

Tests/SubprocessTests/SubprocessTests+Linux.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,22 @@ struct SubprocessLinuxTests {
114114
#expect(result.terminationStatus == .unhandledException(SIGTERM))
115115
}
116116
}
117+
118+
@Test func testUniqueProcessIdentifier() async throws {
119+
_ = try await Subprocess.run(
120+
.path("/bin/echo"),
121+
output: .discarded,
122+
error: .discarded
123+
) { subprocess in
124+
if subprocess.processIdentifier.processFileDescriptor > 0 {
125+
var statinfo = stat()
126+
try #require(fstat(subprocess.processIdentifier.processFileDescriptor, &statinfo) == 0)
127+
128+
// In kernel 6.9+, st_ino globally uniquely identifies the process
129+
#expect(statinfo.st_ino > 0)
130+
}
131+
}
132+
}
117133
}
118134

119135
fileprivate enum ProcessState: String {

Tests/SubprocessTests/SubprocessTests+Windows.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,36 @@ extension SubprocessWindowsTests {
695695
#expect(stuckProcess.terminationStatus.isSuccess)
696696
}
697697

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

0 commit comments

Comments
 (0)