Skip to content

Commit e6e13a8

Browse files
committed
Change runDetached to return an Execution instead of a ProcessIdentifier and make the HANDLE public
Currently runDetached returns a ProcessIdentifier, which on Unix is a pid and on Windows is a numeric process identifier. Similar to #92, there is a Windows-specific race condition here. On Unix the pid will be valid until someone waitid's it. But on Windows the ProcessIdentifier may already be invalid by the time runDetached returns. This patch builds on #93 (where the HANDLE is stored in the Execution instead of being closed immediately), and now provides that HANDLE as part of the public Execution API. This allows callers to properly free the handle using CloseHandle, as well as not having to reverse engineer the handle from the numeric pid. #93 by itself would solve the race condition but introduce a resource leak. The API now also better matches the Unix behavior since it is the caller's responsibility to waitid the pid to clean up the resources, which is semantically similar to calling CloseHandle on Windows. Closes #94
1 parent 7f1597d commit e6e13a8

File tree

8 files changed

+29
-131
lines changed

8 files changed

+29
-131
lines changed

Sources/Subprocess/API.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ public func runDetached(
614614
input: FileDescriptor? = nil,
615615
output: FileDescriptor? = nil,
616616
error: FileDescriptor? = nil
617-
) throws -> ProcessIdentifier {
617+
) throws -> Execution {
618618
let config: Configuration = Configuration(
619619
executable: executable,
620620
arguments: arguments,
@@ -643,7 +643,7 @@ public func runDetached(
643643
input: FileDescriptor? = nil,
644644
output: FileDescriptor? = nil,
645645
error: FileDescriptor? = nil
646-
) throws -> ProcessIdentifier {
646+
) throws -> Execution {
647647
switch (input, output, error) {
648648
case (.none, .none, .none):
649649
let processInput = NoInput()
@@ -653,7 +653,7 @@ public func runDetached(
653653
withInput: try processInput.createPipe(),
654654
outputPipe: try processOutput.createPipe(),
655655
errorPipe: try processError.createPipe()
656-
).execution.processIdentifier
656+
).execution
657657
case (.none, .none, .some(let errorFd)):
658658
let processInput = NoInput()
659659
let processOutput = DiscardedOutput()
@@ -665,7 +665,7 @@ public func runDetached(
665665
withInput: try processInput.createPipe(),
666666
outputPipe: try processOutput.createPipe(),
667667
errorPipe: try processError.createPipe()
668-
).execution.processIdentifier
668+
).execution
669669
case (.none, .some(let outputFd), .none):
670670
let processInput = NoInput()
671671
let processOutput = FileDescriptorOutput(
@@ -676,7 +676,7 @@ public func runDetached(
676676
withInput: try processInput.createPipe(),
677677
outputPipe: try processOutput.createPipe(),
678678
errorPipe: try processError.createPipe()
679-
).execution.processIdentifier
679+
).execution
680680
case (.none, .some(let outputFd), .some(let errorFd)):
681681
let processInput = NoInput()
682682
let processOutput = FileDescriptorOutput(
@@ -691,7 +691,7 @@ public func runDetached(
691691
withInput: try processInput.createPipe(),
692692
outputPipe: try processOutput.createPipe(),
693693
errorPipe: try processError.createPipe()
694-
).execution.processIdentifier
694+
).execution
695695
case (.some(let inputFd), .none, .none):
696696
let processInput = FileDescriptorInput(
697697
fileDescriptor: inputFd,
@@ -703,7 +703,7 @@ public func runDetached(
703703
withInput: try processInput.createPipe(),
704704
outputPipe: try processOutput.createPipe(),
705705
errorPipe: try processError.createPipe()
706-
).execution.processIdentifier
706+
).execution
707707
case (.some(let inputFd), .none, .some(let errorFd)):
708708
let processInput = FileDescriptorInput(
709709
fileDescriptor: inputFd, closeAfterSpawningProcess: false
@@ -717,7 +717,7 @@ public func runDetached(
717717
withInput: try processInput.createPipe(),
718718
outputPipe: try processOutput.createPipe(),
719719
errorPipe: try processError.createPipe()
720-
).execution.processIdentifier
720+
).execution
721721
case (.some(let inputFd), .some(let outputFd), .none):
722722
let processInput = FileDescriptorInput(
723723
fileDescriptor: inputFd,
@@ -732,7 +732,7 @@ public func runDetached(
732732
withInput: try processInput.createPipe(),
733733
outputPipe: try processOutput.createPipe(),
734734
errorPipe: try processError.createPipe()
735-
).execution.processIdentifier
735+
).execution
736736
case (.some(let inputFd), .some(let outputFd), .some(let errorFd)):
737737
let processInput = FileDescriptorInput(
738738
fileDescriptor: inputFd,
@@ -750,7 +750,7 @@ public func runDetached(
750750
withInput: try processInput.createPipe(),
751751
outputPipe: try processOutput.createPipe(),
752752
errorPipe: try processError.createPipe()
753-
).execution.processIdentifier
753+
).execution
754754
}
755755
}
756756

