fix(win): route shell-outs through a windowless helper (no cmd.exe flash)

_popen/_popen-style shell-outs flash a cmd.exe console window on Windows. Add
Platform::runHiddenCapture() — CreateProcess + CREATE_NO_WINDOW capturing stdout on
Windows, popen on POSIX — and route the remaining shell-outs through it:
- GPU-aware idle detection (getGpuUtilization: "where nvidia-smi" / "nvidia-smi --query-gpu")
- xmrig discovery + version (findXmrigBinary "where xmrig.exe"; "<bin> --version", stderr merged)
- wallet-rebuild helper (app_network) — keeps its exit-code check via the new exitCode out-param

None of these are on the launch path (that was the daemon spawn, fixed in a2f84be); each
would flash a console only when it ran (idle-GPU mining, mining tab, wallet recovery).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 23:58:54 -05:00
parent a2f84be2d4
commit 0942691eb3
4 changed files with 101 additions and 71 deletions

View File

@@ -5043,21 +5043,12 @@ void App::rebuildWalletDatabase()
const std::string tmpOut = datadir + "/wallet.rebuilt-" + std::string(ts) + ".tmp"; const std::string tmpOut = datadir + "/wallet.rebuilt-" + std::string(ts) + ".tmp";
{ std::error_code ec; fs::remove(tmpOut, ec); } // helper uses DB_EXCL — path must be fresh { std::error_code ec; fs::remove(tmpOut, ec); } // helper uses DB_EXCL — path must be fresh
// 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line. // 3. Run the helper (src -> tmpOut). Quote both paths; capture its JSON line. Windowless
std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\""; // (runHiddenCapture) so a wallet rebuild never flashes a cmd.exe console; it runs the
#ifdef _WIN32 // helper via CreateProcess directly on Windows, so no cmd.exe outer-quote wrap is needed.
cmd = "\"" + cmd + "\""; // cmd.exe strips the outermost quotes const std::string cmd = "\"" + helper + "\" \"" + src + "\" \"" + tmpOut + "\"";
FILE* fp = _popen(cmd.c_str(), "r"); int rc = -1;
#else const std::string jout = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/false, &rc);
FILE* fp = popen(cmd.c_str(), "r");
#endif
std::string jout;
if (fp) { char b[512]; while (std::fgets(b, sizeof b, fp)) jout += b; }
#ifdef _WIN32
const int rc = fp ? _pclose(fp) : -1;
#else
const int rc = fp ? pclose(fp) : -1;
#endif
DEBUG_LOGF("[App] wallet-rebuild helper rc=%d out=%s\n", rc, jout.c_str()); DEBUG_LOGF("[App] wallet-rebuild helper rc=%d out=%s\n", rc, jout.c_str());
// 4. Verify-before-swap: the output must be a readable BDB with the fund-critical keys. // 4. Verify-before-swap: the output must be a readable BDB with the fund-critical keys.

View File

@@ -23,6 +23,7 @@
#include <curl/curl.h> #include <curl/curl.h>
#include "../util/logger.h" #include "../util/logger.h"
#include "../util/platform.h"
#include "../util/pool_registry.h" #include "../util/pool_registry.h"
#ifdef _WIN32 #ifdef _WIN32
@@ -145,32 +146,18 @@ std::string XmrigManager::findXmrigBinary() {
return path; return path;
} }
// Fallback: system PATH // Fallback: system PATH — windowless so it never flashes a console.
#ifdef _WIN32 #ifdef _WIN32
FILE* f = _popen("where xmrig.exe 2>nul", "r"); std::string out = util::Platform::runHiddenCapture("where xmrig.exe");
#else #else
FILE* f = popen("which xmrig 2>/dev/null", "r"); std::string out = util::Platform::runHiddenCapture("which xmrig");
#endif
if (f) {
char line[512];
if (fgets(line, sizeof(line), f)) {
std::string s(line);
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
s.pop_back();
if (!s.empty() && fs::exists(s)) {
#ifdef _WIN32
_pclose(f);
#else
pclose(f);
#endif
return s;
}
}
#ifdef _WIN32
_pclose(f);
#else
pclose(f);
#endif #endif
{
std::string s = out;
const auto nl = s.find_first_of("\r\n"); // first line only
if (nl != std::string::npos) s.erase(nl);
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
if (!s.empty() && fs::exists(s)) return s;
} }
return {}; return {};
@@ -927,24 +914,10 @@ void XmrigManager::startVersionDetection()
const bool binShellSafe = const bool binShellSafe =
!bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos; !bin.empty() && bin.find_first_of("\"'`$;&|<>^%\n\r") == std::string::npos;
if (binShellSafe) { if (binShellSafe) {
const std::string cmd = "\"" + bin + "\" --version 2>&1"; // Windowless capture (mergeStderr: xmrig may print --version to stderr) — never flashes.
#ifdef _WIN32 const std::string cmd = "\"" + bin + "\" --version";
FILE* fp = _popen(cmd.c_str(), "r"); const std::string out = util::Platform::runHiddenCapture(cmd, /*mergeStderr=*/true);
#else if (!out.empty()) ver = parseMinerVersion(out);
FILE* fp = popen(cmd.c_str(), "r");
#endif
if (fp) {
std::string out;
char buf[256];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) out.append(buf, n);
#ifdef _WIN32
_pclose(fp);
#else
pclose(fp);
#endif
ver = parseMinerVersion(out);
}
} }
std::lock_guard<std::mutex> lk(g_installed_ver_mutex); std::lock_guard<std::mutex> lk(g_installed_ver_mutex);
g_installed_ver = ver; g_installed_ver = ver;

