Files
Cheviiot 91471397fa Fix toolrelay.exe's mt.exe detection: it never matched a real path
IsMtExe() looked for the last '\\' in the target executable path to
find the bare filename, but vintner passes toolExePath straight from
Go's filepath.Join (forward slashes) all the way through to
CreateProcessW - so the path toolrelay.exe actually receives has no
backslash in it at all, and IsMtExe() compared the *entire path*
against "mt.exe", which of course never matched. The whole point of
this file - translating mt.exe's CMake-compatibility exit code
(0x41020001 -> 0xbb) before Wine's own exit-code truncation destroys
it - has silently never fired in real usage.

This had gone unnoticed because MSBuild's rawStdout path bypasses
toolrelay.exe entirely, and every real end-to-end test so far
(msbuild-driven builds, including the KMDF driver) went through that
path. Found while getting a CMake+Ninja-generated MSVC build of a
real project (Ogre3D) past its `cmake -E vs_link_exe` manifest step,
which depends on exactly this translation.

Now checks for both '\\' and '/' and uses whichever separator occurs
last in the path. Verified directly: `mt /manifest ... /notify_update`
through the wrapper now exits 187 (0xbb) instead of 1, and the Ogre
build gets past the manifest-embedding step it was failing at.
2026-07-25 11:55:05 +10:00

135 lines
5.2 KiB
C++

// toolrelay.exe - a thin native launcher for a Wine-hosted MSVC tool
// invocation, built and used by vintner (see internal/install's
// buildToolRelay and internal/wrapper's runViaToolRelay).
//
// It exists to solve two problems that are only solvable from inside a
// real Windows process, not from the Unix side of Wine:
//
// 1. Wine truncates a Windows process's exit code down to a single byte
// when reporting it back to the host OS. mt.exe (the manifest tool)
// can legitimately exit with 0x41020001 ("manifest unchanged"), which
// CMake's own build driver specifically checks for and translates to
// 0xbb before treating it as success - see cmcmd.cxx upstream. If
// Wine has already truncated 0x41020001 down to 0x01 by the time our
// Go wrapper observes it, we can no longer make that translation
// ourselves. Only a native Windows process calling
// GetExitCodeProcess() can see the real, untruncated value - so this
// helper does the translation itself, then exits with the already-
// small 0xbb, which fits in a byte and survives Wine's truncation
// intact.
//
// 2. It gives the launched process its own Job Object (with
// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE), so an aborted build doesn't
// leave orphaned child processes running under Wine.
//
// Usage: toolrelay.exe <real-tool.exe> [args...]
//
// Optional environment variables MSVCGOWINE_STDIN / MSVCGOWINE_STDOUT /
// MSVCGOWINE_STDERR name files to redirect the child's standard handles to
// (used by runViaToolRelay to relay output through named pipes back to the
// Unix side); any that aren't set fall back to this process's own
// inherited handles.
#include <windows.h>
#include <string>
namespace {
constexpr DWORD kMtManifestUnchanged = 0x41020001;
constexpr DWORD kMtManifestUnchangedForCMake = 0xbb;
HANDLE OpenRedirectOrFallback(const wchar_t *envVar, DWORD access,
DWORD disposition, HANDLE fallback) {
wchar_t path[32768];
DWORD n = GetEnvironmentVariableW(envVar, path, ARRAYSIZE(path));
if (n == 0 || n >= ARRAYSIZE(path)) {
return fallback;
}
SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE};
HANDLE h = CreateFileW(path, access,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
&sa, disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
return h == INVALID_HANDLE_VALUE ? fallback : h;
}
// CreateProcessW wants a single mutable command line string, quoting each
// argument and escaping embedded quotes.
std::wstring BuildCommandLine(int argc, wchar_t **argv) {
std::wstring cmd;
for (int i = 0; i < argc; i++) {
if (i > 0) cmd += L' ';
cmd += L'"';
for (const wchar_t *c = argv[i]; *c; c++) {
if (*c == L'"') cmd += L'\\';
cmd += *c;
}
cmd += L'"';
}
return cmd;
}
HANDLE MakeKillOnCloseJob() {
HANDLE job = CreateJobObjectW(nullptr, nullptr);
if (!job) return nullptr;
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info));
return job;
}
bool IsMtExe(const wchar_t *path) {
// vintner passes toolExePath straight through from Go's filepath.Join,
// which uses forward slashes even for a path that's about to be handed
// to a native Windows process - so the separator here isn't reliably
// '\\'. Check both; using whichever comes later in the string covers a
// mixed-separator path too.
const wchar_t *back = wcsrchr(path, L'\\');
const wchar_t *fwd = wcsrchr(path, L'/');
const wchar_t *sep = back;
if (fwd && (!sep || fwd > sep)) sep = fwd;
const wchar_t *name = sep ? sep + 1 : path;
return _wcsicmp(name, L"mt.exe") == 0;
}
} // namespace
int wmain(int argc, wchar_t **argv) {
if (argc < 2) {
return 1;
}
const wchar_t *targetExe = argv[1];
STARTUPINFOW si{sizeof(si)};
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = OpenRedirectOrFallback(L"MSVCGOWINE_STDIN", GENERIC_READ,
OPEN_EXISTING, GetStdHandle(STD_INPUT_HANDLE));
si.hStdOutput = OpenRedirectOrFallback(L"MSVCGOWINE_STDOUT", GENERIC_WRITE,
CREATE_ALWAYS, GetStdHandle(STD_OUTPUT_HANDLE));
si.hStdError = OpenRedirectOrFallback(L"MSVCGOWINE_STDERR", GENERIC_WRITE,
CREATE_ALWAYS, GetStdHandle(STD_ERROR_HANDLE));
HANDLE job = MakeKillOnCloseJob();
std::wstring cmdLine = BuildCommandLine(argc - 1, argv + 1);
PROCESS_INFORMATION pi{};
DWORD exitCode;
if (CreateProcessW(targetExe, cmdLine.empty() ? nullptr : &cmdLine[0], nullptr,
nullptr, TRUE, 0, nullptr, nullptr, &si, &pi)) {
if (job) AssignProcessToJobObject(job, pi.hProcess);
WaitForSingleObject(pi.hProcess, INFINITE);
if (!GetExitCodeProcess(pi.hProcess, &exitCode)) {
exitCode = GetLastError();
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
} else {
exitCode = GetLastError();
}
if (IsMtExe(targetExe) && exitCode == kMtManifestUnchanged) {
exitCode = kMtManifestUnchangedForCMake;
}
return static_cast<int>(exitCode);
}