@@ -46,52 +46,97 @@ struct SubprocessLinuxTests {
4646 }
4747
4848 @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-
7449 _ = try await Subprocess . run (
7550 // This will intentionally hang
7651 . path( " /usr/bin/sleep " ) ,
7752 arguments: [ " infinity " ] ,
7853 error: . discarded
7954 ) { 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+ }
9373 }
9474 }
9575}
9676
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+
97142#endif // canImport(Glibc) || canImport(Bionic) || canImport(Musl)
0 commit comments