-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathSubprocess+Unix.swift
More file actions
857 lines (792 loc) · 33 KB
/
Subprocess+Unix.swift
File metadata and controls
857 lines (792 loc) · 33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
#if canImport(Darwin) || canImport(Glibc) || canImport(Android) || canImport(Musl)
#if canImport(System)
import System
#else
import SystemPackage
#endif
#if os(OpenBSD)
// FIXME: Why is this necessary only on OpenBSD?
public import _SubprocessCShims
#else
import _SubprocessCShims
#endif
#if canImport(Darwin)
import Darwin
#elseif canImport(Android)
public import Android
#elseif canImport(Glibc)
public import Glibc
#elseif canImport(Musl)
public import Musl
#endif
// MARK: - Signals
/// Signals are standardized messages sent to a running program to
/// trigger specific behavior, such as quitting or error handling.
public struct Signal: Hashable, Sendable {
/// The underlying platform-specific value for the signal.
public let rawValue: Int32
public init(rawValue: Int32) {
self.rawValue = rawValue
}
/// The `.interrupt` signal is sent to a process by its
/// controlling terminal when a user wishes to interrupt
/// the process.
public static var interrupt: Self { .init(rawValue: SIGINT) }
/// The `.terminate` signal is sent to a process to request its
/// termination. Unlike the `.kill` signal, it can be caught
/// and interpreted or ignored by the process. This allows
/// the process to perform nice termination releasing resources
/// and saving state if appropriate. `.interrupt` is nearly
/// identical to `.terminate`.
public static var terminate: Self { .init(rawValue: SIGTERM) }
/// The `.suspend` signal instructs the operating system
/// to stop a process for later resumption.
public static var suspend: Self { .init(rawValue: SIGSTOP) }
/// The `.resume` signal instructs the operating system to
/// continue (restart) a process previously paused by the
/// `.suspend` signal.
public static var resume: Self { .init(rawValue: SIGCONT) }
/// The `.kill` signal is sent to a process to cause it to
/// terminate immediately (kill). In contrast to `.terminate`
/// and `.interrupt`, this signal cannot be caught or ignored,
/// and the receiving process cannot perform any
/// clean-up upon receiving this signal.
public static var kill: Self { .init(rawValue: SIGKILL) }
/// The `.terminalClosed` signal is sent to a process when
/// its controlling terminal is closed. In modern systems,
/// this signal usually means that the controlling pseudo
/// or virtual terminal has been closed.
public static var terminalClosed: Self { .init(rawValue: SIGHUP) }
/// The `.quit` signal is sent to a process by its controlling
/// terminal when the user requests that the process quit
/// and perform a core dump.
public static var quit: Self { .init(rawValue: SIGQUIT) }
/// The `.userDefinedOne` signal is sent to a process to indicate
/// user-defined conditions.
public static var userDefinedOne: Self { .init(rawValue: SIGUSR1) }
/// The `.userDefinedTwo` signal is sent to a process to indicate
/// user-defined conditions.
public static var userDefinedTwo: Self { .init(rawValue: SIGUSR2) }
/// The `.alarm` signal is sent to a process when the corresponding
/// time limit is reached.
public static var alarm: Self { .init(rawValue: SIGALRM) }
/// The `.windowSizeChange` signal is sent to a process when
/// its controlling terminal changes its size (a window change).
public static var windowSizeChange: Self { .init(rawValue: SIGWINCH) }
}
extension Execution {
/// Sends the given signal to the child process.
/// - Parameters:
/// - signal: The signal to send.
/// - shouldSendToProcessGroup: Whether this signal should be sent to
/// the entire process group.
/// - Throws: `SubprocessError` with error code `.processControlFailed`.
/// See `.underlyingError` for more details.
public func send(
signal: Signal,
toProcessGroup shouldSendToProcessGroup: Bool = false
) throws(SubprocessError) {
func _kill(_ pid: pid_t, signal: Signal) throws(SubprocessError) {
guard kill(pid, signal.rawValue) == 0 else {
throw SubprocessError.processControlFailed(
.sendSignal(signal.rawValue),
underlyingError: Errno(rawValue: errno)
)
}
}
let pid = shouldSendToProcessGroup ? -(processIdentifier.value) : processIdentifier.value
#if os(Linux) || os(Android) || os(FreeBSD)
// On platforms with process descriptors, use _subprocess_pdkill if possible
if shouldSendToProcessGroup || self.processIdentifier.processDescriptor == .invalidDescriptor {
// _subprocess_pdkill does not support sending signal to process group
try _kill(pid, signal: signal)
} else {
let rc = _subprocess_pdkill(
processIdentifier.processDescriptor,
signal.rawValue
)
if rc == 0 {
// _pidfd_send_signal succeeded
return
}
if errno == ENOSYS {
// _pidfd_send_signal is not implemented. Fallback to kill
try _kill(pid, signal: signal)
return
}
// Throw all other errors
throw SubprocessError.processControlFailed(
.sendSignal(signal.rawValue),
underlyingError: Errno(rawValue: errno)
)
}
#else
try _kill(pid, signal: signal)
#endif
}
}
// MARK: - Environment Resolution
extension Environment {
internal func pathValue() -> String? {
switch self.config {
case .inherit(let overrides):
// If PATH value exists in overrides, use it
if let value = overrides[.path] {
return value
}
// Fall back to current process
return Self.currentEnvironmentValues()[.path]
case .custom(let fullEnvironment):
if let value = fullEnvironment[.path] {
return value
}
return nil
case .rawBytes(let rawBytesArray):
let needle: [UInt8] = Array("\(Key.path.rawValue)=".utf8)
for row in rawBytesArray {
guard row.starts(with: needle) else {
continue
}
// Attempt to
let pathValue = row.dropFirst(needle.count)
return String(decoding: pathValue, as: UTF8.self)
}
return nil
}
}
// This method follows the standard "create" rule: `env` needs to be
// manually deallocated
internal func createEnv() -> [UnsafeMutablePointer<CChar>?] {
func createFullCString(
fromKey keyContainer: StringOrRawBytes,
value valueContainer: StringOrRawBytes
) -> UnsafeMutablePointer<CChar> {
let rawByteKey: UnsafeMutablePointer<CChar> = keyContainer.createRawBytes()
let rawByteValue: UnsafeMutablePointer<CChar> = valueContainer.createRawBytes()
defer {
rawByteKey.deallocate()
rawByteValue.deallocate()
}
/// length = `key` + `=` + `value` + `\null`
let totalLength = keyContainer.count + 1 + valueContainer.count + 1
let fullString: UnsafeMutablePointer<CChar> = .allocate(capacity: totalLength)
#if os(OpenBSD) || os(Linux) || os(Android)
_ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue)
#else
_ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue)
#endif
return fullString
}
var env: [UnsafeMutablePointer<CChar>?] = []
switch self.config {
case .inherit(let updates):
var current = Self.currentEnvironmentValues()
for (key, value) in updates {
// Remove the value from current to override it
// If the `value` is nil, we effectively "unset"
// this value from current
current.removeValue(forKey: key)
if let value {
let fullString = "\(key)=\(value)"
env.append(strdup(fullString))
}
}
// Add the rest of `current` to env
for (key, value) in current {
let fullString = "\(key)=\(value)"
env.append(strdup(fullString))
}
case .custom(let customValues):
for (key, value) in customValues {
let fullString = "\(key)=\(value)"
env.append(strdup(fullString))
}
case .rawBytes(let rawBytesArray):
for rawBytes in rawBytesArray {
env.append(strdup(rawBytes))
}
}
env.append(nil)
return env
}
internal static func withCopiedEnv<R>(_ body: ([UnsafeMutablePointer<CChar>]) -> R) -> R {
var values: [UnsafeMutablePointer<CChar>] = []
// This lock is taken by calls to getenv, so we want as few callouts to other code as possible here.
_subprocess_lock_environ()
guard
let environments: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?> =
_subprocess_get_environ()
else {
_subprocess_unlock_environ()
return body([])
}
var curr = environments
while let value = curr.pointee {
values.append(strdup(value))
curr = curr.advanced(by: 1)
}
_subprocess_unlock_environ()
defer { values.forEach { free($0) } }
return body(values)
}
}
// MARK: Args Creation
extension Arguments {
// This method follows the standard "create" rule: `args` needs to be
// manually deallocated
internal func createArgs(withExecutablePath executablePath: String) -> [UnsafeMutablePointer<CChar>?] {
var argv: [UnsafeMutablePointer<CChar>?] = self.storage.map { $0.createRawBytes() }
// argv[0] = executable path
if let override = self.executablePathOverride {
argv.insert(override.createRawBytes(), at: 0)
} else {
argv.insert(strdup(executablePath), at: 0)
}
argv.append(nil)
return argv
}
}
// MARK: - Executable Searching
extension Executable {
internal static let defaultSearchPaths = [
"/usr/bin",
"/bin",
"/usr/sbin",
"/sbin",
"/usr/local/bin",
]
internal func resolveExecutablePath(withPathValue pathValue: String?) throws(SubprocessError) -> String {
switch self.storage {
case .executable(let executableName):
// If the executableName in is already a full path, return it directly
if Configuration.pathAccessible(executableName, mode: X_OK) {
return executableName
}
let firstAccessibleExecutable = possibleExecutablePaths(withPathValue: pathValue)
.first { Configuration.pathAccessible($0, mode: X_OK) }
if let firstAccessibleExecutable {
return firstAccessibleExecutable
}
throw SubprocessError.executableNotFound(executableName, underlyingError: nil)
case .path(let executablePath):
// Use path directly
return executablePath.string
}
}
internal func possibleExecutablePaths(
withPathValue pathValue: String?
) -> _OrderedSet<String> {
switch self.storage {
case .executable(let executableName):
var results: _OrderedSet<String> = .init()
// executableName could be a full path
results.insert(executableName)
// Get $PATH from environment
let searchPaths =
if let pathValue = pathValue {
pathValue.split(separator: ":").map { String($0) } + Self.defaultSearchPaths
} else {
Self.defaultSearchPaths
}
for path in searchPaths {
results.insert(
FilePath(path).appending(executableName).string
)
}
return results
case .path(let executablePath):
return _OrderedSet([executablePath.string])
}
}
}
// MARK: - PreSpawn
extension Configuration {
internal typealias PreSpawnArgs = (
env: [UnsafeMutablePointer<CChar>?],
uidPtr: UnsafeMutablePointer<uid_t>?,
gidPtr: UnsafeMutablePointer<gid_t>?,
supplementaryGroups: [gid_t]?
)
internal func preSpawn<Result: ~Copyable>(
_ work: (PreSpawnArgs) async throws -> Result
) async throws -> Result {
// Prepare environment
let env = self.environment.createEnv()
defer {
for ptr in env { ptr?.deallocate() }
}
var uidPtr: UnsafeMutablePointer<uid_t>? = nil
if let userID = self.platformOptions.userID {
uidPtr = .allocate(capacity: 1)
uidPtr?.pointee = userID
}
defer {
uidPtr?.deallocate()
}
var gidPtr: UnsafeMutablePointer<gid_t>? = nil
if let groupID = self.platformOptions.groupID {
gidPtr = .allocate(capacity: 1)
gidPtr?.pointee = groupID
}
defer {
gidPtr?.deallocate()
}
var supplementaryGroups: [gid_t]?
if let groupsValue = self.platformOptions.supplementaryGroups {
supplementaryGroups = groupsValue
}
return try await work(
(
env: env,
uidPtr: uidPtr,
gidPtr: gidPtr,
supplementaryGroups: supplementaryGroups
)
)
}
internal static func pathAccessible(_ path: String, mode: Int32) -> Bool {
return path.withCString {
return access($0, mode) == 0
}
}
}
// MARK: - FileDescriptor extensions
extension FileDescriptor {
internal static func ssp_pipe() throws(SubprocessError) -> (
readEnd: FileDescriptor,
writeEnd: FileDescriptor
) {
do {
return try pipe()
} catch {
throw SubprocessError.asyncIOFailed(
reason: "Failed to create pipe",
underlyingError: error as? SubprocessError.UnderlyingError
)
}
}
internal var platformDescriptor: PlatformFileDescriptor {
return self.rawValue
}
}
internal typealias PlatformFileDescriptor = CInt
internal extension PlatformFileDescriptor {
static var invalidDescriptor: Self { -1 }
}
// MARK: - Spawning
#if !canImport(Darwin)
#if canImport(Android)
public typealias pid_t = Android.pid_t
public typealias uid_t = Android.uid_t
public typealias gid_t = Android.gid_t
#elseif canImport(Glibc)
public typealias pid_t = Glibc.pid_t
public typealias uid_t = Glibc.uid_t
public typealias gid_t = Glibc.gid_t
#elseif canImport(Musl)
public typealias pid_t = Musl.pid_t
public typealias uid_t = Musl.uid_t
public typealias gid_t = Musl.gid_t
#endif
extension Configuration {
// @unchecked Sendable because we need to capture UnsafePointers
// to send to another thread. While UnsafePointers are not
// Sendable, we are not mutating them -- we only need these type
// for C interface.
internal struct SpawnContext: @unchecked Sendable {
let argv: [UnsafeMutablePointer<CChar>?]
let env: [UnsafeMutablePointer<CChar>?]
let uidPtr: UnsafeMutablePointer<uid_t>?
let gidPtr: UnsafeMutablePointer<gid_t>?
let processGroupIDPtr: UnsafeMutablePointer<gid_t>?
}
internal func spawn(
withInput inputPipe: consuming CreatedPipe,
outputPipe: consuming CreatedPipe,
errorPipe: consuming CreatedPipe
) async throws -> SpawnResult {
// Ensure the waiter thread is running.
_setupMonitorSignalHandler()
// Instead of checking if every possible executable path
// is valid, spawn each directly and catch ENOENT
let possiblePaths = self.executable.possibleExecutablePaths(
withPathValue: self.environment.pathValue()
)
var inputPipeBox: CreatedPipe? = consume inputPipe
var outputPipeBox: CreatedPipe? = consume outputPipe
var errorPipeBox: CreatedPipe? = consume errorPipe
return try await self.preSpawn { args throws -> SpawnResult in
let (env, uidPtr, gidPtr, supplementaryGroups) = args
var _inputPipe = inputPipeBox.take()!
var _outputPipe = outputPipeBox.take()!
var _errorPipe = errorPipeBox.take()!
let inputReadFileDescriptor: IODescriptor? = _inputPipe.readFileDescriptor()
let inputWriteFileDescriptor: IODescriptor? = _inputPipe.writeFileDescriptor()
let outputReadFileDescriptor: IODescriptor? = _outputPipe.readFileDescriptor()
let outputWriteFileDescriptor: IODescriptor? = _outputPipe.writeFileDescriptor()
let errorReadFileDescriptor: IODescriptor? = _errorPipe.readFileDescriptor()
let errorWriteFileDescriptor: IODescriptor? = _errorPipe.writeFileDescriptor()
for possibleExecutablePath in possiblePaths {
var processGroupIDPtr: UnsafeMutablePointer<gid_t>? = nil
if let processGroupID = self.platformOptions.processGroupID {
processGroupIDPtr = .allocate(capacity: 1)
processGroupIDPtr?.pointee = gid_t(processGroupID)
}
// Setup Arguments
let argv: [UnsafeMutablePointer<CChar>?] = self.arguments.createArgs(
withExecutablePath: possibleExecutablePath
)
defer {
for ptr in argv { ptr?.deallocate() }
}
// Setup input
let fileDescriptors: [CInt] = [
inputReadFileDescriptor?.platformDescriptor() ?? -1,
inputWriteFileDescriptor?.platformDescriptor() ?? -1,
outputWriteFileDescriptor?.platformDescriptor() ?? -1,
outputReadFileDescriptor?.platformDescriptor() ?? -1,
errorWriteFileDescriptor?.platformDescriptor() ?? -1,
errorReadFileDescriptor?.platformDescriptor() ?? -1,
]
// Spawn
let spawnContext = SpawnContext(
argv: argv, env: env, uidPtr: uidPtr, gidPtr: gidPtr, processGroupIDPtr: processGroupIDPtr
)
let (pid, processDescriptor, spawnError) = try await runOnBackgroundThread { () throws(SubprocessError) in
return try possibleExecutablePath._withCString { exePath throws(SubprocessError) in
return try (self.workingDirectory?.string).withOptionalCString { workingDir in
return supplementaryGroups.withOptionalUnsafeBufferPointer { sgroups in
return fileDescriptors.withUnsafeBufferPointer { fds in
var pid: pid_t = 0
var processDescriptor: PlatformFileDescriptor = .invalidDescriptor
let rc = _subprocess_fork_exec(
&pid,
&processDescriptor,
exePath,
workingDir,
fds.baseAddress!,
spawnContext.argv,
spawnContext.env,
spawnContext.uidPtr,
spawnContext.gidPtr,
spawnContext.processGroupIDPtr,
CInt(supplementaryGroups?.count ?? 0),
sgroups?.baseAddress,
self.platformOptions.createSession ? 1 : 0
)
return (pid, processDescriptor, rc)
}
}
}
}
}
// Spawn error
if spawnError != 0 {
if [ENOENT, EACCES, ENOTDIR].contains(spawnError) {
// Move on to another possible path
continue
}
// Throw all other errors
try self.safelyCloseMultiple(
inputRead: inputReadFileDescriptor,
inputWrite: inputWriteFileDescriptor,
outputRead: outputReadFileDescriptor,
outputWrite: outputWriteFileDescriptor,
errorRead: errorReadFileDescriptor,
errorWrite: errorWriteFileDescriptor
)
throw SubprocessError.spawnFailed(withUnderlyingError: Errno(rawValue: spawnError))
}
// After spawn finishes, close all child side fds
try self.safelyCloseMultiple(
inputRead: inputReadFileDescriptor,
inputWrite: nil,
outputRead: nil,
outputWrite: outputWriteFileDescriptor,
errorRead: nil,
errorWrite: errorWriteFileDescriptor
)
let processIdentifier: ProcessIdentifier = .init(
value: pid,
processDescriptor: processDescriptor
)
return SpawnResult(
processIdentifier: processIdentifier,
inputWriteEnd: inputWriteFileDescriptor,
outputReadEnd: outputReadFileDescriptor,
errorReadEnd: errorReadFileDescriptor
)
}
// If we reach this point, it means either the executable path
// or working directory is not valid. Since posix_spawn does not
// provide which one is not valid, here we make a best effort guess
// by checking whether the working directory is valid. This technically
// still causes TOUTOC issue, but it's the best we can do for error recovery.
try self.safelyCloseMultiple(
inputRead: inputReadFileDescriptor,
inputWrite: inputWriteFileDescriptor,
outputRead: outputReadFileDescriptor,
outputWrite: outputWriteFileDescriptor,
errorRead: errorReadFileDescriptor,
errorWrite: errorWriteFileDescriptor
)
if let workingDirectory = self.workingDirectory?.string {
guard Configuration.pathAccessible(workingDirectory, mode: F_OK) else {
throw SubprocessError.failedToChangeWorkingDirectory(
workingDirectory,
underlyingError: Errno(rawValue: ENOENT)
)
}
}
throw SubprocessError.executableNotFound(
self.executable.description,
underlyingError: Errno(rawValue: ENOENT)
)
}
}
}
// MARK: - ProcessIdentifier
/// A platform-independent identifier for a subprocess.
public struct ProcessIdentifier: Sendable, Hashable {
/// The platform-specific process identifier value.
public let value: pid_t
#if os(Linux) || os(Android) || os(FreeBSD)
/// The process file descriptor (pidfd) for the running execution.
public let processDescriptor: CInt
#else
internal let processDescriptor: CInt // not used on other platforms
#endif
internal init(value: pid_t, processDescriptor: PlatformFileDescriptor) {
self.value = value
self.processDescriptor = processDescriptor
}
internal func close() {
if self.processDescriptor != .invalidDescriptor {
try? FileDescriptor(rawValue: self.processDescriptor).close()
}
}
}
extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertible {
/// A textual representation of the process identifier.
public var description: String { "\(self.value)" }
/// A debug-oriented textual representation of the process identifier.
public var debugDescription: String { "\(self.value)" }
}
// MARK: - Platform Specific Options
/// The collection of platform-specific settings
/// to configure the subprocess when running.
public struct PlatformOptions: Sendable {
/// The user ID for the subprocess.
public var userID: uid_t? = nil
/// The real and effective group ID and the saved
/// set-group-ID of the subprocess, equivalent to calling
/// `setgid()` on the child process.
///
/// The group ID controls permissions, particularly
/// for file access.
public var groupID: gid_t? = nil
/// The list of supplementary group IDs for the subprocess.
public var supplementaryGroups: [gid_t]? = nil
/// The process group for the subprocess, equivalent to
/// calling `setpgid()` on the child process.
///
/// The process group ID groups related processes for
/// controlling signals.
public var processGroupID: pid_t? = nil
/// A Boolean value that indicates whether to create a session
/// and detach from the terminal.
public var createSession: Bool = false
/// An ordered list of steps to tear down the child
/// process if the parent task is canceled before
/// the child process terminates.
///
/// The sequence always ends by sending a `.kill` signal.
public var teardownSequence: [TeardownStep] = []
/// Creates platform options with default values.
public init() {}
}
extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible {
internal func description(withIndent indent: Int) -> String {
let indent = String(repeating: " ", count: indent * 4)
return """
PlatformOptions(
\(indent) userID: \(String(describing: userID)),
\(indent) groupID: \(String(describing: groupID)),
\(indent) supplementaryGroups: \(String(describing: supplementaryGroups)),
\(indent) processGroupID: \(String(describing: processGroupID)),
\(indent) createSession: \(createSession)
\(indent))
"""
}
/// A textual representation of the platform options.
public var description: String {
return self.description(withIndent: 0)
}
/// A debug-oriented textual representation of the platform options.
public var debugDescription: String {
return self.description(withIndent: 0)
}
}
#endif // !canImport(Darwin)
@Sendable
internal func reapProcess(
with processIdentifier: ProcessIdentifier
) throws(SubprocessError) -> (TerminationStatus, ResourceUsage) {
do throws(Errno) {
// On some platforms, the process exit notification (in particular NOTE_EXIT from kqueue)
// may be delivered slightly before the process becomes reapable,
// so we must call waitid without WNOHANG to avoid a narrow possibility of a race condition.
// If waitid does block, it won't do so for very long at all.
return try processIdentifier.blockingReap()
} catch {
let subprocessError: SubprocessError = .failedToMonitor(withUnderlyingError: error)
throw subprocessError
}
}
extension ProcessIdentifier {
/// Reaps the zombie for the exited process. This function may block.
@available(*, noasync)
internal func blockingReap() throws(Errno) -> (TerminationStatus, ResourceUsage) {
try _blockingReap(pid: value)
}
/// Reaps the zombie for the exited process, or returns `nil` if the process is still running. This function will not block.
internal func reap() throws(Errno) -> (TerminationStatus, ResourceUsage)? {
try _reap(pid: value)
}
/// Checks whether the process has already exited without consuming the
/// zombie. Returns `true` if a child has exited (or stopped/continued in a
/// way `waitid` reports), `false` if the child is still running. The
/// zombie remains available for a subsequent call to ``blockingReap()``.
internal func peekIfExited() throws(Errno) -> Bool {
try _peekIfExited(pid: value)
}
}
@available(*, noasync)
internal func _blockingReap(pid: pid_t) throws(Errno) -> (TerminationStatus, ResourceUsage) {
while true {
var usage = rusage()
#if os(macOS) || os(FreeBSD) || os(OpenBSD)
var status: CInt = 0
let rc = wait4(pid, &status, 0, &usage)
#elseif os(Linux) || os(Android)
var siginfo = siginfo_t()
let rc = linux_waitid(P_PID, id_t(pid), &siginfo, WEXITED, &usage)
#endif
if rc >= 0 {
#if os(macOS) || os(FreeBSD) || os(OpenBSD)
return (TerminationStatus(waitStatus: status), ResourceUsage(usage))
#elseif os(Linux) || os(Android)
return (TerminationStatus(siginfo), ResourceUsage(usage))
#endif
} else if errno != EINTR {
throw Errno(rawValue: errno)
}
}
}
internal func _reap(pid: pid_t) throws(Errno) -> (TerminationStatus, ResourceUsage)? {
while true {
var usage = rusage()
#if os(macOS) || os(FreeBSD) || os(OpenBSD)
var status: CInt = 0
let rc = wait4(pid, &status, WNOHANG, &usage)
if rc > 0 {
return (TerminationStatus(waitStatus: status), ResourceUsage(usage))
} else if rc == 0 {
return nil // Child still running
}
#elseif os(Linux) || os(Android)
var siginfo = siginfo_t()
let rc = linux_waitid(P_PID, id_t(pid), &siginfo, WEXITED | WNOHANG, &usage)
if rc != -1 {
// If si_pid and si_signo are both 0, the child is still running since we used WNOHANG
if siginfo.si_pid == 0 && siginfo.si_signo == 0 {
return nil
}
return (TerminationStatus(siginfo), ResourceUsage(usage))
}
#endif
// rc == -1: either EINTR (retry) or a real error
if errno != EINTR {
throw Errno(rawValue: errno)
}
}
}
internal func _peekIfExited(pid: pid_t) throws(Errno) -> Bool {
// WNOWAIT leaves the zombie in the process table so a subsequent
// `_blockingReap` (or `_reap`) can still consume it.
let siginfo = try _waitid(idtype: P_PID, id: id_t(pid), flags: WEXITED | WNOHANG | WNOWAIT)
return !(siginfo.si_pid == 0 && siginfo.si_signo == 0)
}
internal func _waitid(idtype: idtype_t, id: id_t, flags: Int32) throws(Errno) -> siginfo_t {
while true {
var siginfo = siginfo_t()
if waitid(idtype, id, &siginfo, flags) != -1 {
return siginfo
} else if errno != EINTR {
throw Errno(rawValue: errno)
}
}
}
internal extension TerminationStatus {
init(_ siginfo: siginfo_t) {
switch siginfo.si_code {
case .init(CLD_EXITED):
self = .exited(siginfo.si_status)
case .init(CLD_KILLED), .init(CLD_DUMPED):
self = .signaled(siginfo.si_status)
default:
fatalError("Unexpected exit status: \(siginfo.si_code)")
}
}
}
#if os(macOS) || os(FreeBSD) || os(OpenBSD)
internal extension TerminationStatus {
init(waitStatus: CInt) {
switch (_was_process_exited(waitStatus) != 0, _was_process_signaled(waitStatus) != 0) {
case (true, false):
self = .exited(CInt(_get_exit_code(waitStatus)))
case (false, true):
self = .signaled(CInt(_get_signal_code(waitStatus)))
case (true, true), (false, false):
fatalError("Unexpected wait status: \(waitStatus)")
}
}
}
#endif
#if os(OpenBSD) || os(Linux) || os(Android)
internal extension siginfo_t {
var si_status: Int32 {
#if os(OpenBSD)
return _data._proc._pdata._cld._status
#elseif canImport(Glibc)
return _sifields._sigchld.si_status
#elseif canImport(Musl)
return __si_fields.__si_common.__second.__sigchld.si_status
#elseif canImport(Bionic)
return _sifields._sigchld._status
#endif
}
var si_pid: pid_t {
#if os(OpenBSD)
return _data._proc._pid
#elseif canImport(Glibc)
return _sifields._sigchld.si_pid
#elseif canImport(Musl)
return __si_fields.__si_common.__first.__piduid.si_pid
#elseif canImport(Bionic)
return _sifields._kill._pid
#endif
}
}
#endif
#endif // canImport(Darwin) || canImport(Glibc) || canImport(Android) || canImport(Musl)