Skip to content

Reset the working directory of child processes we spawn on Windows. #1212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 8, 2025
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