@@ -98,11 +98,36 @@ public struct Configuration: Sendable {
9898 ) async throws -> Result
9999 )
100100 ) async throws -> ExecutionOutcome < Result > {
101- var spawnResults = try await self . spawn (
102- withInput: input,
103- outputPipe: output,
104- errorPipe: error
105- )
101+ // Built before spawning so it can be attached to spawn failures, where
102+ // no resolved values exist yet.
103+ let baseContext = ExecutionContext ( self )
104+
105+ var spawnResults : SpawnResult
106+ #if os(Windows)
107+ // `spawn` is `throws(SubprocessError)` on Windows, so `error` is
108+ // already typed; an `as SubprocessError` pattern is a redundant cast
109+ // that crashes the 6.2 toolchain's SIL ownership verifier. Elsewhere
110+ // `spawn` is untyped `throws`, where the cast is meaningful.
111+ do {
112+ spawnResults = try await self . spawn (
113+ withInput: input,
114+ outputPipe: output,
115+ errorPipe: error
116+ )
117+ } catch {
118+ throw error. withExecutionContext ( baseContext)
119+ }
120+ #else
121+ do {
122+ spawnResults = try await self . spawn (
123+ withInput: input,
124+ outputPipe: output,
125+ errorPipe: error
126+ )
127+ } catch let error as SubprocessError {
128+ throw error. withExecutionContext ( baseContext)
129+ }
130+ #endif
106131
107132 let processIdentifier = spawnResults. processIdentifier
108133
@@ -111,84 +136,97 @@ public struct Configuration: Sendable {
111136 processIdentifier. close ( )
112137 }
113138
114- return try await withAsyncTaskCleanupHandler { ( ) throws -> ExecutionOutcome < Result > in
115- // The counter coordinates a race between two finishers: the body
116- // closure and the process-termination monitor. Whichever side
117- // increments first observes a value of `1` and owns the response;
118- // the other side sees `2` and stays out of the way.
119- //
120- // If the monitor wins, the child has exited while the body is
121- // still reading or writing. The body might be blocked on a pipe
122- // that an inherited grandchild keeps open, so the monitor calls
123- // `cancelAsyncIO` to unblock it. If the body wins, the I/O
124- // already finished cleanly and no cancellation is needed.
125- let taskFinishFlag = AtomicCounter ( )
126-
127- let ( result, monitorError) = try await withThrowingTaskGroup (
128- of: SubprocessError ? . self,
129- returning: ( Swift . Result < Result , any Swift . Error > , SubprocessError? ) . self
130- ) { group in
131- group. addTask {
132- do throws ( SubprocessError) {
133- try await waitForProcessTermination ( for: processIdentifier)
134- if taskFinishFlag. addOne ( ) == 1 {
135- // The body closure hasn't finished but the child
136- // process has terminated. Cancel all active
137- // AsyncIO now.
138- try AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
139+ // Spawn succeeded, so resolved values are available. This context is
140+ // attached to any error surfacing after spawn.
141+ let executionContext = ExecutionContext (
142+ self ,
143+ resolvedExecutable: spawnResults. resolvedExecutable,
144+ resolvedEnvironment: self . environment. resolvedValues ( ) ,
145+ resolvedWorkingDirectory: self . workingDirectory ?? currentWorkingDirectory ( )
146+ )
147+
148+ do {
149+ return try await withAsyncTaskCleanupHandler { ( ) throws -> ExecutionOutcome < Result > in
150+ // The counter coordinates a race between two finishers: the body
151+ // closure and the process-termination monitor. Whichever side
152+ // increments first observes a value of `1` and owns the response;
153+ // the other side sees `2` and stays out of the way.
154+ //
155+ // If the monitor wins, the child has exited while the body is
156+ // still reading or writing. The body might be blocked on a pipe
157+ // that an inherited grandchild keeps open, so the monitor calls
158+ // `cancelAsyncIO` to unblock it. If the body wins, the I/O
159+ // already finished cleanly and no cancellation is needed.
160+ let taskFinishFlag = AtomicCounter ( )
161+
162+ let ( result, monitorError) = try await withThrowingTaskGroup (
163+ of: SubprocessError ? . self,
164+ returning: ( Swift . Result < Result , any Swift . Error > , SubprocessError? ) . self
165+ ) { group in
166+ group. addTask {
167+ do throws ( SubprocessError) {
168+ try await waitForProcessTermination ( for: processIdentifier)
169+ if taskFinishFlag. addOne ( ) == 1 {
170+ // The body closure hasn't finished but the child
171+ // process has terminated. Cancel all active
172+ // AsyncIO now.
173+ try AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
174+ }
175+ return nil
176+ } catch {
177+ try ? AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
178+ return error
139179 }
140- return nil
141- } catch {
142- try ? AsyncIO . shared. cancelAsyncIO ( for: processIdentifier)
143- return error
144180 }
145- }
146181
147- let inputIO = spawnResults. inputWriteEnd ( )
148- let outputIO = spawnResults. outputReadEnd ( )
149- let errorIO = spawnResults. errorReadEnd ( )
182+ let inputIO = spawnResults. inputWriteEnd ( )
183+ let outputIO = spawnResults. outputReadEnd ( )
184+ let errorIO = spawnResults. errorReadEnd ( )
150185
151- let result : Swift . Result < Result , any Swift . Error >
152- do {
153- // Body runs in the same isolation.
154- let bodyResult = try await body ( processIdentifier, inputIO, outputIO, errorIO)
155- taskFinishFlag. addOne ( )
156- result = . success( bodyResult)
157- } catch {
158- result = . failure( error)
186+ let result : Swift . Result < Result , any Swift . Error >
187+ do {
188+ // Body runs in the same isolation.
189+ let bodyResult = try await body ( processIdentifier, inputIO, outputIO, errorIO)
190+ taskFinishFlag. addOne ( )
191+ result = . success( bodyResult)
192+ } catch {
193+ result = . failure( error)
194+ }
195+
196+ // Wait for the monitor child task to finish.
197+ let monitorError = try await group. next ( ) ?? nil
198+ return ( result, monitorError)
159199 }
160200
161- // Wait for the monitor child task to finish.
162- let monitorError = try await group. next ( ) ?? nil
163- return ( result, monitorError)
164- }
201+ // Drop the cancellation marker before reaping the zombie. After
202+ // `reapProcess` runs the kernel can immediately reuse this PID,
203+ // so the marker must be gone first; otherwise a concurrent
204+ // `run()` that happens to inherit the same PID would see the
205+ // stale entry and reject its registrations.
206+ AsyncIO . shared. cleanup ( processIdentifier: processIdentifier)
165207
166- // Drop the cancellation marker before reaping the zombie. After
167- // `reapProcess` runs the kernel can immediately reuse this PID,
168- // so the marker must be gone first; otherwise a concurrent
169- // `run()` that happens to inherit the same PID would see the
170- // stale entry and reject its registrations.
171- AsyncIO . shared. cleanup ( processIdentifier: processIdentifier)
208+ let terminationStatus = try reapProcess ( with: processIdentifier)
172209
173- let terminationStatus = try reapProcess ( with: processIdentifier)
210+ if let monitorError {
211+ throw monitorError
212+ }
174213
175- if let monitorError {
176- throw monitorError
214+ return try ExecutionOutcome (
215+ terminationStatus: terminationStatus,
216+ value: result. get ( )
217+ )
218+ } onCleanup: {
219+ let execution = Execution < Input , Output , Error > (
220+ processIdentifier: processIdentifier,
221+ inputWriter: nil ,
222+ outputStream: nil ,
223+ errorStream: nil
224+ )
225+ // Attempt to terminate the child process.
226+ await execution. teardown ( using: self . platformOptions. teardownSequence)
177227 }
178-
179- return try ExecutionOutcome (
180- terminationStatus: terminationStatus,
181- value: result. get ( )
182- )
183- } onCleanup: {
184- let execution = Execution < Input , Output , Error > (
185- processIdentifier: processIdentifier,
186- inputWriter: nil ,
187- outputStream: nil ,
188- errorStream: nil
189- )
190- // Attempt to terminate the child process.
191- await execution. teardown ( using: self . platformOptions. teardownSequence)
228+ } catch let error as SubprocessError {
229+ throw error. withExecutionContext ( executionContext)
192230 }
193231 }
194232}
@@ -581,6 +619,40 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
581619 return results
582620 }
583621 }
622+
623+ /// The concrete environment variables passed to the child process, or
624+ /// `nil` if they cannot be represented as a `[Key: String]` map.
625+ ///
626+ /// This mirrors the environment built by the platform spawn path: for
627+ /// `.inherit`, the parent's current values with the configured overrides
628+ /// applied (a `nil` override removes the key); for `.custom`, those values
629+ /// directly; for raw-byte environments, `nil`. On Windows, `PATH` is
630+ /// injected from the parent when absent, matching the spawn path's
631+ /// DLL-resolution behavior.
632+ internal func resolvedValues( ) -> [ Environment . Key : String ] ? {
633+ var values : [ Environment . Key : String ]
634+ switch self . config {
635+ case . custom( let customValues) :
636+ values = customValues
637+ case . inherit( let overrides) :
638+ values = Self . currentEnvironmentValues ( )
639+ for (key, override) in overrides {
640+ values [ key] = override
641+ }
642+ #if !os(Windows)
643+ case . rawBytes:
644+ return nil
645+ #endif
646+ }
647+ #if os(Windows)
648+ if values [ . path] == nil ,
649+ let parentPath = Self . currentEnvironmentValues ( ) [ . path]
650+ {
651+ values [ . path] = parentPath
652+ }
653+ #endif
654+ return values
655+ }
584656}
585657
586658extension Environment . Key {
@@ -705,6 +777,7 @@ extension Configuration {
705777 /// via `SpawnResult` to perform actual reads
706778 internal struct SpawnResult : ~ Copyable {
707779 let processIdentifier : ProcessIdentifier
780+ let resolvedExecutable : FilePath ?
708781 var _inputWriteEnd : IODescriptor ?
709782 var _outputReadEnd : IODescriptor ?
710783 var _errorReadEnd : IODescriptor ?
@@ -713,12 +786,14 @@ extension Configuration {
713786 processIdentifier: ProcessIdentifier ,
714787 inputWriteEnd: consuming IODescriptor ? ,
715788 outputReadEnd: consuming IODescriptor ? ,
716- errorReadEnd: consuming IODescriptor ?
789+ errorReadEnd: consuming IODescriptor ? ,
790+ resolvedExecutable: FilePath ? = nil
717791 ) {
718792 self . processIdentifier = processIdentifier
719793 self . _inputWriteEnd = consume inputWriteEnd
720794 self . _outputReadEnd = consume outputReadEnd
721795 self . _errorReadEnd = consume errorReadEnd
796+ self . resolvedExecutable = resolvedExecutable
722797 }
723798
724799 mutating func inputWriteEnd( ) -> IODescriptor ? {
@@ -1354,3 +1429,28 @@ extension StringProtocol {
13541429 }
13551430 }
13561431}
1432+
1433+ /// Best-effort lookup of the parent process's current working directory.
1434+ ///
1435+ /// Returns `nil` on failure; never throws, since it only feeds diagnostics.
1436+ internal func currentWorkingDirectory( ) -> FilePath ? {
1437+ #if os(Windows)
1438+ let length = GetCurrentDirectoryW ( 0 , nil )
1439+ guard length > 0 else { return nil }
1440+ let path = try ? fillNullTerminatedWideStringBuffer (
1441+ initialSize: length,
1442+ maxSize: DWORD ( Int16 . max)
1443+ ) {
1444+ GetCurrentDirectoryW ( DWORD ( $0. count) , $0. baseAddress)
1445+ }
1446+ guard let path else { return nil }
1447+ return FilePath ( path)
1448+ #else
1449+ return withUnsafeTemporaryAllocation ( of: CChar . self, capacity: Int ( PATH_MAX) ) { buffer -> FilePath ? in
1450+ guard getcwd ( buffer. baseAddress, buffer. count) != nil else {
1451+ return nil
1452+ }
1453+ return FilePath ( platformString: buffer. baseAddress!)
1454+ }
1455+ #endif
1456+ }
0 commit comments