Sources/Subprocess/Configuration.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,11 @@ public struct Configuration: Sendable {
7171
outputPipe: output,
7272
errorPipe: error
7373
)
74-
let pid = spawnResults.execution.processIdentifier
7574

7675
var spawnResultBox: SpawnResult?? = consume spawnResults
7776
var _spawnResult = spawnResultBox!.take()!
7877

79-
let processIdentifier = _spawnResult.execution.processIdentifier
78+
let execution = _spawnResult.execution
8079

8180
let result: Swift.Result<Result, Error>
8281
do {
@@ -89,9 +88,8 @@ public struct Configuration: Sendable {
8988
return try await body(_spawnResult.execution, inputIO, outputIO, errorIO)
9089
} onCleanup: {
9190
// Attempt to terminate the child process
92-
await Execution.runTeardownSequence(
93-
self.platformOptions.teardownSequence,
94-
on: pid
91+
await execution.runTeardownSequence(
92+
self.platformOptions.teardownSequence
9593
)
9694
})
9795
} catch {

Sources/Subprocess/Execution.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public struct Execution: Sendable {
3535
public let processIdentifier: ProcessIdentifier
3636

3737
#if os(Windows)
38-
internal nonisolated(unsafe) let processHandle: HANDLE
38+
public nonisolated(unsafe) let processHandle: HANDLE
3939
internal let consoleBehavior: PlatformOptions.ConsoleBehavior
4040

4141
init(

Sources/Subprocess/Platforms/Subprocess+Unix.swift

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,18 +117,6 @@ extension Execution {
117117
public func send(
118118
signal: Signal,
119119
toProcessGroup shouldSendToProcessGroup: Bool = false
120-
) throws {
121-
try Self.send(
122-
signal: signal,
123-
to: self.processIdentifier,
124-
toProcessGroup: shouldSendToProcessGroup
125-
)
126-
}
127-
128-
internal static func send(
129-
signal: Signal,
130-
to processIdentifier: ProcessIdentifier,
131-
toProcessGroup shouldSendToProcessGroup: Bool
132120
) throws {
133121
let pid = shouldSendToProcessGroup ? -(processIdentifier.value) : processIdentifier.value
134122
guard kill(pid, signal.rawValue) == 0 else {

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -501,29 +501,6 @@ extension Execution {
501501
/// Terminate the current subprocess with the given exit code
502502
/// - Parameter exitCode: The exit code to use for the subprocess.
503503
public func terminate(withExitCode exitCode: DWORD) throws {
504-
try Self.terminate(self.processIdentifier, withExitCode: exitCode)
505-
}
506-
507-
internal static func terminate(
508-
_ processIdentifier: ProcessIdentifier,
509-
withExitCode exitCode: DWORD
510-
) throws {
511-
guard
512-
let processHandle = OpenProcess(
513-
// PROCESS_ALL_ACCESS
514-
DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF),
515-
false,
516-
processIdentifier.value
517-
)
518-
else {
519-
throw SubprocessError(
520-
code: .init(.failedToTerminate),
521-
underlyingError: .init(rawValue: GetLastError())
522-
)
523-
}
524-
defer {
525-
CloseHandle(processHandle)
526-
}
527504
guard TerminateProcess(processHandle, exitCode) else {
528505
throw SubprocessError(
529506
code: .init(.failedToTerminate),
@@ -534,23 +511,6 @@ extension Execution {
534511

535512
/// Suspend the current subprocess
536513
public func suspend() throws {
537-
guard
538-
let processHandle = OpenProcess(
539-
// PROCESS_ALL_ACCESS
540-
DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF),
541-
false,
542-
self.processIdentifier.value
543-
)
544-
else {
545-
throw SubprocessError(
546-
code: .init(.failedToSuspend),
547-
underlyingError: .init(rawValue: GetLastError())
548-
)
549-
}
550-
defer {
551-
CloseHandle(processHandle)
552-
}
553-
554514
let NTSuspendProcess: (@convention(c) (HANDLE) -> LONG)? =
555515
unsafeBitCast(
556516
GetProcAddress(
@@ -575,23 +535,6 @@ extension Execution {
575535

576536
/// Resume the current subprocess after suspension
577537
public func resume() throws {
578-
guard
579-
let processHandle = OpenProcess(
580-
// PROCESS_ALL_ACCESS
581-
DWORD(STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF),
582-
false,
583-
self.processIdentifier.value
584-
)
585-
else {
586-
throw SubprocessError(
587-
code: .init(.failedToResume),
588-
underlyingError: .init(rawValue: GetLastError())
589-
)
590-
}
591-
defer {
592-
CloseHandle(processHandle)
593-
}
594-
595538
let NTResumeProcess: (@convention(c) (HANDLE) -> LONG)? =
596539
unsafeBitCast(
597540
GetProcAddress(

Sources/Subprocess/Teardown.swift

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,8 @@ extension Execution {
7878
/// Teardown sequence always ends with a `.kill` signal
7979
/// - Parameter sequence: The steps to perform.
8080
public func teardown(using sequence: some Sequence<TeardownStep> & Sendable) async {
81-
await Self.runTeardownSequence(sequence, on: self.processIdentifier)
82-
}
83-
84-
internal static func teardown(
85-
using sequence: some Sequence<TeardownStep> & Sendable,
86-
on processIdentifier: ProcessIdentifier
87-
) async {
8881
await withUncancelledTask {
89-
await Self.runTeardownSequence(sequence, on: processIdentifier)
82+
await runTeardownSequence(sequence)
9083
}
9184
}
9285
}
@@ -98,25 +91,10 @@ internal enum TeardownStepCompletion {
9891
}
9992

10093
extension Execution {
101-
internal static func gracefulShutDown(
102-
_ processIdentifier: ProcessIdentifier,
94+
internal func gracefulShutDown(
10395
allowedDurationToNextStep duration: Duration
10496
) async {
10597
#if os(Windows)
106-
guard
107-
let processHandle = OpenProcess(
108-
DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE),
109-
false,
110-
processIdentifier.value
111-
)
112-
else {
113-
// Nothing more we can do
114-
return
115-
}
116-
defer {
117-
CloseHandle(processHandle)
118-
}
119-
12098
// 1. Attempt to send WM_CLOSE to the main window
12199
if _subprocess_windows_send_vm_close(
122100
processIdentifier.value
@@ -148,15 +126,13 @@ extension Execution {
148126
// Send SIGTERM
149127
try? self.send(
150128
signal: .terminate,
151-
to: processIdentifier,
152129
toProcessGroup: false
153130
)
154131
#endif
155132
}
156133

157-
internal static func runTeardownSequence(
158-
_ sequence: some Sequence<TeardownStep> & Sendable,
159-
on processIdentifier: ProcessIdentifier
134+
internal func runTeardownSequence(
135+
_ sequence: some Sequence<TeardownStep> & Sendable
160136
) async {
161137
// First insert the `.kill` step
162138
let finalSequence = sequence + [TeardownStep(storage: .kill)]
@@ -177,7 +153,6 @@ extension Execution {
177153
}
178154
}
179155
await self.gracefulShutDown(
180-
processIdentifier,
181156
allowedDurationToNextStep: allowedDuration
182157
)
183158
return await group.next()!
@@ -195,7 +170,7 @@ extension Execution {
195170
return .processHasExited
196171
}
197172
}
198-
try? self.send(signal: signal, to: processIdentifier, toProcessGroup: false)
173+
try? self.send(signal: signal, toProcessGroup: false)
199174
return await group.next()!
200175
}
201176
#endif // !os(Windows)
@@ -206,7 +181,7 @@ extension Execution {
206181
withExitCode: 0
207182
)
208183
#else
209-
try? self.send(signal: .kill, to: processIdentifier, toProcessGroup: false)
184+
try? self.send(signal: .kill, toProcessGroup: false)
210185
#endif
211186
stepCompletion = .killedTheProcess
212187
}

Tests/SubprocessTests/SubprocessTests+Unix.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -783,20 +783,20 @@ extension SubprocessUnixTests {
783783
extension SubprocessUnixTests {
784784
@Test func testRunDetached() async throws {
785785
let (readFd, writeFd) = try FileDescriptor.pipe()
786-
let pid = try runDetached(
786+
let execution = try runDetached(
787787
.path("/bin/bash"),
788788
arguments: ["-c", "echo $$"],
789789
output: writeFd
790790
)
791791
var status: Int32 = 0
792-
waitpid(pid.value, &status, 0)
792+
waitpid(execution.processIdentifier.value, &status, 0)
793793
#expect(_was_process_exited(status) > 0)
794794
try writeFd.close()
795795
let data = try await readFd.readUntilEOF(upToLength: 10)
796796
let resultPID = try #require(
797797
String(data: Data(data), encoding: .utf8)
798798
).trimmingCharacters(in: .whitespacesAndNewlines)
799-
#expect("\(pid.value)" == resultPID)
799+
#expect("\(execution.processIdentifier.value)" == resultPID)
800800
try readFd.close()
801801
}
802802

Tests/SubprocessTests/SubprocessTests+Windows.swift

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -684,27 +684,21 @@ extension SubprocessWindowsTests {
684684
DWORD(HANDLE_FLAG_INHERIT),
685685
0
686686
)
687-
let pid = try Subprocess.runDetached(
687+
let execution = try Subprocess.runDetached(
688688
.path("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
689689
arguments: [
690690
"-Command", "Write-Host $PID",
691691
],
692692
output: writeFd
693693
)
694-
// Wait for process to finish
695-
guard
696-
let processHandle = OpenProcess(
697-
DWORD(PROCESS_QUERY_INFORMATION | SYNCHRONIZE),
698-
false,
699-
pid.value
700-
)
701-
else {
702-
Issue.record("Failed to get process handle")
703-
return
694+
defer {
695+
guard CloseHandle(execution.processHandle) else {
696+
Issue.record("Failed to close process handle")
697+
}
704698
}
705699

706700
// Wait for the process to finish
707-
WaitForSingleObject(processHandle, INFINITE)
701+
WaitForSingleObject(execution.processHandle, INFINITE)
708702

709703
// Up to 10 characters because Windows process IDs are DWORDs (UInt32), whose max value is 10 digits.
710704
let data = try await readFd.readUntilEOF(upToLength: 10)

0 commit comments

Comments
 (0)