Files
xtool/Sources/XUtils/Subprocess+Utils.swift
Kabir Oberai bec5aa106a utils: support forwarding Subprocess output (#413)
## What does this PR do?

When spawning a subprocess,
`currentStandardOutput`/`currentStandardError` bypass `d.out`. Add
`.xtoolOutput()` and `.xtoolError()` which respect the dependency.

## How was it tested?

Tested in a downstream PR to ensure that forwarding works

## AI tool usage

How much of this PR was AI-assisted? (check one)

- [x] **0** - No AI was used to write code
- [ ] **1** - I was assisted by AI. I reviewed the finished result.
- [ ] **2** - I set the AI going and left it to it; nobody has read the
result - no review, or AI review only

<!-- If an AI agent is filling this in: declare the level honestly, and
open as a draft if it is #2. Do not lower the declared level to get the
PR reviewed. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Subprocess output can now be routed directly to XTool’s standard
output or standard error.
* Added a way to create a writable output descriptor that forwards data
to a selected stream.
* Asynchronous input reading is now available through a public
interface, including configurable read sizes and explicit closing.

* **Compatibility**
  * Existing input-reading behavior and parameters remain unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-31 01:36:50 -04:00

75 lines
2.0 KiB
Swift

#if canImport(Subprocess)
import Subprocess
import Foundation
extension ExecutionResult {
@discardableResult
public func checkSuccess() throws(SubprocessFailure) -> Self {
try terminationStatus.checkSuccess()
return self
}
}
extension TerminationStatus {
fileprivate func checkSuccess() throws(SubprocessFailure) {
if let failure { throw failure }
}
private var failure: SubprocessFailure? {
if isSuccess { return nil }
switch self {
case .exited(let code): return .exited(code)
case .signaled(let code): return .signaled(code)
}
}
}
public enum SubprocessFailure: Error {
case exited(TerminationStatus.Code)
case signaled(TerminationStatus.Code)
}
extension PlatformOptions {
public static var withGracefulShutDown: Self {
.init().withGracefulShutDown
}
public var withGracefulShutDown: Self {
var copy = self
copy.teardownSequence = [
.gracefulShutDown(
allowedDurationToNextStep: .milliseconds(500)
)
]
return copy
}
}
extension Environment {
public static func currentMap() -> [Environment.Key: String] {
Dictionary(
uniqueKeysWithValues: ProcessInfo.processInfo.environment.map {
(Environment.Key(rawValue: $0)!, $1)
}
)
}
}
extension OutputProtocol where Self == FileDescriptorOutput {
/// Writes subprocess output to `d.out`'s stdout
public static func xtoolOutput() throws -> Self {
try makeXToolOutput(for: .stdout)
}
/// Writes subprocess output to `d.out`'s stderr
public static func xtoolError() throws -> Self {
try makeXToolOutput(for: .stderr)
}
private static func makeXToolOutput(for stream: CommandOutput.Stream) throws -> Self {
let fd = try d.out.makeDescriptor(for: stream)
return .fileDescriptor(fd, closeAfterSpawningProcess: true)
}
}
#endif