Skip to content

Commit 4d58af5

Browse files
committed
Enable runOnBackgroundThread for spawn() on Windows
1 parent 0adedd7 commit 4d58af5

File tree

2 files changed

+115
-85
lines changed

2 files changed

+115
-85
lines changed

Sources/Subprocess/Configuration.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,11 @@ internal struct IODescriptor: ~Copyable {
644644
#endif
645645

646646
internal var closeWhenDone: Bool
647+
#if canImport(WinSDK)
648+
internal nonisolated(unsafe) let descriptor: Descriptor
649+
#else
647650
internal let descriptor: Descriptor
651+
#endif
648652

649653
internal init(
650654
_ descriptor: Descriptor,

Sources/Subprocess/Platforms/Subprocess+Windows.swift

Lines changed: 111 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,30 @@ import _SubprocessCShims
2323

2424
// Windows specific implementation
2525
extension Configuration {
26+
// @unchecked Sendable because we need to capture UnsafePointers
27+
// to send to another thread. While UnsafePointers are not
28+
// Sendable, we are not mutating them -- we only need these type
29+
// for C interface.
30+
internal struct SpawnContext: @unchecked Sendable {
31+
let startupInfo: UnsafeMutablePointer<STARTUPINFOEXW>
32+
let createProcessFlags: DWORD
33+
}
34+
2635
internal func spawn(
2736
withInput inputPipe: consuming CreatedPipe,
2837
outputPipe: consuming CreatedPipe,
2938
errorPipe: consuming CreatedPipe
30-
) throws -> SpawnResult {
39+
) async throws -> SpawnResult {
3140
// Spawn differently depending on whether
3241
// we need to spawn as a user
3342
guard let userCredentials = self.platformOptions.userCredentials else {
34-
return try self.spawnDirect(
43+
return try await self.spawnDirect(
3544
withInput: inputPipe,
3645
outputPipe: outputPipe,
3746
errorPipe: errorPipe
3847
)
3948
}
40-
return try self.spawnAsUser(
49+
return try await self.spawnAsUser(
4150
withInput: inputPipe,
4251
outputPipe: outputPipe,
4352
errorPipe: errorPipe,
@@ -49,7 +58,7 @@ extension Configuration {
4958
withInput inputPipe: consuming CreatedPipe,
5059
outputPipe: consuming CreatedPipe,
5160
errorPipe: consuming CreatedPipe
52-
) throws -> SpawnResult {
61+
) async throws -> SpawnResult {
5362
var inputReadFileDescriptor: IODescriptor? = inputPipe.readFileDescriptor()
5463
var inputWriteFileDescriptor: IODescriptor? = inputPipe.writeFileDescriptor()
5564
var outputReadFileDescriptor: IODescriptor? = outputPipe.readFileDescriptor()
@@ -104,10 +113,9 @@ extension Configuration {
104113
throw error
105114
}
106115

107-
var processInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
108116
var createProcessFlags = self.generateCreateProcessFlag()
109117

110-
let created = try self.withStartupInfoEx(
118+
let (created, processInfo, windowsError) = try await self.withStartupInfoEx(
111119
inputRead: inputReadFileDescriptor,
112120
inputWrite: inputWriteFileDescriptor,
113121
outputRead: outputReadFileDescriptor,
@@ -121,34 +129,41 @@ extension Configuration {
121129
}
122130

123131
// Spawn!
124-
return try applicationName.withOptionalNTPathRepresentation { applicationNameW in
125-
try commandAndArgs.withCString(
126-
encodedAs: UTF16.self
127-
) { commandAndArgsW in
128-
try environment.withCString(
132+
let spawnContext = SpawnContext(
133+
startupInfo: startupInfo,
134+
createProcessFlags: createProcessFlags
135+
)
136+
return try await runOnBackgroundThread {
137+
return try applicationName.withOptionalNTPathRepresentation { applicationNameW in
138+
try commandAndArgs.withCString(
129139
encodedAs: UTF16.self
130-
) { environmentW in
131-
try intendedWorkingDir.withOptionalNTPathRepresentation { intendedWorkingDirW in
132-
CreateProcessW(
133-
applicationNameW,
134-
UnsafeMutablePointer<WCHAR>(mutating: commandAndArgsW),
135-
nil, // lpProcessAttributes
136-
nil, // lpThreadAttributes
137-
true, // bInheritHandles
138-
createProcessFlags,
139-
UnsafeMutableRawPointer(mutating: environmentW),
140-
intendedWorkingDirW,
141-
startupInfo.pointer(to: \.StartupInfo)!,
142-
&processInfo
143-
)
140+
) { commandAndArgsW in
141+
try environment.withCString(
142+
encodedAs: UTF16.self
143+
) { environmentW in
144+
try intendedWorkingDir.withOptionalNTPathRepresentation { intendedWorkingDirW in
145+
var processInfo = PROCESS_INFORMATION()
146+
let result = CreateProcessW(
147+
applicationNameW,
148+
UnsafeMutablePointer<WCHAR>(mutating: commandAndArgsW),
149+
nil, // lpProcessAttributes
150+
nil, // lpThreadAttributes
151+
true, // bInheritHandles
152+
spawnContext.createProcessFlags,
153+
UnsafeMutableRawPointer(mutating: environmentW),
154+
intendedWorkingDirW,
155+
spawnContext.startupInfo.pointer(to: \.StartupInfo)!,
156+
&processInfo
157+
)
158+
return (result, processInfo, GetLastError())
159+
}
144160
}
145161
}
146162
}
147163
}
148164
}
149165

150166
guard created else {
151-
let windowsError = GetLastError()
152167
if windowsError == ERROR_FILE_NOT_FOUND || windowsError == ERROR_PATH_NOT_FOUND {
153168
// This execution path is not it. Try the next one
154169
continue
@@ -233,7 +248,7 @@ extension Configuration {
233248
outputPipe: consuming CreatedPipe,
234249
errorPipe: consuming CreatedPipe,
235250
userCredentials: PlatformOptions.UserCredentials
236-
) throws -> SpawnResult {
251+
) async throws -> SpawnResult {
237252
var inputPipeBox: CreatedPipe? = consume inputPipe
238253
var outputPipeBox: CreatedPipe? = consume outputPipe
239254
var errorPipeBox: CreatedPipe? = consume errorPipe
@@ -295,10 +310,9 @@ extension Configuration {
295310
throw error
296311
}
297312

298-
var processInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
299313
var createProcessFlags = self.generateCreateProcessFlag()
300314

301-
let created = try self.withStartupInfoEx(
315+
let (created, processInfo, windowsError) = try await self.withStartupInfoEx(
302316
inputRead: inputReadFileDescriptor,
303317
inputWrite: inputWriteFileDescriptor,
304318
outputRead: outputReadFileDescriptor,
@@ -311,37 +325,45 @@ extension Configuration {
311325
try configurator(&createProcessFlags, &startupInfo.pointer(to: \.StartupInfo)!.pointee)
312326
}
313327

328+
let spawnContext = SpawnContext(
329+
startupInfo: startupInfo,
330+
createProcessFlags: createProcessFlags
331+
)
314332
// Spawn (featuring pyramid!)
315-
return try userCredentials.username.withCString(
316-
encodedAs: UTF16.self
317-
) { usernameW in
318-
try userCredentials.password.withCString(
333+
return try await runOnBackgroundThread {
334+
return try userCredentials.username.withCString(
319335
encodedAs: UTF16.self
320-
) { passwordW in
321-
try userCredentials.domain.withOptionalCString(
336+
) { usernameW in
337+
try userCredentials.password.withCString(
322338
encodedAs: UTF16.self
323-
) { domainW in
324-
try applicationName.withOptionalNTPathRepresentation { applicationNameW in
325-
try commandAndArgs.withCString(
326-
encodedAs: UTF16.self
327-
) { commandAndArgsW in
328-
try environment.withCString(
339+
) { passwordW in
340+
try userCredentials.domain.withOptionalCString(
341+
encodedAs: UTF16.self
342+
) { domainW in
343+
try applicationName.withOptionalNTPathRepresentation { applicationNameW in
344+
try commandAndArgs.withCString(
329345
encodedAs: UTF16.self
330-
) { environmentW in
331-
try intendedWorkingDir.withOptionalNTPathRepresentation { intendedWorkingDirW in
332-
CreateProcessWithLogonW(
333-
usernameW,
334-
domainW,
335-
passwordW,
336-
DWORD(LOGON_WITH_PROFILE),
337-
applicationNameW,
338-
UnsafeMutablePointer<WCHAR>(mutating: commandAndArgsW),
339-
createProcessFlags,
340-
UnsafeMutableRawPointer(mutating: environmentW),
341-
intendedWorkingDirW,
342-
startupInfo.pointer(to: \.StartupInfo)!,
343-
&processInfo
344-
)
346+
) { commandAndArgsW in
347+
try environment.withCString(
348+
encodedAs: UTF16.self
349+
) { environmentW in
350+
try intendedWorkingDir.withOptionalNTPathRepresentation { intendedWorkingDirW in
351+
var processInfo = PROCESS_INFORMATION()
352+
let created = CreateProcessWithLogonW(
353+
usernameW,
354+
domainW,
355+
passwordW,
356+
DWORD(LOGON_WITH_PROFILE),
357+
applicationNameW,
358+
UnsafeMutablePointer<WCHAR>(mutating: commandAndArgsW),
359+
spawnContext.createProcessFlags,
360+
UnsafeMutableRawPointer(mutating: environmentW),
361+
intendedWorkingDirW,
362+
spawnContext.startupInfo.pointer(to: \.StartupInfo)!,
363+
&processInfo
364+
)
365+
return (created, processInfo, GetLastError())
366+
}
345367
}
346368
}
347369
}
@@ -353,8 +375,6 @@ extension Configuration {
353375

354376

355377
guard created else {
356-
let windowsError = GetLastError()
357-
358378
if windowsError == ERROR_FILE_NOT_FOUND || windowsError == ERROR_PATH_NOT_FOUND {
359379
// This executable path is not it. Try the next one
360380
continue
@@ -1044,8 +1064,8 @@ extension Configuration {
10441064
outputWrite outputWriteFileDescriptor: borrowing IODescriptor?,
10451065
errorRead errorReadFileDescriptor: borrowing IODescriptor?,
10461066
errorWrite errorWriteFileDescriptor: borrowing IODescriptor?,
1047-
_ body: (UnsafeMutablePointer<STARTUPINFOEXW>) throws -> Result
1048-
) rethrows -> Result {
1067+
_ body: (UnsafeMutablePointer<STARTUPINFOEXW>) async throws -> Result
1068+
) async throws -> Result {
10491069
var info: STARTUPINFOEXW = STARTUPINFOEXW()
10501070
info.StartupInfo.cb = DWORD(MemoryLayout.size(ofValue: info))
10511071
info.StartupInfo.dwFlags |= DWORD(STARTF_USESTDHANDLES)
@@ -1111,35 +1131,41 @@ extension Configuration {
11111131
let alignment = 16
11121132
var attributeListByteCount = SIZE_T(0)
11131133
_ = InitializeProcThreadAttributeList(nil, 1, 0, &attributeListByteCount)
1114-
return try withUnsafeTemporaryAllocation(byteCount: Int(attributeListByteCount), alignment: alignment) { attributeListPtr in
1115-
let attributeList = LPPROC_THREAD_ATTRIBUTE_LIST(attributeListPtr.baseAddress!)
1116-
guard InitializeProcThreadAttributeList(attributeList, 1, 0, &attributeListByteCount) else {
1117-
throw SubprocessError(
1118-
code: .init(.spawnFailed),
1119-
underlyingError: .init(rawValue: GetLastError())
1120-
)
1121-
}
1122-
defer {
1123-
DeleteProcThreadAttributeList(attributeList)
1124-
}
1134+
// We can't use withUnsafeTemporaryAllocation here because body is async
1135+
let attributeListPtr: UnsafeMutableRawBufferPointer = .allocate(
1136+
byteCount: Int(attributeListByteCount),
1137+
alignment: alignment
1138+
)
1139+
defer {
1140+
attributeListPtr.deallocate()
1141+
}
11251142

1126-
var handles = Array(inheritedHandles)
1127-
return try handles.withUnsafeMutableBufferPointer { inheritedHandlesPtr in
1128-
_ = UpdateProcThreadAttribute(
1129-
attributeList,
1130-
0,
1131-
_subprocess_PROC_THREAD_ATTRIBUTE_HANDLE_LIST(),
1132-
inheritedHandlesPtr.baseAddress!,
1133-
SIZE_T(MemoryLayout<HANDLE>.stride * inheritedHandlesPtr.count),
1134-
nil,
1135-
nil
1136-
)
1143+
let attributeList = LPPROC_THREAD_ATTRIBUTE_LIST(attributeListPtr.baseAddress!)
1144+
guard InitializeProcThreadAttributeList(attributeList, 1, 0, &attributeListByteCount) else {
1145+
throw SubprocessError(
1146+
code: .init(.spawnFailed),
1147+
underlyingError: .init(rawValue: GetLastError())
1148+
)
1149+
}
1150+
defer {
1151+
DeleteProcThreadAttributeList(attributeList)
1152+
}
11371153

1138-
info.lpAttributeList = attributeList
1154+
var handles = Array(inheritedHandles)
1155+
handles.withUnsafeMutableBufferPointer { inheritedHandlesPtr in
1156+
_ = UpdateProcThreadAttribute(
1157+
attributeList,
1158+
0,
1159+
_subprocess_PROC_THREAD_ATTRIBUTE_HANDLE_LIST(),
1160+
inheritedHandlesPtr.baseAddress!,
1161+
SIZE_T(MemoryLayout<HANDLE>.stride * inheritedHandlesPtr.count),
1162+
nil,
1163+
nil
1164+
)
11391165

1140-
return try body(&info)
1141-
}
1166+
info.lpAttributeList = attributeList
11421167
}
1168+
return try await body(&info)
11431169
}
11441170

11451171
private func generateWindowsCommandAndArguments(

0 commit comments

Comments
 (0)