@@ -46,52 +46,97 @@ struct SubprocessLinuxTests {
46
46
}
47
47
48
48
@Test func testSuspendResumeProcess( ) async throws {
49
- func isProcessSuspended( _ pid: pid_t ) throws -> Bool {
50
- let status = try Data (
51
- contentsOf: URL ( filePath: " /proc/ \( pid) /status " )
52
- )
53
- let statusString = try #require(
54
- String ( data: status, encoding: . utf8)
55
- )
56
- // Parse the status string
57
- let stats = statusString. split ( separator: " \n " )
58
- if let index = stats. firstIndex (
59
- where: { $0. hasPrefix ( " State: " ) }
60
- ) {
61
- let processState = stats [ index] . split (
62
- separator: " : "
63
- ) . map {
64
- $0. trimmingCharacters (
65
- in: . whitespacesAndNewlines
66
- )
67
- }
68
-
69
- return processState [ 1 ] . hasPrefix ( " T " )
70
- }
71
- return false
72
- }
73
-
74
49
_ = try await Subprocess . run (
75
50
// This will intentionally hang
76
51
. path( " /usr/bin/sleep " ) ,
77
52
arguments: [ " infinity " ] ,
78
53
error: . discarded
79
54
) { subprocess, standardOutput in
80
- // First suspend the process
81
- try subprocess. send ( signal: . suspend)
82
- #expect(
83
- try isProcessSuspended ( subprocess. processIdentifier. value)
84
- )
85
- // Now resume the process
86
- try subprocess. send ( signal: . resume)
87
- #expect(
88
- try isProcessSuspended ( subprocess. processIdentifier. value) == false
89
- )
90
- // Now kill the process
91
- try subprocess. send ( signal: . terminate)
92
- for try await _ in standardOutput { }
55
+ try await tryFinally {
56
+ // First suspend the process
57
+ try subprocess. send ( signal: . suspend)
58
+ try await waitForCondition ( timeout: . seconds( 30 ) ) {
59
+ let state = try subprocess. state ( )
60
+ return state == . stopped
61
+ }
62
+ // Now resume the process
63
+ try subprocess. send ( signal: . resume)
64
+ try await waitForCondition ( timeout: . seconds( 30 ) ) {
65
+ let state = try subprocess. state ( )
66
+ return state == . running
67
+ }
68
+ } finally: { error in
69
+ // Now kill the process
70
+ try subprocess. send ( signal: error != nil ? . kill : . terminate)
71
+ for try await _ in standardOutput { }
72
+ }
93
73
}
94
74
}
95
75
}
96
76
77
+ fileprivate enum ProcessState : String {
78
+ case running = " R "
79
+ case sleeping = " S "
80
+ case uninterruptibleWait = " D "
81
+ case zombie = " Z "
82
+ case stopped = " T "
83
+ }
84
+
85
+ extension Execution {
86
+ fileprivate func state( ) throws -> ProcessState {
87
+ let processStatusFile = " /proc/ \( processIdentifier. value) /status "
88
+ let processStatusData = try Data (
89
+ contentsOf: URL ( filePath: processStatusFile)
90
+ )
91
+ let stateMatches = try String ( decoding: processStatusData, as: UTF8 . self)
92
+ . split ( separator: " \n " )
93
+ . compactMap ( { line in
94
+ return try #/^State:\s+(?<status>[A-Z])\s+.*/# . wholeMatch ( in: line)
95
+ } )
96
+ guard let status = stateMatches. first, stateMatches. count == 1 , let processState = ProcessState ( rawValue: String ( status. output. status) ) else {
97
+ struct ProcStatusParseError : Error , CustomStringConvertible {
98
+ let filePath : String
99
+ let contents : Data
100
+ var description : String {
101
+ " Could not parse \( filePath) : \n \( String ( decoding: contents, as: UTF8 . self) ) "
102
+ }
103
+ }
104
+ throw ProcStatusParseError ( filePath: processStatusFile, contents: processStatusData)
105
+ }
106
+ return processState
107
+ }
108
+ }
109
+
110
+ func waitForCondition( timeout: Duration , _ evaluateCondition: ( ) throws -> Bool ) async throws {
111
+ var currentCondition = try evaluateCondition ( )
112
+ let deadline = ContinuousClock . now + timeout
113
+ while ContinuousClock . now < deadline {
114
+ if currentCondition {
115
+ return
116
+ }
117
+ try await Task . sleep ( for: . milliseconds( 10 ) )
118
+ currentCondition = try evaluateCondition ( )
119
+ }
120
+ struct TimeoutError : Error , CustomStringConvertible {
121
+ var description : String {
122
+ " Timed out waiting for condition to be true "
123
+ }
124
+ }
125
+ throw TimeoutError ( )
126
+ }
127
+
128
+ func tryFinally( _ work: ( ) async throws -> ( ) , finally: ( Error ? ) async throws -> ( ) ) async throws {
129
+ let error : Error ?
130
+ do {
131
+ try await work ( )
132
+ error = nil
133
+ } catch let e {
134
+ error = e
135
+ }
136
+ try await finally ( error)
137
+ if let error {
138
+ throw error
139
+ }
140
+ }
141
+
97
142
#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl)
0 commit comments