Skip to content

Commit 5ff2f38

Browse files
committed
executeCommandMingw, Windows does not have fork,pipe,etc.
1 parent 0aa2b81 commit 5ff2f38

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.smithy.kotlin.runtime.client.util
6+
7+
import kotlinx.cinterop.*
8+
import kotlinx.coroutines.withContext
9+
import aws.sdk.kotlin.runtime.util.SdkDispatchers // adjust import
10+
import platform.windows.*
11+
import platform.posix._wunlink // to delete the temp file afterwards
12+
13+
@OptIn(ExperimentalForeignApi::class)
14+
private fun String.wideCString(mem: MemScope) = wcstr.getPointer(mem)
15+
16+
@OptIn(ExperimentalForeignApi::class)
17+
internal actual suspend fun executeCommand(
18+
command: String,
19+
platformProvider: PlatformProvider,
20+
maxOutputLengthBytes: Long,
21+
timeoutMillis: Long,
22+
clock: Clock,
23+
): Pair<Int, String> = withContext(SdkDispatchers.IO) { memScoped {
24+
// 1) Make a temp file to capture stdout+stderr
25+
val tmpDirBuf = allocArray<UShortVar>(MAX_PATH)
26+
val tmpNameBuf = allocArray<UShortVar>(MAX_PATH)
27+
val gotTmp = GetTempPathW(MAX_PATH, tmpDirBuf) != 0u
28+
if (!gotTmp) error("GetTempPathW failed")
29+
30+
val gotName = GetTempFileNameW(tmpDirBuf, "KNR".wideCString(this), 0u, tmpNameBuf) != 0u
31+
if (!gotName) error("GetTempFileNameW failed")
32+
val outPath = tmpNameBuf
33+
34+
// Create the file for the child to write into (inherit handle)
35+
val sa = alloc<SECURITY_ATTRIBUTES>().apply {
36+
nLength = sizeOf<SECURITY_ATTRIBUTES>().toUInt()
37+
bInheritHandle = TRUE
38+
lpSecurityDescriptor = null
39+
}
40+
val hOut: HANDLE = CreateFileW(
41+
outPath,
42+
(GENERIC_WRITE or FILE_GENERIC_WRITE).toUInt(),
43+
FILE_SHARE_READ or FILE_SHARE_WRITE,
44+
sa.ptr,
45+
CREATE_ALWAYS,
46+
FILE_ATTRIBUTE_NORMAL.toUInt(),
47+
null
48+
)
49+
if (hOut == INVALID_HANDLE_VALUE) error("CreateFileW failed for temp output")
50+
51+
try {
52+
// 2) Build command: use cmd.exe /C "<command>"
53+
// Prefer %ComSpec% if present, else fallback.
54+
val comspecBuf = allocArray<WCHARVar>(MAX_PATH)
55+
val comspecLen = GetEnvironmentVariableW("ComSpec".wideCString(this), comspecBuf, MAX_PATH)
56+
val cmdExe = if (comspecLen > 0u) { comspecBuf } else { "C:\\Windows\\System32\\cmd.exe".wideCString(this) }
57+
58+
val cmdLine = (" /C " + command).wideCString(this)
59+
60+
// 3) Launch child with redirected stdout/stderr
61+
val si = alloc<STARTUPINFOW>().apply {
62+
cb = sizeOf<STARTUPINFOW>().toUInt()
63+
dwFlags = STARTF_USESTDHANDLES.toUInt()
64+
hStdOutput = hOut
65+
hStdError = hOut
66+
hStdInput = GetStdHandle(STD_INPUT_HANDLE) // leave as-is
67+
}
68+
val pi = alloc<PROCESS_INFORMATION>()
69+
70+
val created = CreateProcessW(
71+
cmdExe,
72+
cmdLine, // mutable buffer OK; wcstr gives writable copy here
73+
null,
74+
null,
75+
TRUE, // inherit handles (so child gets hOut)
76+
CREATE_NO_WINDOW.toUInt(),
77+
null,
78+
null,
79+
si.ptr,
80+
pi.ptr
81+
)
82+
if (created == 0) error("CreateProcessW failed: ${GetLastError()}")
83+
84+
try {
85+
// 4) Wait up to timeout; if it times out, terminate
86+
val waitRc = WaitForSingleObject(pi.hProcess, timeoutMillis.toUInt())
87+
if (waitRc == WAIT_TIMEOUT) {
88+
TerminateProcess(pi.hProcess, 124u)
89+
// close I/O + process handles and delete temp before throwing
90+
CloseHandle(hOut)
91+
CloseHandle(pi.hThread)
92+
CloseHandle(pi.hProcess)
93+
_wunlink(outPath)
94+
error("Process timed out after ${timeoutMillis}ms")
95+
}
96+
97+
98+
// 5) Get exit code
99+
val exitCodeVar = alloc<DWORDVar>()
100+
GetExitCodeProcess(pi.hProcess, exitCodeVar.ptr)
101+
val exitCode = exitCodeVar.value.toInt()
102+
103+
// 6) Read the file (up to maxOutputLengthBytes)
104+
// Re-open for reading (child still closed hOut on exit)
105+
val hIn: HANDLE = CreateFileW(
106+
outPath,
107+
GENERIC_READ.toUInt(),
108+
FILE_SHARE_READ or FILE_SHARE_WRITE,
109+
null,
110+
OPEN_EXISTING,
111+
FILE_ATTRIBUTE_NORMAL.toUInt(),
112+
null
113+
)
114+
if (hIn == INVALID_HANDLE_VALUE) {
115+
// Clean up and bail
116+
_wunlink(outPath)
117+
CloseHandle(pi.hThread)
118+
CloseHandle(pi.hProcess)
119+
return@memScoped exitCode to ""
120+
}
121+
val sb = StringBuilder()
122+
val buf = ByteArray(4096)
123+
val bytesReadVar = alloc<DWORDVar>()
124+
var total = 0L
125+
try {
126+
while (true) {
127+
val toRead = minOf(buf.size.toLong(), maxOutputLengthBytes - total).toInt()
128+
if (toRead <= 0) {
129+
// ensure cleanup before throwing
130+
CloseHandle(hIn)
131+
_wunlink(outPath)
132+
throw CredentialsProviderException("Process output exceeded limit of $maxOutputLengthBytes bytes")
133+
}
134+
val n = buf.usePinned {
135+
val ok = ReadFile(hIn, it.addressOf(0), toRead.toUInt(), bytesReadVar.ptr, null)
136+
if (ok == 0 || bytesReadVar.value == 0u) 0 else bytesReadVar.value.toInt()
137+
}
138+
if (n <= 0) break
139+
total += n
140+
sb.append(buf.decodeToString(0, n))
141+
}
142+
} finally {
143+
CloseHandle(hIn)
144+
_wunlink(outPath)
145+
}
146+
exitCode to sb.toString()
147+
} finally {
148+
CloseHandle(pi.hThread)
149+
CloseHandle(pi.hProcess)
150+
}
151+
} finally {
152+
CloseHandle(hOut)
153+
}
154+
} }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ internal actual suspend fun executeCommand(
7979
throw CredentialsProviderException("Process output exceeded limit of $maxOutputLengthBytes bytes")
8080
}
8181

82+
@OptIn(UnsafeNumber::class)
8283
val rc = read(readFd, buffer.refTo(0), nBytes.toULong()).toInt()
8384
if (rc <= 0) break
8485
totalBytesRead += rc

0 commit comments

Comments
 (0)