diff --git a/Sources/Subprocess/API.swift b/Sources/Subprocess/API.swift index c6054bba..88578135 100644 --- a/Sources/Subprocess/API.swift +++ b/Sources/Subprocess/API.swift @@ -464,6 +464,7 @@ public func run< return ExecutionRecord( processIdentifier: result.value.processIdentifier, terminationStatus: result.terminationStatus, + resourceUsage: result.resourceUsage, standardOutput: result.value.standardOutput, standardError: result.value.standardError ) @@ -567,6 +568,7 @@ public func run< return ExecutionRecord( processIdentifier: result.value.processIdentifier, terminationStatus: result.terminationStatus, + resourceUsage: result.resourceUsage, standardOutput: result.value.standardOutput, standardError: result.value.standardError ) diff --git a/Sources/Subprocess/Configuration.swift b/Sources/Subprocess/Configuration.swift index 85ab9238..5c15559a 100644 --- a/Sources/Subprocess/Configuration.swift +++ b/Sources/Subprocess/Configuration.swift @@ -133,12 +133,13 @@ public struct Configuration: Sendable { // even if `body` throws, and we are not leaving zombie processes in the // process table which will cause the process termination monitoring thread // to effectively hang due to the pid never being awaited - let terminationStatus = try await monitorProcessTermination( + let (terminationStatus, resourceUsage) = try await monitorProcessTermination( for: execution.processIdentifier ) return ExecutionOutcome( terminationStatus: terminationStatus, + resourceUsage: resourceUsage, value: try result.get() ) } onCleanup: { diff --git a/Sources/Subprocess/Platforms/Subprocess+BSD.swift b/Sources/Subprocess/Platforms/Subprocess+BSD.swift index 18d511c8..5aba8dc4 100644 --- a/Sources/Subprocess/Platforms/Subprocess+BSD.swift +++ b/Sources/Subprocess/Platforms/Subprocess+BSD.swift @@ -29,10 +29,10 @@ internal import Dispatch @Sendable internal func monitorProcessTermination( for processIdentifier: ProcessIdentifier -) async throws(SubprocessError) -> TerminationStatus { - switch Result(catching: { () throws(Errno) -> TerminationStatus? in try processIdentifier.reap() }) { - case let .success(status?): - return status +) async throws(SubprocessError) -> (TerminationStatus, ResourceUsage) { + switch Result(catching: { () throws(Errno) -> (TerminationStatus, ResourceUsage)? in try processIdentifier.reap() }) { + case let .success(result?): + return result case .success(nil): break case let .failure(error): @@ -50,10 +50,9 @@ internal func monitorProcessTermination( do throws(Errno) { // NOTE_EXIT 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. - let status = try processIdentifier.blockingReap() - continuation.resume(returning: status) + // so we must call wait4 without WNOHANG to avoid a narrow possibility of a race condition. + // If wait4 does block, it won't do so for very long at all. + continuation.resume(returning: try processIdentifier.blockingReap()) } catch { let subprocessError: SubprocessError = .failedToMonitor(withUnderlyingError: error) continuation.resume(throwing: subprocessError) diff --git a/Sources/Subprocess/Platforms/Subprocess+Linux.swift b/Sources/Subprocess/Platforms/Subprocess+Linux.swift index 403c0461..5d9f783b 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Linux.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Linux.swift @@ -79,10 +79,10 @@ extension Int32 { @Sendable internal func monitorProcessTermination( for processIdentifier: ProcessIdentifier -) async throws(SubprocessError) -> TerminationStatus { +) async throws(SubprocessError) -> (TerminationStatus, ResourceUsage) { return try await _castError { return try await withCheckedThrowingContinuation { continuation in - let status = _processMonitorState.withLock { state -> Result? in + let status = _processMonitorState.withLock { state -> Result<(TerminationStatus, ResourceUsage), SubprocessError>? in switch state { case .notStarted: let error: SubprocessError = .failedToMonitor(withUnderlyingError: nil) @@ -123,9 +123,9 @@ internal func monitorProcessTermination( // Since Linux coalesce signals, it's possible by the time we request // monitoring the process has already exited. Check to make sure that // is not the case and only save continuation then. - switch Result(catching: { () throws(Errno) -> TerminationStatus? in try processIdentifier.reap() }) { - case let .success(status?): - return .success(status) + switch Result(catching: { () throws(Errno) -> (TerminationStatus, ResourceUsage)? in try processIdentifier.reap() }) { + case let .success(result?): + return .success(result) case .success(nil): // Save this continuation to be called by signal handler var newState = storage @@ -158,7 +158,7 @@ private enum ProcessMonitorState { let epollFileDescriptor: CInt let shutdownFileDescriptor: CInt let monitorThread: pthread_t - var continuations: [PlatformFileDescriptor: CheckedContinuation] + var continuations: [PlatformFileDescriptor: CheckedContinuation<(TerminationStatus, ResourceUsage), any Error>] } case notStarted @@ -243,8 +243,8 @@ private func monitorThreadFunc(context: MonitorThreadContext) { let error: SubprocessError = .failedToMonitor( withUnderlyingError: Errno(rawValue: pwaitErrno) ) - let continuations = _processMonitorState.withLock { state -> [CheckedContinuation] in - let result: [CheckedContinuation] + let continuations = _processMonitorState.withLock { state -> [CheckedContinuation<(TerminationStatus, ResourceUsage), any Error>] in + let result: [CheckedContinuation<(TerminationStatus, ResourceUsage), any Error>] if case .started(let storage) = state { result = Array(storage.continuations.values) } else { @@ -407,8 +407,17 @@ internal func _setupMonitorSignalHandler() { } private func _blockAndWaitForProcessDescriptor(_ pidfd: CInt, context: MonitorThreadContext) { - var terminationStatus = Result(catching: { () throws(Errno) in - try TerminationStatus(_waitid(idtype: idtype_t(UInt32(P_PIDFD)), id: id_t(pidfd), flags: WEXITED)) + var terminationResult = Result(catching: { () throws(Errno) -> (TerminationStatus, ResourceUsage) in + while true { + var siginfo = siginfo_t() + var usage = rusage() + let rc = linux_waitid(idtype_t(UInt32(P_PIDFD)), id_t(pidfd), &siginfo, WEXITED, &usage) + if rc != -1 { + return (TerminationStatus(siginfo), ResourceUsage(usage)) + } else if errno != EINTR { + throw Errno(rawValue: errno) + } + } }).mapError { underlyingError in return SubprocessError.failedToMonitor(withUnderlyingError: underlyingError) } @@ -422,14 +431,14 @@ private func _blockAndWaitForProcessDescriptor(_ pidfd: CInt, context: MonitorTh ) if rc != 0 { let epollErrno = errno - terminationStatus = .failure( + terminationResult = .failure( SubprocessError.failedToMonitor( withUnderlyingError: Errno(rawValue: epollErrno) ) ) } // Notify the continuation - let continuation = _processMonitorState.withLock { state -> CheckedContinuation? in + let continuation = _processMonitorState.withLock { state -> CheckedContinuation<(TerminationStatus, ResourceUsage), any Error>? in guard case .started(let storage) = state, let continuation = storage.continuations[pidfd] else { @@ -441,13 +450,13 @@ private func _blockAndWaitForProcessDescriptor(_ pidfd: CInt, context: MonitorTh state = .started(newStorage) return continuation } - continuation?.resume(with: terminationStatus) + continuation?.resume(with: terminationResult) } // On older kernels, fall back to using signal handlers private typealias ResultContinuation = ( - result: Result, - continuation: CheckedContinuation + result: Result<(TerminationStatus, ResourceUsage), SubprocessError>, + continuation: CheckedContinuation<(TerminationStatus, ResourceUsage), any Error> ) private func _reapAllKnownChildProcesses(_ signalFd: CInt, context: MonitorThreadContext) { guard signalFd == _signalPipe.readEnd else { @@ -467,19 +476,19 @@ private func _reapAllKnownChildProcesses(_ signalFd: CInt, context: MonitorThrea // Since Linux coalesce signals, we need to loop through all known child process // to check if they exited. loop: for (knownChildPID, continuation) in storage.continuations { - let terminationStatus: Result - switch Result(catching: { () throws(Errno) -> TerminationStatus? in try _reap(pid: knownChildPID) }) { - case let .success(status?): - terminationStatus = .success(status) + let terminationResult: Result<(TerminationStatus, ResourceUsage), SubprocessError> + switch Result(catching: { () throws(Errno) -> (TerminationStatus, ResourceUsage)? in try _reap(pid: knownChildPID) }) { + case let .success(result?): + terminationResult = .success(result) case .success(nil): // Move on to the next child continue loop case let .failure(error): - terminationStatus = .failure( + terminationResult = .failure( SubprocessError.failedToMonitor(withUnderlyingError: error) ) } - results.append((result: terminationStatus, continuation: continuation)) + results.append((result: terminationResult, continuation: continuation)) // Now we have the exit code, remove saved continuations updatedContinuations.removeValue(forKey: knownChildPID) } diff --git a/Sources/Subprocess/Platforms/Subprocess+Unix.swift b/Sources/Subprocess/Platforms/Subprocess+Unix.swift index dc911b23..a8b320a9 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Unix.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Unix.swift @@ -676,37 +676,63 @@ extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible extension ProcessIdentifier { /// Reaps the zombie for the exited process. This function may block. @available(*, noasync) - internal func blockingReap() throws(Errno) -> TerminationStatus { + 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? { + internal func reap() throws(Errno) -> (TerminationStatus, ResourceUsage)? { try _reap(pid: value) } } @available(*, noasync) -internal func _blockingReap(pid: pid_t) throws(Errno) -> TerminationStatus { - let siginfo = try _waitid(idtype: P_PID, id: id_t(pid), flags: WEXITED) - return TerminationStatus(siginfo) -} - -internal func _reap(pid: pid_t) throws(Errno) -> TerminationStatus? { - let siginfo = try _waitid(idtype: P_PID, id: id_t(pid), flags: WEXITED | WNOHANG) - // 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 +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) + } } - return TerminationStatus(siginfo) } -internal func _waitid(idtype: idtype_t, id: id_t, flags: Int32) throws(Errno) -> siginfo_t { +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() - if waitid(idtype, id, &siginfo, flags) != -1 { - return siginfo - } else if errno != EINTR { + 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) } } @@ -725,6 +751,20 @@ internal extension TerminationStatus { } } +#if os(macOS) || os(FreeBSD) || os(OpenBSD) +internal extension TerminationStatus { + init(waitStatus: CInt) { + if _was_process_exited(waitStatus) != 0 { + self = .exited(CInt(_get_exit_code(waitStatus))) + } else if _was_process_signaled(waitStatus) != 0 { + self = .signaled(CInt(_get_signal_code(waitStatus))) + } else { + fatalError("Unexpected wait status: \(waitStatus)") + } + } +} +#endif + #if os(OpenBSD) || os(Linux) || os(Android) internal extension siginfo_t { var si_status: Int32 { diff --git a/Sources/Subprocess/Platforms/Subprocess+Windows.swift b/Sources/Subprocess/Platforms/Subprocess+Windows.swift index 98a4ae4e..a6f0bd7b 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Windows.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Windows.swift @@ -610,7 +610,7 @@ extension PlatformOptions: CustomStringConvertible, CustomDebugStringConvertible @Sendable internal func monitorProcessTermination( for processIdentifier: ProcessIdentifier -) async throws(SubprocessError) -> TerminationStatus { +) async throws(SubprocessError) -> (TerminationStatus, ResourceUsage) { // Once the continuation resumes, it will need to unregister the wait, so // yield the wait handle back to the calling scope. var waitHandle: HANDLE? @@ -655,6 +655,9 @@ internal func monitorProcessTermination( } } + // Collect resource usage while the process handle is still valid + let resourceUsage = ResourceUsage(processHandle: processIdentifier.processDescriptor) + var status: DWORD = 0 guard GetExitCodeProcess(processIdentifier.processDescriptor, &status) else { throw SubprocessError.failedToMonitor( @@ -662,7 +665,7 @@ internal func monitorProcessTermination( ) } - return .exited(status) + return (.exited(status), resourceUsage) } // MARK: - Subprocess Control diff --git a/Sources/Subprocess/Result.swift b/Sources/Subprocess/Result.swift index 63a7f3cc..e4b3de6b 100644 --- a/Sources/Subprocess/Result.swift +++ b/Sources/Subprocess/Result.swift @@ -15,6 +15,109 @@ import System import SystemPackage #endif +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#elseif canImport(Android) +import Android +#elseif canImport(WinSDK) +import WinSDK +#endif + +// MARK: - ResourceUsage + +/// Resource usage information for a terminated subprocess. +public struct ResourceUsage: Sendable, Hashable { + /// The total amount of time spent executing in user mode. + public let userTime: Duration + /// The total amount of time spent executing in kernel mode. + public let systemTime: Duration + /// The peak resident set size (maximum memory used), in bytes. + public let maxRSS: Int + + #if !os(Windows) + /// The underlying POSIX resource usage information. + public let rusage: rusage + #endif +} + +extension ResourceUsage { + #if os(Windows) + internal init(processHandle: HANDLE) { + var creationTime = FILETIME() + var exitTime = FILETIME() + var kernelTime = FILETIME() + var userFileTime = FILETIME() + + if GetProcessTimes( + processHandle, + &creationTime, + &exitTime, + &kernelTime, + &userFileTime + ) { + self.userTime = Self.duration(from: userFileTime) + self.systemTime = Self.duration(from: kernelTime) + } else { + self.userTime = .zero + self.systemTime = .zero + } + + var memInfo = PROCESS_MEMORY_COUNTERS() + memInfo.cb = DWORD(MemoryLayout.size) + if K32GetProcessMemoryInfo( + processHandle, + &memInfo, + DWORD(MemoryLayout.size) + ) { + self.maxRSS = Int(memInfo.PeakWorkingSetSize) + } else { + self.maxRSS = 0 + } + } + + private static func duration(from ft: FILETIME) -> Duration { + let hundredNanos = UInt64(ft.dwHighDateTime) << 32 | UInt64(ft.dwLowDateTime) + let seconds = Int64(hundredNanos / 10_000_000) + let remainder = Int64(hundredNanos % 10_000_000) + return Duration( + secondsComponent: seconds, + attosecondsComponent: remainder * 100_000_000_000 + ) + } + #else + internal init(_ usage: rusage) { + self.userTime = Duration( + secondsComponent: Int64(usage.ru_utime.tv_sec), + attosecondsComponent: Int64(usage.ru_utime.tv_usec) * 1_000_000_000_000 + ) + self.systemTime = Duration( + secondsComponent: Int64(usage.ru_stime.tv_sec), + attosecondsComponent: Int64(usage.ru_stime.tv_usec) * 1_000_000_000_000 + ) + #if canImport(Darwin) + self.maxRSS = Int(usage.ru_maxrss) // bytes on Darwin + #else + self.maxRSS = Int(usage.ru_maxrss) * 1024 // KiB to bytes (Linux, FreeBSD, OpenBSD, NetBSD) + #endif + self.rusage = usage + } + #endif +} + +// MARK: - ExecutionSummary Protocol + +/// Protocol providing common properties for subprocess execution results. +public protocol ExecutionSummary: Sendable { + /// The termination status of the child process. + var terminationStatus: TerminationStatus { get } + /// The resource usage of the terminated child process. + var resourceUsage: ResourceUsage { get } +} + // MARK: - Result /// The outcome of a subprocess execution, containing the closure's return value and the termination status of the child process. @@ -23,9 +126,12 @@ public struct ExecutionOutcome: Sendable { public let terminationStatus: TerminationStatus /// The value returned by the closure passed to the `run` method. public let value: Result + /// The resource usage of the terminated child process. + public let resourceUsage: ResourceUsage - internal init(terminationStatus: TerminationStatus, value: Result) { + internal init(terminationStatus: TerminationStatus, resourceUsage: ResourceUsage, value: Result) { self.terminationStatus = terminationStatus + self.resourceUsage = resourceUsage self.value = value } } @@ -43,20 +149,50 @@ public struct ExecutionRecord< public let standardOutput: Output.OutputType /// The collected standard error of the subprocess. public let standardError: Error.OutputType + /// The resource usage of the terminated child process. + public let resourceUsage: ResourceUsage internal init( processIdentifier: ProcessIdentifier, terminationStatus: TerminationStatus, + resourceUsage: ResourceUsage, standardOutput: Output.OutputType, standardError: Error.OutputType ) { self.processIdentifier = processIdentifier self.terminationStatus = terminationStatus + self.resourceUsage = resourceUsage self.standardOutput = standardOutput self.standardError = standardError } } +// MARK: - ExecutionSummary Conformances + +extension ExecutionOutcome: ExecutionSummary {} +extension ExecutionRecord: ExecutionSummary {} + +// MARK: - rusage Conformances +#if !os(Windows) +extension rusage: @retroactive Equatable { + public static func == (lhs: rusage, rhs: rusage) -> Bool { + withUnsafeBytes(of: lhs) { lhsBytes in + withUnsafeBytes(of: rhs) { rhsBytes in + lhsBytes.elementsEqual(rhsBytes) + } + } + } +} + +extension rusage: @retroactive Hashable { + public func hash(into hasher: inout Hasher) { + withUnsafeBytes(of: self) { bytes in + hasher.combine(bytes: bytes) + } + } +} +#endif + // MARK: - ExecutionRecord Conformances extension ExecutionRecord: Equatable where Output.OutputType: Equatable, Error.OutputType: Equatable {} @@ -71,6 +207,7 @@ where Output.OutputType: CustomStringConvertible, Error.OutputType: CustomString ExecutionRecord( processIdentifier: \(self.processIdentifier), terminationStatus: \(self.terminationStatus.description), + resourceUsage: \(self.resourceUsage), standardOutput: \(self.standardOutput.description) standardError: \(self.standardError.description) ) @@ -86,6 +223,7 @@ where Output.OutputType: CustomDebugStringConvertible, Error.OutputType: CustomD ExecutionRecord( processIdentifier: \(self.processIdentifier), terminationStatus: \(self.terminationStatus.description), + resourceUsage: \(self.resourceUsage), standardOutput: \(self.standardOutput.debugDescription) standardError: \(self.standardError.debugDescription) ) @@ -104,6 +242,7 @@ extension ExecutionOutcome: CustomStringConvertible where Result: CustomStringCo return """ ExecutionOutcome( terminationStatus: \(self.terminationStatus.description), + resourceUsage: \(self.resourceUsage), value: \(self.value.description) ) """ @@ -116,6 +255,7 @@ extension ExecutionOutcome: CustomDebugStringConvertible where Result: CustomDeb return """ ExecutionOutcome( terminationStatus: \(self.terminationStatus.debugDescription), + resourceUsage: \(self.resourceUsage), value: \(self.value.debugDescription) ) """ diff --git a/Sources/_SubprocessCShims/include/process_shims.h b/Sources/_SubprocessCShims/include/process_shims.h index c87b9b87..acbf2fe2 100644 --- a/Sources/_SubprocessCShims/include/process_shims.h +++ b/Sources/_SubprocessCShims/include/process_shims.h @@ -16,6 +16,7 @@ #if !TARGET_OS_WINDOWS #include +#include #include #if _POSIX_SPAWN @@ -110,6 +111,8 @@ int _shims_snprintf( #if TARGET_OS_LINUX int _pidfd_open(pid_t pid); +int linux_waitid(idtype_t idtype, id_t id, siginfo_t * _Nonnull infop, int options, struct rusage * _Nonnull rusage); + // P_PIDFD is only defined on Linux Kernel 5.4 and above // Define our value if it's not available #ifndef P_PIDFD diff --git a/Sources/_SubprocessCShims/process_shims.c b/Sources/_SubprocessCShims/process_shims.c index 3aa6180e..85d344df 100644 --- a/Sources/_SubprocessCShims/process_shims.c +++ b/Sources/_SubprocessCShims/process_shims.c @@ -296,6 +296,12 @@ int _pidfd_send_signal(int pidfd, int signal) { return syscall(SYS_pidfd_send_signal, pidfd, signal, NULL, 0); } +// glibc/musl only expose the 4-parameter POSIX waitid variant. +// The Linux kernel's waitid syscall accepts a 5th parameter: struct rusage. +int linux_waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options, struct rusage *rusage) { + return syscall(SYS_waitid, idtype, id, infop, options, rusage); +} + // SYS_clone3 is only defined on Linux Kernel 5.3 and above // Define our dummy value if it's not available (as is the case with Musl libc) #ifndef SYS_clone3 diff --git a/Tests/SubprocessTests/ProcessMonitoringTests.swift b/Tests/SubprocessTests/ProcessMonitoringTests.swift index afd8960b..d48c5a54 100644 --- a/Tests/SubprocessTests/ProcessMonitoringTests.swift +++ b/Tests/SubprocessTests/ProcessMonitoringTests.swift @@ -121,10 +121,9 @@ extension SubprocessProcessMonitoringTests { @Test func testNormalExit() async throws { let config = self.immediateExitProcess(withExitCode: 0) try await withSpawnedExecution(config: config) { execution in - let monitorResult = try await monitorProcessTermination( + let (monitorResult, _) = try await monitorProcessTermination( for: execution.processIdentifier ) - #expect(monitorResult.isSuccess) } } @@ -132,10 +131,9 @@ extension SubprocessProcessMonitoringTests { @Test func testExitCode() async throws { let config = self.immediateExitProcess(withExitCode: 42) try await withSpawnedExecution(config: config) { execution in - let monitorResult = try await monitorProcessTermination( + let (monitorResult, _) = try await monitorProcessTermination( for: execution.processIdentifier ) - #expect(monitorResult == .exited(42)) } } @@ -150,7 +148,7 @@ extension SubprocessProcessMonitoringTests { // Send signal to process try execution.send(signal: .terminate) - let result = try await monitorProcessTermination( + let (result, _) = try await monitorProcessTermination( for: execution.processIdentifier ) #expect(result == .signaled(SIGTERM)) @@ -180,7 +178,7 @@ extension SubprocessProcessMonitoringTests { ) #endif // Now make sure monitorProcessTermination() can still get the correct result - let monitorResult = try await monitorProcessTermination( + let (monitorResult, _) = try await monitorProcessTermination( for: execution.processIdentifier ) #expect(monitorResult == TerminationStatus.exited(0)) @@ -190,10 +188,9 @@ extension SubprocessProcessMonitoringTests { @Test func testCanMonitorLongRunningProcess() async throws { let config = self.longRunningProcess(withTimeOutSeconds: 1) try await withSpawnedExecution(config: config) { execution in - let monitorResult = try await monitorProcessTermination( + let (monitorResult, _) = try await monitorProcessTermination( for: execution.processIdentifier ) - #expect(monitorResult.isSuccess) } } @@ -228,7 +225,7 @@ extension SubprocessProcessMonitoringTests { try await withSpawnedExecution(config: child1) { child1Execution in try await withSpawnedExecution(config: child2) { child2Execution in // Monitor child2, but make sure we don't reap child1's status - let status = try await monitorProcessTermination( + let (status, _) = try await monitorProcessTermination( for: child2Execution.processIdentifier ) #expect(status.isSuccess) @@ -276,7 +273,7 @@ extension SubprocessProcessMonitoringTests { ) try await withSpawnedExecution(config: config) { execution in - let monitorResult = try await monitorProcessTermination( + let (monitorResult, _) = try await monitorProcessTermination( for: execution.processIdentifier ) #expect(monitorResult.isSuccess) @@ -313,7 +310,7 @@ extension SubprocessProcessMonitoringTests { try await withThrowingTaskGroup { group in for pid in spawnedProcesses { group.addTask { - let status = try await monitorProcessTermination(for: pid) + let (status, _) = try await monitorProcessTermination(for: pid) #expect(status.isSuccess) } }