Skip to content

Commit dbba14e

Browse files
Expose platform-specific process file descriptors (#101)
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 8fdb554 commit dbba14e

File tree

5 files changed

+58
-4
lines changed

5 files changed

+58
-4
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 processDescriptor: PlatformFileDescriptor
194+
public let processDescriptor: CInt
195195

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

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -677,9 +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 processDescriptor: HANDLE
681-
internal nonisolated(unsafe) let threadHandle: HANDLE
682-
680+
public nonisolated(unsafe) let processDescriptor: HANDLE
681+
public nonisolated(unsafe) let threadHandle: HANDLE
683682

684683
internal init(value: DWORD, processDescriptor: HANDLE, threadHandle: HANDLE) {
685684
self.value = value

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 processDescriptor: PlatformFileDescriptor { get }
41+
#endif
42+
43+
#if os(Windows)
44+
nonisolated(unsafe) var processDescriptor: 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.processDescriptor > 0 {
125+
var statinfo = stat()
126+
try #require(fstat(subprocess.processIdentifier.processDescriptor, &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
@@ -697,6 +697,36 @@ extension SubprocessWindowsTests {
697697
}
698698
#expect(stuckProcess.terminationStatus.isSuccess)
699699
}
700+
701+
/// Tests a use case for Windows platform handles by assigning the newly created process to a Job Object
702+
/// - see: https://devblogs.microsoft.com/oldnewthing/20131209-00/
703+
@Test func testPlatformHandles() async throws {
704+
let hJob = CreateJobObjectW(nil, nil)
705+
defer { #expect(CloseHandle(hJob)) }
706+
var info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
707+
info.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
708+
#expect(SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size)))
709+
710+
var platformOptions = PlatformOptions()
711+
platformOptions.preSpawnProcessConfigurator = { (createProcessFlags, startupInfo) in
712+
createProcessFlags |= DWORD(CREATE_SUSPENDED)
713+
}
714+
715+
let result = try await Subprocess.run(
716+
self.cmdExe,
717+
arguments: ["/c", "echo"],
718+
platformOptions: platformOptions,
719+
output: .discarded
720+
) { execution, _ in
721+
guard AssignProcessToJobObject(hJob, execution.processIdentifier.processDescriptor) else {
722+
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
723+
}
724+
guard ResumeThread(execution.processIdentifier.threadHandle) != DWORD(bitPattern: -1) else {
725+
throw SubprocessError.UnderlyingError(rawValue: GetLastError())
726+
}
727+
}
728+
#expect(result.terminationStatus.isSuccess)
729+
}
700730
}
701731

702732
// MARK: - User Utils

0 commit comments

Comments
 (0)