Skip to content
30 changes: 26 additions & 4 deletions Sources/Testing/ExitTests/SpawnProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,25 +284,47 @@ func spawnExecutable(
let commandLine = _escapeCommandLine(CollectionOfOne(executablePath) + arguments)
let environ = environment.map { "\($0.key)=\($0.value)" }.joined(separator: "\0") + "\0\0"

// CreateProcessW() may modify the command line argument, so we must make
// a mutable copy of it. (environ is also passed as a mutable raw pointer,
// but it is not documented as actually being mutated.)
let commandLineCopy = commandLine.withCString(encodedAs: UTF16.self) { _wcsdup($0) }
defer {
free(commandLineCopy)
}

// On Windows, a process holds a reference to its current working
// directory, which prevents other processes from deleting it. This causes
// code to fail if it tries to set the working directory to a temporary
// path. SEE: https://github.com/swiftlang/swift-testing/issues/1209
//
// This problem manifests for us when we spawn a child process without
// setting its working directory, which causes it to default to that of
// the parent process. To avoid this problem, we set the working directory
// of the new process to the root directory of the boot volume (which is
// unlikely to be deleted, one hopes).
//
// SEE: https://devblogs.microsoft.com/oldnewthing/20101109-00/?p=12323
let workingDirectoryPath = rootDirectoryPath

var flags = DWORD(CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT)
#if DEBUG
// Start the process suspended so we can attach a debugger if needed.
flags |= DWORD(CREATE_SUSPENDED)
#endif

return try commandLine.withCString(encodedAs: UTF16.self) { commandLine in
try environ.withCString(encodedAs: UTF16.self) { environ in
return try environ.withCString(encodedAs: UTF16.self) { environ in
try workingDirectoryPath.withCString(encodedAs: UTF16.self) { workingDirectoryPath in
var processInfo = PROCESS_INFORMATION()

guard CreateProcessW(
nil,
.init(mutating: commandLine),
commandLineCopy,
nil,
nil,
true, // bInheritHandles
flags,
.init(mutating: environ),
nil,
workingDirectoryPath,
startupInfo.pointer(to: \.StartupInfo)!,
&processInfo
) else {
Expand Down
31 changes: 31 additions & 0 deletions Sources/Testing/Support/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -719,4 +719,35 @@ func setFD_CLOEXEC(_ flag: Bool, onFileDescriptor fd: CInt) throws {
}
}
#endif

/// The path to the root directory of the boot volume.
///
/// On Windows, this string is usually of the form `"C:\"`. On UNIX-like
/// platforms, it is always equal to `"/"`.
let rootDirectoryPath: String = {
#if os(Windows)
var result: String?

// The boot volume is, except in some legacy scenarios, the volume that
// contains the system Windows directory. For an explanation of the difference
// between the Windows directory and the _system_ Windows directory, see
// https://devblogs.microsoft.com/oldnewthing/20140723-00/?p=423 .
let count = GetSystemWindowsDirectoryW(nil, 0)
if count > 0 {
withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: Int(count) + 1) { buffer in
_ = GetSystemWindowsDirectoryW(buffer.baseAddress!, UINT(buffer.count))
let rStrip = PathCchStripToRoot(buffer.baseAddress!, buffer.count)
if rStrip == S_OK || rStrip == S_FALSE {
result = String.decodeCString(buffer.baseAddress!, as: UTF16.self)?.result
}
}
}

// If we weren't able to get a path, fall back to "C:\" on the assumption that
// it's the common case and most likely correct.
return result ?? #"C:\"#
#else
return "/"
#endif
}()
#endif
11 changes: 11 additions & 0 deletions Tests/TestingTests/Support/FileHandleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,17 @@ struct FileHandleTests {
#endif
}
#endif

@Test("Root directory path is correct")
func rootDirectoryPathIsCorrect() throws {
#if os(Windows)
if let systemDrive = Environment.variable(named: "SYSTEMDRIVE") {
#expect(rootDirectoryPath.starts(with: systemDrive))
}
#else
#expect(rootDirectoryPath == "/")
#endif
}
}

// MARK: - Fixtures
Expand Down