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