@@ -266,7 +266,7 @@ extension ExitTest {
266
266
} ( )
267
267
268
268
return { exitTest in
269
- let childProcessURL = try URL ( fileURLWithPath : childProcessExecutablePath. get ( ) , isDirectory : false )
269
+ let childProcessExecutablePath = try childProcessExecutablePath. get ( )
270
270
271
271
// Inherit the environment from the parent process and make any necessary
272
272
// platform-specific changes.
@@ -288,31 +288,162 @@ extension ExitTest {
288
288
// to run.
289
289
childEnvironment [ " SWT_EXPERIMENTAL_EXIT_TEST_SOURCE_LOCATION " ] = try String ( data: JSONEncoder ( ) . encode ( exitTest. sourceLocation) , encoding: . utf8) !
290
290
291
- let ( actualExitCode, wasSignalled) = try await withCheckedThrowingContinuation { continuation in
292
- let process = Process ( )
293
- process. executableURL = childProcessURL
294
- process. arguments = childArguments
295
- process. environment = childEnvironment
296
- process. terminationHandler = { process in
297
- continuation. resume ( returning: ( process. terminationStatus, process. terminationReason == . uncaughtSignal) )
291
+ return try await _spawnAndWait (
292
+ forExecutableAtPath: childProcessExecutablePath,
293
+ arguments: childArguments,
294
+ environment: childEnvironment
295
+ )
296
+ }
297
+ }
298
+
299
+ /// Spawn a process and wait for it to terminate.
300
+ ///
301
+ /// - Parameters:
302
+ /// - executablePath: The path to the executable to spawn.
303
+ /// - arguments: The arguments to pass to the executable, not including the
304
+ /// executable path.
305
+ /// - environment: The environment block to pass to the executable.
306
+ ///
307
+ /// - Returns: The exit condition of the spawned process.
308
+ ///
309
+ /// - Throws: Any error that prevented the process from spawning or its exit
310
+ /// condition from being read.
311
+ private static func _spawnAndWait(
312
+ forExecutableAtPath executablePath: String ,
313
+ arguments: [ String ] ,
314
+ environment: [ String : String ]
315
+ ) async throws -> ExitCondition {
316
+ // Darwin and Linux differ in their optionality for the posix_spawn types we
317
+ // use, so use this typealias to paper over the differences.
318
+ #if SWT_TARGET_OS_APPLE
319
+ typealias P < T> = T ?
320
+ #elseif os(Linux)
321
+ typealias P < T> = T
322
+ #endif
323
+
324
+ #if SWT_TARGET_OS_APPLE || os(Linux)
325
+ let pid = try withUnsafeTemporaryAllocation ( of: P< posix_spawn_file_actions_t> . self , capacity: 1 ) { fileActions in
326
+ guard 0 == posix_spawn_file_actions_init ( fileActions. baseAddress!) else {
327
+ throw CError ( rawValue: swt_errno ( ) )
328
+ }
329
+ defer {
330
+ _ = posix_spawn_file_actions_destroy ( fileActions. baseAddress!)
331
+ }
332
+
333
+ // Do not forward standard I/O.
334
+ _ = posix_spawn_file_actions_addopen ( fileActions. baseAddress!, STDIN_FILENO, " /dev/null " , O_RDONLY, 0 )
335
+ _ = posix_spawn_file_actions_addopen ( fileActions. baseAddress!, STDOUT_FILENO, " /dev/null " , O_WRONLY, 0 )
336
+ _ = posix_spawn_file_actions_addopen ( fileActions. baseAddress!, STDERR_FILENO, " /dev/null " , O_WRONLY, 0 )
337
+
338
+ return try withUnsafeTemporaryAllocation ( of: P< posix_spawnattr_t> . self , capacity: 1 ) { attrs in
339
+ guard 0 == posix_spawnattr_init ( attrs. baseAddress!) else {
340
+ throw CError ( rawValue: swt_errno ( ) )
298
341
}
299
- do {
300
- try process. run ( )
301
- } catch {
302
- continuation. resume ( throwing: error)
342
+ defer {
343
+ _ = posix_spawnattr_destroy ( attrs. baseAddress!)
344
+ }
345
+ #if SWT_TARGET_OS_APPLE
346
+ // Close all other file descriptors open in the parent. Note that Linux
347
+ // does not support this flag and, unlike Foundation.Process, we do not
348
+ // attempt to emulate it.
349
+ _ = posix_spawnattr_setflags ( attrs. baseAddress!, CShort ( POSIX_SPAWN_CLOEXEC_DEFAULT) )
350
+ #endif
351
+
352
+ var argv : [ UnsafeMutablePointer < CChar > ? ] = [ strdup ( executablePath) ]
353
+ argv += arguments. lazy. map { strdup ( $0) }
354
+ argv. append ( nil )
355
+ defer {
356
+ for arg in argv {
357
+ free ( arg)
358
+ }
303
359
}
360
+
361
+ var environ : [ UnsafeMutablePointer < CChar > ? ] = environment. map { strdup ( " \( $0. key) = \( $0. value) " ) }
362
+ environ. append ( nil )
363
+ defer {
364
+ for environ in environ {
365
+ free ( environ)
366
+ }
367
+ }
368
+
369
+ var pid = pid_t ( )
370
+ guard 0 == posix_spawn ( & pid, executablePath, fileActions. baseAddress!, attrs. baseAddress, argv, environ) else {
371
+ throw CError ( rawValue: swt_errno ( ) )
372
+ }
373
+ return pid
304
374
}
375
+ }
305
376
306
- if wasSignalled {
307
- #if os(Windows)
308
- // Actually an uncaught SEH/VEH exception (which we don't model yet.)
309
- return . failure
310
- #else
311
- return . signal( actualExitCode)
312
- #endif
377
+ return try await wait ( for: pid)
378
+ #elseif os(Windows)
379
+ // NOTE: Windows processes are responsible for handling their own
380
+ // command-line escaping. This code is adapted from the code in
381
+ // swift-corelibs-foundation (SEE: quoteWindowsCommandLine()) which was
382
+ // itself adapted from the code published by Microsoft at
383
+ // https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
384
+ let commandLine = ( CollectionOfOne ( executablePath) + arguments) . lazy
385
+ . map { arg in
386
+ if !arg. contains ( where: { " \t \n \" " . contains ( $0) } ) {
387
+ return arg
388
+ }
389
+
390
+ var quoted = " \" "
391
+ var unquoted = arg. unicodeScalars
392
+ while !unquoted. isEmpty {
393
+ guard let firstNonBackslash = unquoted. firstIndex ( where: { $0 != " \\ " } ) else {
394
+ let backslashCount = unquoted. count
395
+ quoted. append ( String ( repeating: " \\ " , count: backslashCount * 2 ) )
396
+ break
397
+ }
398
+ let backslashCount = unquoted. distance ( from: unquoted. startIndex, to: firstNonBackslash)
399
+ if ( unquoted [ firstNonBackslash] == " \" " ) {
400
+ quoted. append ( String ( repeating: " \\ " , count: backslashCount * 2 + 1 ) )
401
+ quoted. append ( String ( unquoted [ firstNonBackslash] ) )
402
+ } else {
403
+ quoted. append ( String ( repeating: " \\ " , count: backslashCount) )
404
+ quoted. append ( String ( unquoted [ firstNonBackslash] ) )
405
+ }
406
+ unquoted. removeFirst ( backslashCount + 1 )
407
+ }
408
+ quoted. append ( " \" " )
409
+ return quoted
410
+ } . joined ( separator: " " )
411
+ let environ = environment. map { " \( $0. key) = \( $0. value) " } . joined ( separator: " \0 " ) + " \0 \0 "
412
+
413
+ let processHandle : HANDLE ! = try commandLine. withCString ( encodedAs: UTF16 . self) { commandLine in
414
+ try environ. withCString ( encodedAs: UTF16 . self) { environ in
415
+ var processInfo = PROCESS_INFORMATION ( )
416
+
417
+ var startupInfo = STARTUPINFOW ( )
418
+ startupInfo. cb = DWORD ( MemoryLayout . size ( ofValue: startupInfo) )
419
+ guard CreateProcessW (
420
+ nil ,
421
+ . init( mutating: commandLine) ,
422
+ nil ,
423
+ nil ,
424
+ false ,
425
+ DWORD ( CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT) ,
426
+ . init( mutating: environ) ,
427
+ nil ,
428
+ & startupInfo,
429
+ & processInfo
430
+ ) else {
431
+ throw Win32Error ( rawValue: GetLastError ( ) )
432
+ }
433
+ _ = CloseHandle ( processInfo. hThread)
434
+
435
+ return processInfo. hProcess
313
436
}
314
- return . exitCode( actualExitCode)
315
437
}
438
+ defer {
439
+ CloseHandle ( processHandle)
440
+ }
441
+
442
+ return try await wait ( for: processHandle)
443
+ #else
444
+ #warning("Platform-specific implementation missing: process spawning unavailable")
445
+ throw SystemError ( description: " Exit tests are unimplemented on this platform. " )
446
+ #endif
316
447
}
317
448
}
318
449
#endif
0 commit comments