Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 69 additions & 64 deletions Sources/Subprocess/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,38 +79,42 @@ public struct Configuration: Sendable {

let execution = _spawnResult.execution

let result: Swift.Result<Result, Error>
do {
result = try await .success(withAsyncTaskCleanupHandler {
let inputIO = _spawnResult.inputWriteEnd()
let outputIO = _spawnResult.outputReadEnd()
let errorIO = _spawnResult.errorReadEnd()
return try await withAsyncTaskCleanupHandler {
let inputIO = _spawnResult.inputWriteEnd()
let outputIO = _spawnResult.outputReadEnd()
let errorIO = _spawnResult.errorReadEnd()

let result: Swift.Result<Result, Error>
do {
// Body runs in the same isolation
return try await body(_spawnResult.execution, inputIO, outputIO, errorIO)
} onCleanup: {
// Attempt to terminate the child process
await execution.runTeardownSequence(
self.platformOptions.teardownSequence
)
})
} catch {
result = .failure(error)
}
let bodyResult = try await body(_spawnResult.execution, inputIO, outputIO, errorIO)
result = .success(bodyResult)
} catch {
result = .failure(error)
}

// Ensure that we begin monitoring process termination after `body` runs
// and regardless of whether `body` throws, so that the pid gets reaped
// 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 Subprocess.monitorProcessTermination(
for: execution.processIdentifier
)
// Ensure that we begin monitoring process termination after `body` runs
// and regardless of whether `body` throws, so that the pid gets reaped
// 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(
for: execution.processIdentifier
)

// Close process file descriptor now we finished monitoring
execution.processIdentifier.close()
// Close process file descriptor now we finished monitoring
execution.processIdentifier.close()

return try ExecutionResult(terminationStatus: terminationStatus, value: result.get())
return ExecutionResult(
terminationStatus: terminationStatus,
value: try result.get()
)
} onCleanup: {
// Attempt to terminate the child process
await execution.runTeardownSequence(
self.platformOptions.teardownSequence
)
}
}
}

Expand Down Expand Up @@ -246,40 +250,6 @@ extension Executable: CustomStringConvertible, CustomDebugStringConvertible {
}
}

extension Executable {
internal func possibleExecutablePaths(
withPathValue pathValue: String?
) -> Set<String> {
switch self.storage {
case .executable(let executableName):
#if os(Windows)
// Windows CreateProcessW accepts executable name directly
return Set([executableName])
#else
var results: Set<String> = []
// executableName could be a full path
results.insert(executableName)
// Get $PATH from environment
let searchPaths: Set<String>
if let pathValue = pathValue {
let localSearchPaths = pathValue.split(separator: ":").map { String($0) }
searchPaths = Set(localSearchPaths).union(Self.defaultSearchPaths)
} else {
searchPaths = Self.defaultSearchPaths
}
for path in searchPaths {
results.insert(
FilePath(path).appending(executableName).string
)
}
return results
#endif
case .path(let executablePath):
return Set([executablePath.string])
}
}
}

// MARK: - Arguments

/// A collection of arguments to pass to the subprocess.
Expand All @@ -300,7 +270,6 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
self.executablePathOverride = nil
}

#if !os(Windows) // Windows does NOT support arg0 override
/// Create an `Argument` object using the given values, but
/// override the first Argument value to `executablePathOverride`.
/// If `executablePathOverride` is nil,
Expand All @@ -317,7 +286,7 @@ public struct Arguments: Sendable, ExpressibleByArrayLiteral, Hashable {
self.executablePathOverride = nil
}
}

#if !os(Windows) // Windows does not support non-unicode arguments
/// Create an `Argument` object using the given values, but
/// override the first Argument value to `executablePathOverride`.
/// If `executablePathOverride` is nil,
Expand Down Expand Up @@ -869,7 +838,7 @@ internal struct CreatedPipe: ~Copyable {
DWORD(readBufferSize),
DWORD(readBufferSize),
0,
&saAttributes
nil
)
}
guard let parentEnd, parentEnd != INVALID_HANDLE_VALUE else {
Expand Down Expand Up @@ -1029,3 +998,39 @@ internal func withAsyncTaskCleanupHandler<Result>(
}
}
}

internal struct _OrderedSet<Element: Hashable & Sendable>: Hashable, Sendable {
private var elements: [Element]
private var hashValueSet: Set<Int>

internal init() {
self.elements = []
self.hashValueSet = Set()
}

internal init(_ arrayValue: [Element]) {
self.elements = []
self.hashValueSet = Set()

for element in arrayValue {
self.insert(element)
}
}

mutating func insert(_ element: Element) {
guard !self.hashValueSet.contains(element.hashValue) else {
return
}
self.elements.append(element)
self.hashValueSet.insert(element.hashValue)
}
}

extension _OrderedSet : Sequence {
typealias Iterator = Array<Element>.Iterator

internal func makeIterator() -> Iterator {
return self.elements.makeIterator()
}
}

2 changes: 1 addition & 1 deletion Sources/Subprocess/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible
case .failedToWriteToSubprocess:
return "Failed to write bytes to the child process."
case .failedToMonitorProcess:
return "Failed to monitor the state of child process with underlying error: \(self.underlyingError!)"
return "Failed to monitor the state of child process with underlying error: \(self.underlyingError.map { "\($0)" } ?? "nil")"
case .streamOutputExceedsLimit(let limit):
return "Failed to create output from current buffer because the output limit (\(limit)) was reached."
case .asyncIOFailed(let reason):
Expand Down
4 changes: 3 additions & 1 deletion Sources/Subprocess/IO/AsyncIO+Darwin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ internal import Dispatch
final class AsyncIO: Sendable {
static let shared: AsyncIO = AsyncIO()

private init() {}
internal init() {}

internal func shutdown() { /* noop on Darwin */ }

internal func read(
from diskIO: borrowing IOChannel,
Expand Down
Loading