Skip to content

Commit 17c268b

Browse files
committed
Introduce ProcessMonitoringTests
1 parent e6cc238 commit 17c268b

File tree

7 files changed

+465
-53
lines changed

7 files changed

+465
-53
lines changed

Sources/Subprocess/Error.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ extension SubprocessError: CustomStringConvertible, CustomDebugStringConvertible
111111
case .failedToWriteToSubprocess:
112112
return "Failed to write bytes to the child process."
113113
case .failedToMonitorProcess:
114-
return "Failed to monitor the state of child process with underlying error: \(self.underlyingError!)"
114+
return "Failed to monitor the state of child process with underlying error: \(self.underlyingError.map { "\($0)" } ?? "nil")"
115115
case .streamOutputExceedsLimit(let limit):
116116
return "Failed to create output from current buffer because the output limit (\(limit)) was reached."
117117
case .asyncIOFailed(let reason):

Sources/Subprocess/Platforms/Subprocess+Linux.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ private struct MonitorThreadContext: Sendable {
447447
}
448448
}
449449

450-
private extension siginfo_t {
450+
internal extension siginfo_t {
451451
var si_status: Int32 {
452452
#if canImport(Glibc)
453453
return _sifields._sigchld.si_status
@@ -700,7 +700,7 @@ private let setup: () = {
700700
}()
701701

702702

703-
private func _setupMonitorSignalHandler() {
703+
internal func _setupMonitorSignalHandler() {
704704
// Only executed once
705705
setup
706706
}

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ internal func monitorProcessTermination(
592592
}
593593
}
594594

595-
try? await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
595+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
596596
// Set up a callback that immediately resumes the continuation and does no
597597
// other work.
598598
let context = Unmanaged.passRetained(continuation as AnyObject).toOpaque()
@@ -804,14 +804,14 @@ extension Executable {
804804
}
805805
// 1. The directory from which the application loaded.
806806
let applicationDirectory = try? fillNullTerminatedWideStringBuffer(
807-
initialSize: DWORD(MAX_PATH), maxSize: DWORD(MAX_PATH)
807+
initialSize: DWORD(MAX_PATH), maxSize: DWORD(Int16.max)
808808
) {
809809
return GetModuleFileNameW(nil, $0.baseAddress, DWORD($0.count))
810810
}
811811
if let applicationDirectory {
812812
insertExecutableAddingExtension(
813813
name,
814-
currentPath: applicationDirectory,
814+
currentPath: FilePath(applicationDirectory).removingLastComponent().string,
815815
pathExtensions: pathExtensions,
816816
storage: &possiblePaths
817817
)
@@ -820,7 +820,7 @@ extension Executable {
820820
let directorySize = GetCurrentDirectoryW(0, nil)
821821
let currentDirectory = try? fillNullTerminatedWideStringBuffer(
822822
initialSize: directorySize >= 0 ? directorySize : DWORD(MAX_PATH),
823-
maxSize: DWORD(MAX_PATH)
823+
maxSize: DWORD(Int16.max)
824824
) {
825825
return GetCurrentDirectoryW(DWORD($0.count), $0.baseAddress)
826826
}
@@ -836,7 +836,7 @@ extension Executable {
836836
let systemDirectorySize = GetSystemDirectoryW(nil, 0)
837837
let systemDirectory = try? fillNullTerminatedWideStringBuffer(
838838
initialSize: systemDirectorySize >= 0 ? systemDirectorySize : DWORD(MAX_PATH),
839-
maxSize: DWORD(MAX_PATH)
839+
maxSize: DWORD(Int16.max)
840840
) {
841841
return GetSystemDirectoryW($0.baseAddress, DWORD($0.count))
842842
}
@@ -848,24 +848,13 @@ extension Executable {
848848
storage: &possiblePaths
849849
)
850850
}
851-
// 4. 16 bit Systen Directory
852-
// Windows documentation stats that
853-
// "No such standard function (similar to GetSystemDirectory)
854-
// exists for the 16-bit system folder". Use C:\Windows\System instead
855-
let systemDirectory16 = FilePath(#"C:\Windows\System"#)
856-
insertExecutableAddingExtension(
857-
name,
858-
currentPath: systemDirectory16.string,
859-
pathExtensions: pathExtensions,
860-
storage: &possiblePaths
861-
)
862-
// 5. The Windows directory
863-
let windowsDirectorySize = GetSystemWindowsDirectoryW(nil, 0)
851+
// 4. The Windows directory
852+
let windowsDirectorySize = GetWindowsDirectoryW(nil, 0)
864853
let windowsDirectory = try? fillNullTerminatedWideStringBuffer(
865854
initialSize: windowsDirectorySize >= 0 ? windowsDirectorySize : DWORD(MAX_PATH),
866-
maxSize: DWORD(MAX_PATH)
855+
maxSize: DWORD(Int16.max)
867856
) {
868-
return GetSystemWindowsDirectoryW($0.baseAddress, DWORD($0.count))
857+
return GetWindowsDirectoryW($0.baseAddress, DWORD($0.count))
869858
}
870859
if let windowsDirectory {
871860
insertExecutableAddingExtension(
@@ -874,6 +863,18 @@ extension Executable {
874863
pathExtensions: pathExtensions,
875864
storage: &possiblePaths
876865
)
866+
867+
// 5. 16 bit Systen Directory
868+
// Windows documentation stats that "No such standard function
869+
// (similar to GetSystemDirectory) exists for the 16-bit system folder".
870+
// Use "\(windowsDirectory)\System" instead
871+
let systemDirectory16 = FilePath(windowsDirectory).appending("System")
872+
insertExecutableAddingExtension(
873+
name,
874+
currentPath: systemDirectory16.string,
875+
pathExtensions: pathExtensions,
876+
storage: &possiblePaths
877+
)
877878
}
878879
// 6. The directories that are listed in the PATH environment variable
879880
if let pathValue {
@@ -896,19 +897,17 @@ extension Executable {
896897

897898
// MARK: - Environment Resolution
898899
extension Environment {
899-
internal static let pathVariableName = "Path"
900-
901900
internal func pathValue() -> String? {
902901
switch self.config {
903902
case .inherit(let overrides):
904903
// If PATH value exists in overrides, use it
905-
if let value = overrides[Self.pathVariableName] {
904+
if let value = overrides.pathValue() {
906905
return value
907906
}
908907
// Fall back to current process
909-
return Self.currentEnvironmentValues()[Self.pathVariableName]
908+
return Self.currentEnvironmentValues().pathValue()
910909
case .custom(let fullEnvironment):
911-
if let value = fullEnvironment[Self.pathVariableName] {
910+
if let value = fullEnvironment.pathValue() {
912911
return value
913912
}
914913
return nil
@@ -992,11 +991,10 @@ extension Configuration {
992991
}
993992
// On Windows, the PATH is required in order to locate dlls needed by
994993
// the process so we should also pass that to the child
995-
let pathVariableName = Environment.pathVariableName
996-
if env[pathVariableName] == nil,
997-
let parentPath = Environment.currentEnvironmentValues()[pathVariableName]
994+
if env.pathValue() == nil,
995+
let parentPath = Environment.currentEnvironmentValues().pathValue()
998996
{
999-
env[pathVariableName] = parentPath
997+
env["Path"] = parentPath
1000998
}
1001999
// The environment string must be terminated by a double
10021000
// null-terminator. Otherwise, CreateProcess will fail with
@@ -1472,5 +1470,12 @@ internal func fillNullTerminatedWideStringBuffer(
14721470
throw SubprocessError.UnderlyingError(rawValue: DWORD(ERROR_INSUFFICIENT_BUFFER))
14731471
}
14741472

1473+
// Windows environment key is case insensitive
1474+
extension Dictionary where Key == String, Value == String {
1475+
internal func pathValue() -> String? {
1476+
return self["Path"] ?? self["PATH"] ?? self["path"]
1477+
}
1478+
}
1479+
14751480

14761481
#endif // canImport(WinSDK)

Tests/SubprocessTests/DarwinTests.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ struct SubprocessDarwinTests {
3939
#expect(idResult.terminationStatus == .exited(1234567))
4040
}
4141

42-
@Test func testSubprocessPlatformOptionsPreExecProcessActionAndProcessConfigurator() async throws {
42+
@Test(
43+
.disabled("Constantly fails on macOS 26 and Swift 6.2"),
44+
.bug("https://github.com/swiftlang/swift-subprocess/issues/148")
45+
) func testSubprocessPlatformOptionsPreExecProcessActionAndProcessConfigurator() async throws {
4346
let (readFD, writeFD) = try FileDescriptor.pipe()
4447
try await readFD.closeAfter {
4548
let childPID = try await writeFD.closeAfter {
@@ -165,4 +168,38 @@ struct SubprocessDarwinTests {
165168
}
166169
}
167170

171+
extension FileDescriptor {
172+
internal func readUntilEOF(upToLength maxLength: Int) async throws -> Data {
173+
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Data, any Error>) in
174+
let dispatchIO = DispatchIO(
175+
type: .stream,
176+
fileDescriptor: self.rawValue,
177+
queue: .global()
178+
) { error in
179+
if error != 0 {
180+
continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV))
181+
}
182+
}
183+
var buffer: Data = Data()
184+
dispatchIO.read(
185+
offset: 0,
186+
length: maxLength,
187+
queue: .global()
188+
) { done, data, error in
189+
guard error == 0 else {
190+
continuation.resume(throwing: POSIXError(.init(rawValue: error) ?? .ENODEV))
191+
return
192+
}
193+
if let data = data {
194+
buffer += Data(data)
195+
}
196+
if done {
197+
dispatchIO.close()
198+
continuation.resume(returning: buffer)
199+
}
200+
}
201+
}
202+
}
203+
}
204+
168205
#endif // canImport(Darwin)

Tests/SubprocessTests/IntegrationTests.swift

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,11 @@ extension SubprocessIntegrationTests {
9090
@Test func testExecutableAtPath() async throws {
9191
#if os(Windows)
9292
let cmdExe = ProcessInfo.processInfo.environment["COMSPEC"] ??
93-
#"C:\Windows\System32\cmd.exe"#
93+
ProcessInfo.processInfo.environment["ComSpec"] ??
94+
ProcessInfo.processInfo.environment["comspec"]
95+
9496
let setup = TestSetup(
95-
executable: .path(FilePath(cmdExe)),
97+
executable: .path(FilePath(try #require(cmdExe))),
9698
arguments: .init(["/c", "cd"])
9799
)
98100
#else
@@ -169,8 +171,6 @@ extension SubprocessIntegrationTests {
169171
)
170172
}
171173

172-
// Windows does not support argument 0 override
173-
// This test will not compile on Windows
174174
@Test func testArgumentsOverride() async throws {
175175
#if os(Windows)
176176
let setup = TestSetup(
@@ -255,7 +255,10 @@ extension SubprocessIntegrationTests {
255255
#expect(result.terminationStatus.isSuccess)
256256
let pathValue = try #require(result.standardOutput)
257257
#if os(Windows)
258-
let expected = try #require(ProcessInfo.processInfo.environment["Path"])
258+
let expectedPathValue = ProcessInfo.processInfo.environment["Path"] ??
259+
ProcessInfo.processInfo.environment["PATH"] ??
260+
ProcessInfo.processInfo.environment["path"]
261+
let expected = try #require(expectedPathValue)
259262
#else
260263
let expected = try #require(ProcessInfo.processInfo.environment["PATH"])
261264
#endif
@@ -317,11 +320,14 @@ extension SubprocessIntegrationTests {
317320
)
318321
func testEnvironmentCustom() async throws {
319322
#if os(Windows)
323+
let pathValue = ProcessInfo.processInfo.environment["Path"] ??
324+
ProcessInfo.processInfo.environment["PATH"] ??
325+
ProcessInfo.processInfo.environment["path"]
320326
let setup = TestSetup(
321327
executable: .name("cmd.exe"),
322328
arguments: ["/c", "set"],
323329
environment: .custom([
324-
"Path": try #require(ProcessInfo.processInfo.environment["Path"]),
330+
"Path": try #require(pathValue),
325331
"ComSpec": try #require(ProcessInfo.processInfo.environment["ComSpec"]),
326332
])
327333
)
@@ -1577,12 +1583,13 @@ extension SubprocessIntegrationTests {
15771583
@Test func testTerminateProcess() async throws {
15781584
#if os(Windows)
15791585
let setup = TestSetup(
1580-
executable: .name("cmd.exe"),
1581-
arguments: ["/c", "timeout /t 99999 >nul"])
1586+
executable: .name("powershell.exe"),
1587+
arguments: ["-Command", "Start-Sleep -Seconds 9999"]
1588+
)
15821589
#else
15831590
let setup = TestSetup(
1584-
executable: .path("/bin/sleep"),
1585-
arguments: ["infinite"]
1591+
executable: .path("/usr/bin/tail"),
1592+
arguments: ["-f", "/dev/null"]
15861593
)
15871594
#endif
15881595
let stuckResult = try await _run(
@@ -1789,13 +1796,13 @@ extension SubprocessIntegrationTests {
17891796

17901797
#if os(Windows)
17911798
let setup = TestSetup(
1792-
executable: .name("cmd.exe"),
1793-
arguments: ["/c", "timeout /t 100000 /nobreak"]
1799+
executable: .name("powershell.exe"),
1800+
arguments: ["-Command", "Start-Sleep -Seconds 9999"]
17941801
)
17951802
#else
17961803
let setup = TestSetup(
1797-
executable: .path("/bin/sleep"),
1798-
arguments: ["100000"]
1804+
executable: .path("/usr/bin/tail"),
1805+
arguments: ["-f", "/dev/null"]
17991806
)
18001807
#endif
18011808
for i in 0 ..< 100 {
@@ -1811,7 +1818,7 @@ extension SubprocessIntegrationTests {
18111818
setup,
18121819
platformOptions: platformOptions,
18131820
input: .none,
1814-
output: .string(limit: .max),
1821+
output: .discarded,
18151822
error: .discarded
18161823
).terminationStatus
18171824
}
@@ -2267,3 +2274,29 @@ func _run<Result>(
22672274
)
22682275
}
22692276

2277+
extension FileDescriptor {
2278+
/// Runs a closure and then closes the FileDescriptor, even if an error occurs.
2279+
///
2280+
/// - Parameter body: The closure to run.
2281+
/// If the closure throws an error,
2282+
/// this method closes the file descriptor before it rethrows that error.
2283+
///
2284+
/// - Returns: The value returned by the closure.
2285+
///
2286+
/// If `body` throws an error
2287+
/// or an error occurs while closing the file descriptor,
2288+
/// this method rethrows that error.
2289+
public func closeAfter<R>(_ body: () async throws -> R) async throws -> R {
2290+
// No underscore helper, since the closure's throw isn't necessarily typed.
2291+
let result: R
2292+
do {
2293+
result = try await body()
2294+
} catch {
2295+
_ = try? self.close() // Squash close error and throw closure's
2296+
throw error
2297+
}
2298+
try self.close()
2299+
return result
2300+
}
2301+
}
2302+

0 commit comments

Comments
 (0)