@@ -82,11 +82,36 @@ public struct Configuration: Sendable {
8282 ) async throws -> Result
8383 )
8484 ) async throws -> ExecutionOutcome < Result > {
85- var spawnResults = try await self . spawn (
86- withInput: input,
87- outputPipe: output,
88- errorPipe: error
89- )
85+ // Built before spawning so it can be attached to spawn failures, where
86+ // no resolved values exist yet.
87+ let baseContext = ExecutionContext ( self )
88+
89+ var spawnResults : SpawnResult
90+ #if os(Windows)
91+ // `spawn` is `throws(SubprocessError)` on Windows, so `error` is
92+ // already typed; an `as SubprocessError` pattern is a redundant cast
93+ // that crashes the 6.2 toolchain's SIL ownership verifier. Elsewhere
94+ // `spawn` is untyped `throws`, where the cast is meaningful.
95+ do {
96+ spawnResults = try await self . spawn (
97+ withInput: input,
98+ outputPipe: output,
99+ errorPipe: error
100+ )
101+ } catch {
102+ throw error. withExecutionContext ( baseContext)
103+ }
104+ #else
105+ do {
106+ spawnResults = try await self . spawn (
107+ withInput: input,
108+ outputPipe: output,
109+ errorPipe: error
110+ )
111+ } catch let error as SubprocessError {
112+ throw error. withExecutionContext ( baseContext)
113+ }
114+ #endif
90115
91116 let processIdentifier = spawnResults. processIdentifier
92117
@@ -95,92 +120,105 @@ public struct Configuration: Sendable {
95120 processIdentifier. close ( )
96121 }
97122
98- return try await withAsyncTaskCleanupHandler { ( ) throws -> ExecutionOutcome < Result > in
99- // The counter coordinates a race between two finishers: the body
100- // closure and the process-termination monitor. Whichever side
101- // increments first observes a value of `1` and owns the response;
102- // the other side sees `2` and stays out of the way.
103- //
104- // If the monitor wins, the child has exited while the body is
105- // still reading or writing. The body might be blocked on a pipe
106- // that an inherited grandchild keeps open, so the monitor calls
107- // `cancelAsyncIO` to unblock it. If the body wins, the I/O
108- // already finished cleanly and no cancellation is needed.
109- let taskFinishFlag = AtomicCounter ( )
110-
111- let ( result, monitorError) = try await withThrowingTaskGroup (
112- of: SubprocessError ? . self,
113- returning: ( Swift . Result < Result , any Swift . Error > , SubprocessError? ) . self
114- ) { group in
115- group. addTask {
116- do throws ( SubprocessError) {
117- try await waitForProcessTermination ( for: processIdentifier)
118- if taskFinishFlag. addOne ( ) == 1 {
119- // The body closure hasn't finished but the child
120- // process has terminated. Cancel all active
121- // AsyncIO now.
122- try AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
123+ // Spawn succeeded, so resolved values are available. This context is
124+ // attached to any error surfacing after spawn.
125+ let executionContext = ExecutionContext (
126+ self ,
127+ resolvedExecutable: spawnResults. resolvedExecutable,
128+ resolvedEnvironment: self . environment. resolvedValues ( ) ,
129+ resolvedWorkingDirectory: self . workingDirectory ?? currentWorkingDirectory ( )
130+ )
131+
132+ do {
133+ return try await withAsyncTaskCleanupHandler { ( ) throws -> ExecutionOutcome < Result > in
134+ // The counter coordinates a race between two finishers: the body
135+ // closure and the process-termination monitor. Whichever side
136+ // increments first observes a value of `1` and owns the response;
137+ // the other side sees `2` and stays out of the way.
138+ //
139+ // If the monitor wins, the child has exited while the body is
140+ // still reading or writing. The body might be blocked on a pipe
141+ // that an inherited grandchild keeps open, so the monitor calls
142+ // `cancelAsyncIO` to unblock it. If the body wins, the I/O
143+ // already finished cleanly and no cancellation is needed.
144+ let taskFinishFlag = AtomicCounter ( )
145+
146+ let ( result, monitorError) = try await withThrowingTaskGroup (
147+ of: SubprocessError ? . self,
148+ returning: ( Swift . Result < Result , any Swift . Error > , SubprocessError? ) . self
149+ ) { group in
150+ group. addTask {
151+ do throws ( SubprocessError) {
152+ try await waitForProcessTermination ( for: processIdentifier)
153+ if taskFinishFlag. addOne ( ) == 1 {
154+ // The body closure hasn't finished but the child
155+ // process has terminated. Cancel all active
156+ // AsyncIO now.
157+ try AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
158+ }
159+ return nil
160+ } catch {
161+ try ? AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
162+ return error
123163 }
124- return nil
125- } catch {
126- try ? AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
127- return error
128164 }
129- }
130165
131- let inputIO = spawnResults. inputWriteEnd ( )
132- let outputIO = spawnResults. outputReadEnd ( )
133- let errorIO = spawnResults. errorReadEnd ( )
166+ let inputIO = spawnResults. inputWriteEnd ( )
167+ let outputIO = spawnResults. outputReadEnd ( )
168+ let errorIO = spawnResults. errorReadEnd ( )
134169
135- let result : Swift . Result < Result , any Swift . Error >
136- do {
137- // Body runs in the same isolation.
138- let bodyResult = try await body ( processIdentifier, inputIO, outputIO, errorIO)
139- taskFinishFlag. addOne ( )
140- result = . success( bodyResult)
141- } catch {
142- let execution = Execution < Input , Output , Error > (
143- processIdentifier: processIdentifier,
144- inputWriter: nil ,
145- outputStream: nil ,
146- errorStream: nil
147- )
148- // Attempt to terminate the child process when the body throws
149- await execution. teardown ( using: self . platformOptions. teardownSequence)
150- result = . failure( error)
170+ let result : Swift . Result < Result , any Swift . Error >
171+ do {
172+ // Body runs in the same isolation.
173+ let bodyResult = try await body ( processIdentifier, inputIO, outputIO, errorIO)
174+ taskFinishFlag. addOne ( )
175+ result = . success( bodyResult)
176+ } catch {
177+ let execution = Execution < Input , Output , Error > (
178+ processIdentifier: processIdentifier,
179+ inputWriter: nil ,
180+ outputStream: nil ,
181+ errorStream: nil
182+ )
183+ // Attempt to terminate the child process when the body throws
184+ await execution. teardown ( using: self . platformOptions. teardownSequence)
185+ result = . failure( error)
186+ }
187+
188+ // Wait for the monitor child task to finish.
189+ let monitorError = try await group. next ( ) ?? nil
190+ return ( result, monitorError)
151191 }
152192
153- // Wait for the monitor child task to finish.
154- let monitorError = try await group. next ( ) ?? nil
155- return ( result, monitorError)
156- }
193+ // Drop the cancellation marker before reaping the zombie. After
194+ // `reapProcess` runs the kernel can immediately reuse this PID,
195+ // so the marker must be gone first; otherwise a concurrent
196+ // `run()` that happens to inherit the same PID would see the
197+ // stale entry and reject its registrations.
198+ AsyncIO . shared. cleanup ( processIdentifier: processIdentifier)
157199
158- // Drop the cancellation marker before reaping the zombie. After
159- // `reapProcess` runs the kernel can immediately reuse this PID,
160- // so the marker must be gone first; otherwise a concurrent
161- // `run()` that happens to inherit the same PID would see the
162- // stale entry and reject its registrations.
163- AsyncIO . shared. cleanup ( processIdentifier: processIdentifier)
200+ let terminationStatus = try reapProcess ( with: processIdentifier)
164201
165- let terminationStatus = try reapProcess ( with: processIdentifier)
202+ if let monitorError {
203+ throw monitorError
204+ }
166205
167- if let monitorError {
168- throw monitorError
206+ return try ExecutionOutcome (
207+ terminationStatus: terminationStatus,
208+ value: result. get ( )
209+ )
210+ } onCleanup: {
211+ let execution = Execution < Input , Output , Error > (
212+ processIdentifier: processIdentifier,
213+ inputWriter: nil ,
214+ outputStream: nil ,
215+ errorStream: nil
216+ )
217+ // Attempt to terminate the child process.
218+ await execution. teardown ( using: self . platformOptions. teardownSequence)
169219 }
170-
171- return try ExecutionOutcome (
172- terminationStatus: terminationStatus,
173- value: result. get ( )
174- )
175- } onCleanup: {
176- let execution = Execution < Input , Output , Error > (
177- processIdentifier: processIdentifier,
178- inputWriter: nil ,
179- outputStream: nil ,
180- errorStream: nil
181- )
182- // Attempt to terminate the child process.
183- await execution. teardown ( using: self . platformOptions. teardownSequence)
220+ } catch let error as SubprocessError {
221+ throw error. withExecutionContext ( executionContext)
184222 }
185223 }
186224}
@@ -573,6 +611,40 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
573611 return results
574612 }
575613 }
614+
615+ /// The concrete environment variables passed to the child process, or
616+ /// `nil` if they cannot be represented as a `[Key: String]` map.
617+ ///
618+ /// This mirrors the environment built by the platform spawn path: for
619+ /// `.inherit`, the parent's current values with the configured overrides
620+ /// applied (a `nil` override removes the key); for `.custom`, those values
621+ /// directly; for raw-byte environments, `nil`. On Windows, `PATH` is
622+ /// injected from the parent when absent, matching the spawn path's
623+ /// DLL-resolution behavior.
624+ internal func resolvedValues( ) -> [ Environment . Key : String ] ? {
625+ var values : [ Environment . Key : String ]
626+ switch self . config {
627+ case . custom( let customValues) :
628+ values = customValues
629+ case . inherit( let overrides) :
630+ values = Self . currentEnvironmentValues ( )
631+ for (key, override) in overrides {
632+ values [ key] = override
633+ }
634+ #if !os(Windows)
635+ case . rawBytes:
636+ return nil
637+ #endif
638+ }
639+ #if os(Windows)
640+ if values [ . path] == nil ,
641+ let parentPath = Self . currentEnvironmentValues ( ) [ . path]
642+ {
643+ values [ . path] = parentPath
644+ }
645+ #endif
646+ return values
647+ }
576648}
577649
578650extension Environment . Key {
@@ -697,6 +769,7 @@ extension Configuration {
697769 /// via `SpawnResult` to perform actual reads
698770 internal struct SpawnResult : ~ Copyable {
699771 let processIdentifier : ProcessIdentifier
772+ let resolvedExecutable : FilePath ?
700773 var _inputWriteEnd : IODescriptor ?
701774 var _outputReadEnd : IODescriptor ?
702775 var _errorReadEnd : IODescriptor ?
@@ -705,12 +778,14 @@ extension Configuration {
705778 processIdentifier: ProcessIdentifier ,
706779 inputWriteEnd: consuming IODescriptor ? ,
707780 outputReadEnd: consuming IODescriptor ? ,
708- errorReadEnd: consuming IODescriptor ?
781+ errorReadEnd: consuming IODescriptor ? ,
782+ resolvedExecutable: FilePath ? = nil
709783 ) {
710784 self . processIdentifier = processIdentifier
711785 self . _inputWriteEnd = consume inputWriteEnd
712786 self . _outputReadEnd = consume outputReadEnd
713787 self . _errorReadEnd = consume errorReadEnd
788+ self . resolvedExecutable = resolvedExecutable
714789 }
715790
716791 mutating func inputWriteEnd( ) -> IODescriptor ? {
@@ -1346,3 +1421,28 @@ extension StringProtocol {
13461421 }
13471422 }
13481423}
1424+
1425+ /// Best-effort lookup of the parent process's current working directory.
1426+ ///
1427+ /// Returns `nil` on failure; never throws, since it only feeds diagnostics.
1428+ internal func currentWorkingDirectory( ) -> FilePath ? {
1429+ #if os(Windows)
1430+ let length = GetCurrentDirectoryW ( 0 , nil )
1431+ guard length > 0 else { return nil }
1432+ let path = try ? fillNullTerminatedWideStringBuffer (
1433+ initialSize: length,
1434+ maxSize: DWORD ( Int16 . max)
1435+ ) {
1436+ GetCurrentDirectoryW ( DWORD ( $0. count) , $0. baseAddress)
1437+ }
1438+ guard let path else { return nil }
1439+ return FilePath ( path)
1440+ #else
1441+ return withUnsafeTemporaryAllocation ( of: CChar . self, capacity: Int ( PATH_MAX) ) { buffer -> FilePath ? in
1442+ guard getcwd ( buffer. baseAddress, buffer. count) != nil else {
1443+ return nil
1444+ }
1445+ return FilePath ( platformString: buffer. baseAddress!)
1446+ }
1447+ #endif
1448+ }
0 commit comments