View File

@@ -867,6 +867,70 @@ int Platform::getSystemIdleSeconds()
// GPU utilization detection // GPU utilization detection
// ============================================================================ // ============================================================================
std::string Platform::runHiddenCapture(const std::string& cmdLine, bool mergeStderr, int* exitCode)
{
if (exitCode) *exitCode = -1;
#ifdef _WIN32
SECURITY_ATTRIBUTES sa;
ZeroMemory(&sa, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE hRead = NULL, hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return {};
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0); // parent's read end stays private
HANDLE hNul = INVALID_HANDLE_VALUE;
if (!mergeStderr) {
hNul = CreateFileA("NUL", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
}
STARTUPINFOA si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = hWrite;
si.hStdError = mergeStderr ? hWrite : hNul;
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
std::string cl = cmdLine; // CreateProcessA may modify lpCommandLine → needs a mutable buffer
std::string out;
if (CreateProcessA(NULL, cl.empty() ? NULL : &cl[0], NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
CloseHandle(hWrite); hWrite = NULL; // close our copy so ReadFile hits EOF when the child exits
if (hNul != INVALID_HANDLE_VALUE) { CloseHandle(hNul); hNul = INVALID_HANDLE_VALUE; }
char buf[4096];
DWORD n = 0;
while (ReadFile(hRead, buf, sizeof(buf), &n, NULL) && n > 0) out.append(buf, n);
WaitForSingleObject(pi.hProcess, INFINITE);
if (exitCode) {
DWORD code = 0;
if (GetExitCodeProcess(pi.hProcess, &code)) *exitCode = static_cast<int>(code);
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
if (hWrite != NULL) CloseHandle(hWrite);
if (hNul != INVALID_HANDLE_VALUE) CloseHandle(hNul);
CloseHandle(hRead);
return out;
#else
const std::string full = cmdLine + (mergeStderr ? " 2>&1" : " 2>/dev/null");
std::string out;
FILE* f = popen(full.c_str(), "r");
if (!f) return out;
char buf[512];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) out.append(buf, n);
const int st = pclose(f);
if (exitCode) *exitCode = st; // raw status (matches prior pclose-based rc checks)
return out;
#endif
}
int Platform::getGpuUtilization() int Platform::getGpuUtilization()
{ {
#ifdef _WIN32 #ifdef _WIN32
@@ -877,23 +941,16 @@ int Platform::getGpuUtilization()
static bool s_has_nvidia = false; static bool s_has_nvidia = false;
if (!s_tried_nvidia) { if (!s_tried_nvidia) {
s_tried_nvidia = true; s_tried_nvidia = true;
FILE* f = _popen("where nvidia-smi 2>nul", "r"); // Windowless (runHiddenCapture) so GPU-aware idle detection never flashes a cmd.exe console.
if (f) { const std::string w = runHiddenCapture("where nvidia-smi");
char buf[256]; s_has_nvidia = (w.find_first_not_of(" \t\r\n") != std::string::npos);
s_has_nvidia = (fgets(buf, sizeof(buf), f) != nullptr);
_pclose(f);
}
} }
if (s_has_nvidia) { if (s_has_nvidia) {
FILE* f = _popen("nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits 2>nul", "r"); const std::string o = runHiddenCapture(
if (f) { "nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits");
char buf[64]; if (!o.empty()) {
int util = -1; int util = atoi(o.c_str());
if (fgets(buf, sizeof(buf), f)) { if (util < 0 || util > 100) util = -1;
util = atoi(buf);
if (util < 0 || util > 100) util = -1;
}
_pclose(f);
return util; return util;
} }
} }

View File

@@ -179,6 +179,15 @@ public:
* @return GPU busy percent, or -1 if unavailable. * @return GPU busy percent, or -1 if unavailable.
*/ */
static int getGpuUtilization(); static int getGpuUtilization();
// Run a command line and capture its stdout WITHOUT ever popping a console window: Windows uses
// CreateProcess + CREATE_NO_WINDOW (a plain popen()/_popen() flashes a cmd.exe console), POSIX uses
// popen(). Use this instead of _popen for anything run while the GUI is up. `mergeStderr` folds the
// child's stderr into the result (like "2>&1"); otherwise stderr is discarded. `exitCode`, if given,
// receives the child's exit status (raw pclose() status on POSIX, GetExitCodeProcess on Windows; -1
// if the process could not be launched).
static std::string runHiddenCapture(const std::string& cmdLine, bool mergeStderr = false,
int* exitCode = nullptr);
}; };
/** /**