Skip to content

Commit 9c8fa82

Browse files
authored
Merge pull request #283 from compnerd/SR-13806
TSCBasic: change the behaviour of `exec` on Windows
2 parents 4afd18e + ed8c5d8 commit 9c8fa82

File tree

1 file changed

+133
-8
lines changed

1 file changed

+133
-8
lines changed

Sources/TSCBasic/misc.swift

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,80 @@
1010

1111
import TSCLibc
1212
import Foundation
13+
#if os(Windows)
14+
import WinSDK
15+
#endif
1316

1417
#if os(Windows)
1518
public let executableFileSuffix = ".exe"
1619
#else
1720
public let executableFileSuffix = ""
1821
#endif
1922

23+
#if os(Windows)
24+
private func quote(_ arguments: [String]) -> String {
25+
func quote(argument: String) -> String {
26+
if !argument.contains(where: { " \t\n\"".contains($0) }) {
27+
return argument
28+
}
29+
30+
// To escape the command line, we surround the argument with quotes.
31+
// However, the complication comes due to how the Windows command line
32+
// parser treats backslashes (\) and quotes (").
33+
//
34+
// - \ is normally treated as a literal backslash
35+
// e.g. alpha\beta\gamma => alpha\beta\gamma
36+
// - The sequence \" is treated as a literal "
37+
// e.g. alpha\"beta => alpha"beta
38+
//
39+
// But then what if we are given a path that ends with a \?
40+
//
41+
// Surrounding alpha\beta\ with " would be "alpha\beta\" which would be
42+
// an unterminated string since it ends on a literal quote. To allow
43+
// this case the parser treats:
44+
//
45+
// - \\" as \ followed by the " metacharacter
46+
// - \\\" as \ followed by a literal "
47+
//
48+
// In general:
49+
// - 2n \ followed by " => n \ followed by the " metacharacter
50+
// - 2n + 1 \ followed by " => n \ followed by a literal "
51+
52+
var quoted = "\""
53+
var unquoted = argument.unicodeScalars
54+
55+
while !unquoted.isEmpty {
56+
guard let firstNonBS = unquoted.firstIndex(where: { $0 != "\\" }) else {
57+
// String ends with a backslash (e.g. first\second\), escape all
58+
// the backslashes then add the metacharacter ".
59+
let count = unquoted.count
60+
quoted.append(String(repeating: "\\", count: 2 * count))
61+
break
62+
}
63+
64+
let count = unquoted.distance(from: unquoted.startIndex, to: firstNonBS)
65+
if unquoted[firstNonBS] == "\"" {
66+
// This is a string of \ followed by a " (e.g. first\"second).
67+
// Escape the backslashes and the quote.
68+
quoted.append(String(repeating: "\\", count: 2 * count + 1))
69+
} else {
70+
// These are just literal backslashes
71+
quoted.append(String(repeating: "\\", count: count))
72+
}
73+
74+
quoted.append(String(unquoted[firstNonBS]))
75+
76+
// Drop the backslashes and the following character
77+
unquoted.removeFirst(count + 1)
78+
}
79+
quoted.append("\"")
80+
81+
return quoted
82+
}
83+
return arguments.map(quote(argument:)).joined(separator: " ")
84+
}
85+
#endif
86+
2087
/// Replace the current process image with a new process image.
2188
///
2289
/// - Parameters:
@@ -25,20 +92,78 @@ public let executableFileSuffix = ""
2592
public func exec(path: String, args: [String]) throws -> Never {
2693
let cArgs = CStringArray(args)
2794
#if os(Windows)
28-
guard cArgs.cArray.withUnsafeBufferPointer({
29-
$0.withMemoryRebound(to: UnsafePointer<Int8>?.self, {
30-
_execv(path, $0.baseAddress) != -1
31-
})
32-
})
33-
else {
34-
throw SystemError.exec(errno, path: path, args: args)
95+
var hJob: HANDLE
96+
97+
hJob = CreateJobObjectA(nil, nil)
98+
if hJob == HANDLE(bitPattern: 0) {
99+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
35100
}
101+
defer { CloseHandle(hJob) }
102+
103+
let hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1)
104+
if hPort == HANDLE(bitPattern: 0) {
105+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
106+
}
107+
108+
var acpAssociation: JOBOBJECT_ASSOCIATE_COMPLETION_PORT = JOBOBJECT_ASSOCIATE_COMPLETION_PORT()
109+
acpAssociation.CompletionKey = hJob
110+
acpAssociation.CompletionPort = hPort
111+
if !SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation,
112+
&acpAssociation, DWORD(MemoryLayout<JOBOBJECT_ASSOCIATE_COMPLETION_PORT>.size)) {
113+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
114+
}
115+
116+
var eliLimits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
117+
eliLimits.BasicLimitInformation.LimitFlags =
118+
DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) | DWORD(JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)
119+
if !SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &eliLimits,
120+
DWORD(MemoryLayout<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>.size)) {
121+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
122+
}
123+
124+
125+
var siInfo: STARTUPINFOW = STARTUPINFOW()
126+
siInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
127+
128+
var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION()
129+
130+
try quote(args).withCString(encodedAs: UTF16.self) { pwszCommandLine in
131+
if !CreateProcessW(nil,
132+
UnsafeMutablePointer<WCHAR>(mutating: pwszCommandLine),
133+
nil, nil, false,
134+
DWORD(CREATE_SUSPENDED) | DWORD(CREATE_NEW_PROCESS_GROUP),
135+
nil, nil, &siInfo, &piInfo) {
136+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
137+
}
138+
}
139+
140+
defer { CloseHandle(piInfo.hThread) }
141+
defer { CloseHandle(piInfo.hProcess) }
142+
143+
if !AssignProcessToJobObject(hJob, piInfo.hProcess) {
144+
throw SystemError.exec(Int32(GetLastError()), path: path, args: args)
145+
}
146+
147+
_ = ResumeThread(piInfo.hThread)
148+
149+
var dwCompletionCode: DWORD = 0
150+
var ulCompletionKey: ULONG_PTR = 0
151+
var lpOverlapped: LPOVERLAPPED?
152+
repeat {
153+
} while GetQueuedCompletionStatus(hPort, &dwCompletionCode, &ulCompletionKey,
154+
&lpOverlapped, INFINITE) &&
155+
!(ulCompletionKey == ULONG_PTR(UInt(bitPattern: hJob)) &&
156+
dwCompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
157+
158+
var dwExitCode: DWORD = DWORD(bitPattern: -1)
159+
_ = GetExitCodeProcess(piInfo.hProcess, &dwExitCode)
160+
_exit(Int32(bitPattern: dwExitCode))
36161
#elseif (!canImport(Darwin) || os(macOS))
37162
guard execv(path, cArgs.cArray) != -1 else {
38163
throw SystemError.exec(errno, path: path, args: args)
39164
}
40-
#endif
41165
fatalError("unreachable")
166+
#endif
42167
}
43168

44169
@_disfavoredOverload

0 commit comments

Comments
 (0)