@@ -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,84 +120,97 @@ 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- 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+ result = . failure( error)
178+ }
179+
180+ // Wait for the monitor child task to finish.
181+ let monitorError = try await group. next ( ) ?? nil
182+ return ( result, monitorError)
143183 }
144184
145- // Wait for the monitor child task to finish.
146- let monitorError = try await group. next ( ) ?? nil
147- return ( result, monitorError)
148- }
185+ // Drop the cancellation marker before reaping the zombie. After
186+ // `reapProcess` runs the kernel can immediately reuse this PID,
187+ // so the marker must be gone first; otherwise a concurrent
188+ // `run()` that happens to inherit the same PID would see the
189+ // stale entry and reject its registrations.
190+ AsyncIO . shared. cleanup ( processIdentifier: processIdentifier)
149191
150- // Drop the cancellation marker before reaping the zombie. After
151- // `reapProcess` runs the kernel can immediately reuse this PID,
152- // so the marker must be gone first; otherwise a concurrent
153- // `run()` that happens to inherit the same PID would see the
154- // stale entry and reject its registrations.
155- AsyncIO . shared. cleanup ( processIdentifier: processIdentifier)
192+ let terminationStatus = try reapProcess ( with: processIdentifier)
156193
157- let terminationStatus = try reapProcess ( with: processIdentifier)
194+ if let monitorError {
195+ throw monitorError
196+ }
158197
159- if let monitorError {
160- throw monitorError
198+ return try ExecutionOutcome (
199+ terminationStatus: terminationStatus,
200+ value: result. get ( )
201+ )
202+ } onCleanup: {
203+ let execution = Execution < Input , Output , Error > (
204+ processIdentifier: processIdentifier,
205+ inputWriter: nil ,
206+ outputStream: nil ,
207+ errorStream: nil
208+ )
209+ // Attempt to terminate the child process.
210+ await execution. teardown ( using: self . platformOptions. teardownSequence)
161211 }
162-
163- return try ExecutionOutcome (
164- terminationStatus: terminationStatus,
165- value: result. get ( )
166- )
167- } onCleanup: {
168- let execution = Execution < Input , Output , Error > (
169- processIdentifier: processIdentifier,
170- inputWriter: nil ,
171- outputStream: nil ,
172- errorStream: nil
173- )
174- // Attempt to terminate the child process.
175- await execution. teardown ( using: self . platformOptions. teardownSequence)
212+ } catch let error as SubprocessError {
213+ throw error. withExecutionContext ( executionContext)
176214 }
177215 }
178216}
@@ -565,6 +603,40 @@ extension Environment: CustomStringConvertible, CustomDebugStringConvertible {
565603 return results
566604 }
567605 }
606+
607+ /// The concrete environment variables passed to the child process, or
608+ /// `nil` if they cannot be represented as a `[Key: String]` map.
609+ ///
610+ /// This mirrors the environment built by the platform spawn path: for
611+ /// `.inherit`, the parent's current values with the configured overrides
612+ /// applied (a `nil` override removes the key); for `.custom`, those values
613+ /// directly; for raw-byte environments, `nil`. On Windows, `PATH` is
614+ /// injected from the parent when absent, matching the spawn path's
615+ /// DLL-resolution behavior.
616+ internal func resolvedValues( ) -> [ Environment . Key : String ] ? {
617+ var values : [ Environment . Key : String ]
618+ switch self . config {
619+ case . custom( let customValues) :
620+ values = customValues
621+ case . inherit( let overrides) :
622+ values = Self . currentEnvironmentValues ( )
623+ for (key, override) in overrides {
624+ values [ key] = override
625+ }
626+ #if !os(Windows)
627+ case . rawBytes:
628+ return nil
629+ #endif
630+ }
631+ #if os(Windows)
632+ if values [ . path] == nil ,
633+ let parentPath = Self . currentEnvironmentValues ( ) [ . path]
634+ {
635+ values [ . path] = parentPath
636+ }
637+ #endif
638+ return values
639+ }
568640}
569641
570642extension Environment . Key {
@@ -689,6 +761,7 @@ extension Configuration {
689761 /// via `SpawnResult` to perform actual reads
690762 internal struct SpawnResult : ~ Copyable {
691763 let processIdentifier : ProcessIdentifier
764+ let resolvedExecutable : FilePath ?
692765 var _inputWriteEnd : IODescriptor ?
693766 var _outputReadEnd : IODescriptor ?
694767 var _errorReadEnd : IODescriptor ?
@@ -697,12 +770,14 @@ extension Configuration {
697770 processIdentifier: ProcessIdentifier ,
698771 inputWriteEnd: consuming IODescriptor ? ,
699772 outputReadEnd: consuming IODescriptor ? ,
700- errorReadEnd: consuming IODescriptor ?
773+ errorReadEnd: consuming IODescriptor ? ,
774+ resolvedExecutable: FilePath ? = nil
701775 ) {
702776 self . processIdentifier = processIdentifier
703777 self . _inputWriteEnd = consume inputWriteEnd
704778 self . _outputReadEnd = consume outputReadEnd
705779 self . _errorReadEnd = consume errorReadEnd
780+ self . resolvedExecutable = resolvedExecutable
706781 }
707782
708783 mutating func inputWriteEnd( ) -> IODescriptor ? {
@@ -1338,3 +1413,28 @@ extension StringProtocol {
13381413 }
13391414 }
13401415}
1416+
1417+ /// Best-effort lookup of the parent process's current working directory.
1418+ ///
1419+ /// Returns `nil` on failure; never throws, since it only feeds diagnostics.
1420+ internal func currentWorkingDirectory( ) -> FilePath ? {
1421+ #if os(Windows)
1422+ let length = GetCurrentDirectoryW ( 0 , nil )
1423+ guard length > 0 else { return nil }
1424+ let path = try ? fillNullTerminatedWideStringBuffer (
1425+ initialSize: length,
1426+ maxSize: DWORD ( Int16 . max)
1427+ ) {
1428+ GetCurrentDirectoryW ( DWORD ( $0. count) , $0. baseAddress)
1429+ }
1430+ guard let path else { return nil }
1431+ return FilePath ( path)
1432+ #else
1433+ return withUnsafeTemporaryAllocation ( of: CChar . self, capacity: Int ( PATH_MAX) ) { buffer -> FilePath ? in
1434+ guard getcwd ( buffer. baseAddress, buffer. count) != nil else {
1435+ return nil
1436+ }
1437+ return FilePath ( platformString: buffer. baseAddress!)
1438+ }
1439+ #endif
1440+ }
0 commit comments