The Notices block takes the hot-plug notices as on gpu-hotplug (card added, not usable, removed). The View block and the Mine rows, the first-run rows and the Settings cards show a removed card (dimmed, no switch, the row goes after five minutes) and a faulty card (named in ember, the OS problem code, the reboot hint, no switch); the big button and the counts take only present cards; the name tooltip carries the tool's code, the device, the platform and the PCI address. view.test.mjs covers the two states. host.cpp keeps both the WM_GETMINMAXINFO and the WM_DEVICECHANGE cases. Build tooling: push-build-inputs.sh and build-job.mjs take --no-node (the app engine only, no node source, no node build, no node tests), for an app-only PC compile. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
467 lines
20 KiB
C++
467 lines
20 KiB
C++
// Igneum Miner for Windows: the window around the engine. One C++ file, built on the PC by BUILD-APP.bat
|
|
// (MSVC, the WebView2 SDK from NuGet, static loader). It starts igneum-app.exe --wrapper next to it, reads
|
|
// "URL ..." / "STATE {...}" / "EXIT" from its stdout, shows the URL in a WebView2 control, keeps a tray icon with the
|
|
// state, and on quit writes "quit" to the engine's stdin and waits for EXIT (miners stop first, then the node).
|
|
// Closing the window hides it to the tray; the miner keeps running. If the WebView2 runtime is missing the dashboard
|
|
// opens in the default browser instead and the window says so. 4 October 2026.
|
|
//
|
|
// Untested on a real PC at the time of writing (written on a Mac): BUILD-APP.bat is the first run.
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#ifndef UNICODE
|
|
#define UNICODE
|
|
#endif
|
|
#ifndef _UNICODE
|
|
#define _UNICODE
|
|
#endif
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#include <dbt.h>
|
|
#include <wrl.h>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include "WebView2.h"
|
|
#include "version.h"
|
|
|
|
using namespace Microsoft::WRL;
|
|
|
|
#define WM_ENGINE_LINE (WM_APP + 1)
|
|
#define WM_TRAY (WM_APP + 2)
|
|
#define ID_TRAY_OPEN 1001
|
|
#define ID_TRAY_PAUSE 1002
|
|
#define ID_TRAY_QUIT 1003
|
|
#define ID_QUIT_TIMER 7
|
|
#define IDI_APP 1
|
|
|
|
static HWND g_hwnd = nullptr;
|
|
static HANDLE g_engine = nullptr, g_engineIn = nullptr, g_engineOut = nullptr;
|
|
static ComPtr<ICoreWebView2Controller> g_controller;
|
|
static ComPtr<ICoreWebView2> g_webview;
|
|
static std::wstring g_url, g_status = L"starting the engine";
|
|
static bool g_paused = false, g_quitting = false, g_exited = false, g_webviewOk = false, g_hintShown = false;
|
|
static NOTIFYICONDATAW g_nid = {};
|
|
static std::wstring g_trayTitle = L"Igneum Miner";
|
|
static ULONGLONG g_quitStarted = 0;
|
|
|
|
static std::wstring widen(const std::string& s) {
|
|
if (s.empty()) return L"";
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
|
|
std::wstring w(n, 0);
|
|
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
|
|
return w;
|
|
}
|
|
|
|
static std::wstring exeDir() {
|
|
wchar_t buf[MAX_PATH];
|
|
GetModuleFileNameW(nullptr, buf, MAX_PATH);
|
|
std::wstring p(buf);
|
|
size_t i = p.find_last_of(L"\\/");
|
|
return i == std::wstring::npos ? L"." : p.substr(0, i);
|
|
}
|
|
|
|
static void sendEngine(const char* line) {
|
|
if (!g_engineIn) return;
|
|
DWORD w = 0;
|
|
WriteFile(g_engineIn, line, (DWORD)strlen(line), &w, nullptr);
|
|
WriteFile(g_engineIn, "\n", 1, &w, nullptr);
|
|
}
|
|
|
|
static void setTray(const std::wstring& tip) {
|
|
g_trayTitle = tip;
|
|
wcsncpy_s(g_nid.szTip, tip.c_str(), _TRUNCATE);
|
|
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
|
|
}
|
|
|
|
static void repaintStatus() {
|
|
InvalidateRect(g_hwnd, nullptr, TRUE);
|
|
}
|
|
|
|
// A tiny JSON number/string/bool reader for the STATE line (flat object, known keys).
|
|
static std::string jsonField(const std::string& j, const char* key) {
|
|
std::string k = std::string("\"") + key + "\":";
|
|
size_t i = j.find(k);
|
|
if (i == std::string::npos) return "";
|
|
i += k.size();
|
|
while (i < j.size() && j[i] == ' ') i++;
|
|
if (i < j.size() && j[i] == '"') {
|
|
size_t e = j.find('"', i + 1);
|
|
return e == std::string::npos ? "" : j.substr(i + 1, e - i - 1);
|
|
}
|
|
size_t e = i;
|
|
while (e < j.size() && j[e] != ',' && j[e] != '}') e++;
|
|
return j.substr(i, e - i);
|
|
}
|
|
|
|
static void applyState(const std::string& j) {
|
|
std::string mining = jsonField(j, "mining"), node = jsonField(j, "node"), phase = jsonField(j, "phase");
|
|
g_paused = jsonField(j, "paused") == "true";
|
|
double hash = atof(jsonField(j, "hash_total").c_str());
|
|
std::string blocks = jsonField(j, "blocks"), accepted = jsonField(j, "accepted_total");
|
|
wchar_t tip[128];
|
|
if (jsonField(j, "quitting") == "true") swprintf_s(tip, L"Igneum Miner: stopping");
|
|
else if (phase != "dashboard") swprintf_s(tip, L"Igneum Miner: set up");
|
|
else if (mining == "mining") swprintf_s(tip, L"Igneum Miner: %.1f MH/s, %S blocks found, node %S", hash, accepted.c_str(), node.c_str());
|
|
else if (mining == "paused") swprintf_s(tip, L"Igneum Miner: paused, node %S", node.c_str());
|
|
else swprintf_s(tip, L"Igneum Miner: %S, node %S (%S blocks)", mining.c_str(), node.c_str(), blocks.c_str());
|
|
setTray(tip);
|
|
}
|
|
|
|
// The engine asks for an elevated step (the NVIDIA power cap): this process has a UI context, so the UAC prompt shows.
|
|
// Runs cmd /c <line> as administrator, waits, and answers on the engine's stdin.
|
|
static void runElevated(std::wstring line) {
|
|
std::thread([line] {
|
|
std::wstring params = L"/c " + line;
|
|
wchar_t sysdir[MAX_PATH];
|
|
GetSystemDirectoryW(sysdir, MAX_PATH);
|
|
std::wstring cmdExe = std::wstring(sysdir) + L"\\cmd.exe"; // the absolute path, never a bare name (R4.3.3)
|
|
SHELLEXECUTEINFOW sei = { sizeof(sei) };
|
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
|
sei.lpVerb = L"runas";
|
|
sei.lpFile = cmdExe.c_str();
|
|
sei.lpParameters = params.c_str();
|
|
sei.nShow = SW_HIDE;
|
|
if (!ShellExecuteExW(&sei) || !sei.hProcess) {
|
|
DWORD err = GetLastError();
|
|
sendEngine(err == ERROR_CANCELLED ? "elevated fail: the administrator prompt was cancelled" : "elevated fail: could not start the elevated step");
|
|
return;
|
|
}
|
|
WaitForSingleObject(sei.hProcess, 120000);
|
|
DWORD code = 1;
|
|
GetExitCodeProcess(sei.hProcess, &code);
|
|
CloseHandle(sei.hProcess);
|
|
if (code == 0) sendEngine("elevated ok");
|
|
else { char buf[64]; sprintf_s(buf, "elevated fail: exit code %lu", code); sendEngine(buf); }
|
|
}).detach();
|
|
}
|
|
|
|
static void openInBrowser(const std::wstring& url) {
|
|
ShellExecuteW(nullptr, L"open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
|
}
|
|
|
|
static void navigate() {
|
|
if (g_webview && !g_url.empty()) g_webview->Navigate((g_url + L"?host=windows").c_str());
|
|
}
|
|
|
|
static void initWebView() {
|
|
std::wstring data = L"";
|
|
wchar_t* local = nullptr;
|
|
size_t len = 0;
|
|
if (_wdupenv_s(&local, &len, L"LOCALAPPDATA") == 0 && local) { data = std::wstring(local) + L"\\igneum\\webview2"; free(local); }
|
|
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, data.empty() ? nullptr : data.c_str(), nullptr,
|
|
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>([](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
|
|
if (FAILED(result) || !env) {
|
|
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
|
|
repaintStatus();
|
|
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
|
return S_OK;
|
|
}
|
|
env->CreateCoreWebView2Controller(g_hwnd, Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>([](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
|
|
if (FAILED(result) || !controller) {
|
|
g_status = L"The app window could not start WebView2. The dashboard opens in your browser.";
|
|
repaintStatus();
|
|
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
|
return S_OK;
|
|
}
|
|
g_controller = controller;
|
|
g_controller->get_CoreWebView2(&g_webview);
|
|
g_webviewOk = true;
|
|
ComPtr<ICoreWebView2Settings> settings;
|
|
if (g_webview && SUCCEEDED(g_webview->get_Settings(&settings)) && settings) {
|
|
settings->put_AreDefaultContextMenusEnabled(FALSE);
|
|
settings->put_IsStatusBarEnabled(FALSE);
|
|
settings->put_AreDevToolsEnabled(FALSE);
|
|
}
|
|
// links off 127.0.0.1 open in the default browser
|
|
g_webview->add_NewWindowRequested(Callback<ICoreWebView2NewWindowRequestedEventHandler>([](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT {
|
|
LPWSTR uri = nullptr;
|
|
if (SUCCEEDED(args->get_Uri(&uri)) && uri) { openInBrowser(uri); CoTaskMemFree(uri); }
|
|
args->put_Handled(TRUE);
|
|
return S_OK;
|
|
}).Get(), nullptr);
|
|
g_webview->add_NavigationStarting(Callback<ICoreWebView2NavigationStartingEventHandler>([](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
|
|
LPWSTR uri = nullptr;
|
|
if (SUCCEEDED(args->get_Uri(&uri)) && uri) {
|
|
std::wstring u(uri);
|
|
CoTaskMemFree(uri);
|
|
if (u.rfind(L"http", 0) == 0 && u.find(L"127.0.0.1") == std::wstring::npos) { args->put_Cancel(TRUE); openInBrowser(u); }
|
|
}
|
|
return S_OK;
|
|
}).Get(), nullptr);
|
|
RECT rc; GetClientRect(g_hwnd, &rc);
|
|
g_controller->put_Bounds(rc);
|
|
COREWEBVIEW2_COLOR dark = { 255, 12, 12, 14 };
|
|
ComPtr<ICoreWebView2Controller2> c2;
|
|
if (SUCCEEDED(g_controller.As(&c2)) && c2) c2->put_DefaultBackgroundColor(dark);
|
|
g_status = L"";
|
|
repaintStatus();
|
|
navigate();
|
|
return S_OK;
|
|
}).Get());
|
|
return S_OK;
|
|
}).Get());
|
|
if (FAILED(hr)) {
|
|
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
|
|
repaintStatus();
|
|
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
|
}
|
|
}
|
|
|
|
static bool startEngine() {
|
|
SECURITY_ATTRIBUTES sa = { sizeof(sa), nullptr, TRUE };
|
|
HANDLE outR, outW, inR, inW;
|
|
if (!CreatePipe(&outR, &outW, &sa, 0) || !CreatePipe(&inR, &inW, &sa, 0)) return false;
|
|
SetHandleInformation(outR, HANDLE_FLAG_INHERIT, 0);
|
|
SetHandleInformation(inW, HANDLE_FLAG_INHERIT, 0);
|
|
STARTUPINFOW si = { sizeof(si) };
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = outW;
|
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
si.hStdInput = inR;
|
|
PROCESS_INFORMATION pi = {};
|
|
std::wstring exe = exeDir() + L"\\igneum-app.exe";
|
|
std::wstring cmd = L"\"" + exe + L"\" --wrapper";
|
|
std::vector<wchar_t> buf(cmd.begin(), cmd.end());
|
|
buf.push_back(0);
|
|
BOOL ok = CreateProcessW(exe.c_str(), buf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, exeDir().c_str(), &si, &pi);
|
|
CloseHandle(outW);
|
|
CloseHandle(inR);
|
|
if (!ok) { CloseHandle(outR); CloseHandle(inW); return false; }
|
|
CloseHandle(pi.hThread);
|
|
g_engine = pi.hProcess;
|
|
g_engineIn = inW;
|
|
g_engineOut = outR;
|
|
std::thread([] {
|
|
std::string acc;
|
|
char chunk[4096];
|
|
DWORD n;
|
|
while (ReadFile(g_engineOut, chunk, sizeof(chunk), &n, nullptr) && n > 0) {
|
|
acc.append(chunk, n);
|
|
size_t nl;
|
|
while ((nl = acc.find('\n')) != std::string::npos) {
|
|
std::string line = acc.substr(0, nl);
|
|
acc.erase(0, nl + 1);
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
PostMessageW(g_hwnd, WM_ENGINE_LINE, 0, (LPARAM) new std::string(line));
|
|
}
|
|
}
|
|
PostMessageW(g_hwnd, WM_ENGINE_LINE, 1, 0);
|
|
}).detach();
|
|
return true;
|
|
}
|
|
|
|
static void showTrayMenu() {
|
|
HMENU m = CreatePopupMenu();
|
|
AppendMenuW(m, MF_STRING, ID_TRAY_OPEN, L"Open Igneum Miner");
|
|
AppendMenuW(m, MF_STRING, ID_TRAY_PAUSE, g_paused ? L"Resume mining" : L"Pause mining");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
|
|
AppendMenuW(m, MF_STRING | MF_GRAYED, 0, g_trayTitle.c_str());
|
|
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
|
|
AppendMenuW(m, MF_STRING, ID_TRAY_QUIT, L"Quit Igneum Miner");
|
|
POINT pt; GetCursorPos(&pt);
|
|
SetForegroundWindow(g_hwnd);
|
|
TrackPopupMenu(m, TPM_RIGHTBUTTON | TPM_BOTTOMALIGN, pt.x, pt.y, 0, g_hwnd, nullptr);
|
|
DestroyMenu(m);
|
|
}
|
|
|
|
static void beginQuit() {
|
|
if (g_quitting) return;
|
|
g_quitting = true;
|
|
g_quitStarted = GetTickCount64();
|
|
g_status = L"Stopping: the miner first, then the node";
|
|
setTray(L"Igneum Miner: stopping");
|
|
if (g_webview) g_webview->ExecuteScript(L"document.getElementById('pill-text').textContent='stopping'", nullptr);
|
|
if (g_engine && !g_exited) {
|
|
sendEngine("quit");
|
|
SetTimer(g_hwnd, ID_QUIT_TIMER, 500, nullptr);
|
|
} else {
|
|
DestroyWindow(g_hwnd);
|
|
}
|
|
}
|
|
|
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
switch (msg) {
|
|
case WM_SIZE:
|
|
if (g_controller) { RECT rc; GetClientRect(hwnd, &rc); g_controller->put_Bounds(rc); }
|
|
return 0;
|
|
case WM_GETMINMAXINFO: // the dashboard lays out from 900 x 600 up (app/igneum-app/ui); the Mac window says the same
|
|
((MINMAXINFO*)lp)->ptMinTrackSize.x = 900; ((MINMAXINFO*)lp)->ptMinTrackSize.y = 600;
|
|
return 0;
|
|
case WM_ENGINE_LINE: {
|
|
if (wp == 1) {
|
|
g_exited = true;
|
|
if (g_quitting) { DestroyWindow(hwnd); return 0; }
|
|
g_status = L"The engine stopped. Close this window and open Igneum Miner again.";
|
|
setTray(L"Igneum Miner: stopped");
|
|
repaintStatus();
|
|
return 0;
|
|
}
|
|
std::string* line = (std::string*)lp;
|
|
if (line->rfind("URL ", 0) == 0) {
|
|
g_url = widen(line->substr(4));
|
|
if (g_webviewOk) navigate();
|
|
else if (!g_webview && g_status.find(L"WebView2") != std::wstring::npos && !g_hintShown) { openInBrowser(g_url); g_hintShown = true; }
|
|
} else if (line->rfind("STATE ", 0) == 0) {
|
|
applyState(line->substr(6));
|
|
} else if (line->rfind("ELEVATE ", 0) == 0) {
|
|
runElevated(widen(line->substr(8)));
|
|
} else if (line->rfind("FATAL ", 0) == 0) {
|
|
g_status = widen(line->substr(6));
|
|
repaintStatus();
|
|
MessageBoxW(hwnd, g_status.c_str(), L"Igneum Miner", MB_OK | MB_ICONERROR);
|
|
} else if (*line == "EXIT") {
|
|
g_exited = true;
|
|
if (g_quitting) DestroyWindow(hwnd);
|
|
}
|
|
delete line;
|
|
return 0;
|
|
}
|
|
case WM_TIMER:
|
|
if (wp == ID_QUIT_TIMER) {
|
|
DWORD code = 0;
|
|
bool gone = !g_engine || (GetExitCodeProcess(g_engine, &code) && code != STILL_ACTIVE);
|
|
if (gone || g_exited || GetTickCount64() - g_quitStarted > 45000) {
|
|
if (!gone && g_engine) TerminateProcess(g_engine, 1);
|
|
KillTimer(hwnd, ID_QUIT_TIMER);
|
|
DestroyWindow(hwnd);
|
|
}
|
|
}
|
|
return 0;
|
|
case WM_DEVICECHANGE:
|
|
// a device arrived or left (an eGPU through a USB4 box, a driver coming up or crashing): the engine enumerates
|
|
// the cards now instead of at its next minute poll (src/hotplug.rs); DBT_DEVNODES_CHANGED needs no registration
|
|
if (wp == DBT_DEVNODES_CHANGED || wp == DBT_DEVICEARRIVAL || wp == DBT_DEVICEREMOVECOMPLETE) sendEngine("detect");
|
|
return TRUE;
|
|
case WM_TRAY:
|
|
if (lp == WM_LBUTTONUP || lp == WM_LBUTTONDBLCLK) { ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); }
|
|
else if (lp == WM_RBUTTONUP || lp == WM_CONTEXTMENU) showTrayMenu();
|
|
return 0;
|
|
case WM_COMMAND:
|
|
switch (LOWORD(wp)) {
|
|
case ID_TRAY_OPEN: ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); return 0;
|
|
case ID_TRAY_PAUSE: sendEngine(g_paused ? "resume" : "pause"); return 0;
|
|
case ID_TRAY_QUIT: beginQuit(); return 0;
|
|
}
|
|
return 0;
|
|
case WM_CLOSE:
|
|
// hide to the tray; the miner keeps running
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
if (!g_hintShown) {
|
|
g_nid.uFlags |= NIF_INFO;
|
|
wcscpy_s(g_nid.szInfoTitle, L"Igneum Miner keeps mining");
|
|
wcscpy_s(g_nid.szInfo, L"The window is in the tray. Right-click the icon to pause or quit.");
|
|
g_nid.dwInfoFlags = NIIF_INFO;
|
|
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
|
|
g_nid.uFlags &= ~NIF_INFO;
|
|
g_hintShown = true;
|
|
}
|
|
return 0;
|
|
case WM_PAINT: {
|
|
PAINTSTRUCT ps;
|
|
HDC dc = BeginPaint(hwnd, &ps);
|
|
RECT rc; GetClientRect(hwnd, &rc);
|
|
HBRUSH bg = CreateSolidBrush(RGB(12, 12, 14));
|
|
FillRect(dc, &rc, bg);
|
|
DeleteObject(bg);
|
|
if (!g_status.empty()) {
|
|
SetBkMode(dc, TRANSPARENT);
|
|
SetTextColor(dc, RGB(154, 154, 158));
|
|
HFONT f = CreateFontW(-15, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
|
HFONT old = (HFONT)SelectObject(dc, f);
|
|
RECT tr = rc; tr.left += 40; tr.right -= 40;
|
|
DrawTextW(dc, g_status.c_str(), -1, &tr, DT_CENTER | DT_VCENTER | DT_WORDBREAK | DT_NOPREFIX | (g_webviewOk ? DT_BOTTOM : DT_VCENTER));
|
|
SelectObject(dc, old);
|
|
DeleteObject(f);
|
|
}
|
|
EndPaint(hwnd, &ps);
|
|
return 0;
|
|
}
|
|
case WM_DESTROY:
|
|
Shell_NotifyIconW(NIM_DELETE, &g_nid);
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
}
|
|
|
|
// --version and --help print one line and exit, for the CI smoke run and support scripts. A windows-subsystem exe has
|
|
// no console of its own: the text goes to the inherited stdout when there is one (a pipe or a file), else to the
|
|
// parent's console.
|
|
static bool handleCliFlags() {
|
|
int argc = 0;
|
|
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
|
if (!argv) return false;
|
|
std::wstring text;
|
|
for (int i = 1; i < argc; i++) {
|
|
std::wstring a = argv[i];
|
|
if (a == L"--version" || a == L"-V") text = L"Igneum Miner " IGNEUM_HOST_VERSION_STR L" (window host)\r\n";
|
|
else if (a == L"--help" || a == L"-h" || a == L"/?")
|
|
text = L"Igneum Miner " IGNEUM_HOST_VERSION_STR L" (window host)\r\n"
|
|
L"Usage: \"Igneum Miner.exe\" [--version | --help]\r\n"
|
|
L"Without flags it starts igneum-app.exe --wrapper next to it and shows the dashboard in a WebView2 window.\r\n";
|
|
}
|
|
LocalFree(argv);
|
|
if (text.empty()) return false;
|
|
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (out == nullptr || out == INVALID_HANDLE_VALUE) {
|
|
if (AttachConsole(ATTACH_PARENT_PROCESS)) out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
}
|
|
if (out && out != INVALID_HANDLE_VALUE) {
|
|
int n = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0, nullptr, nullptr);
|
|
std::string utf8(n, 0);
|
|
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), &utf8[0], n, nullptr, nullptr);
|
|
DWORD written = 0;
|
|
WriteFile(out, utf8.data(), (DWORD)utf8.size(), &written, nullptr);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) {
|
|
if (handleCliFlags()) return 0;
|
|
HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumMinerWindow");
|
|
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
|
HWND other = FindWindowW(L"IgneumMinerWindow", nullptr);
|
|
if (other) { ShowWindow(other, SW_SHOW); SetForegroundWindow(other); }
|
|
return 0;
|
|
}
|
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
WNDCLASSW wc = {};
|
|
wc.lpfnWndProc = WndProc;
|
|
wc.hInstance = hInst;
|
|
wc.lpszClassName = L"IgneumMinerWindow";
|
|
wc.hIcon = LoadIconW(hInst, MAKEINTRESOURCEW(IDI_APP));
|
|
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
wc.hbrBackground = CreateSolidBrush(RGB(12, 12, 14));
|
|
RegisterClassW(&wc);
|
|
int w = 1120, h = 820;
|
|
int sx = (GetSystemMetrics(SM_CXSCREEN) - w) / 2, sy = (GetSystemMetrics(SM_CYSCREEN) - h) / 2;
|
|
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"Igneum Miner", WS_OVERLAPPEDWINDOW, sx, sy, w, h, nullptr, nullptr, hInst, nullptr);
|
|
ShowWindow(g_hwnd, SW_SHOW);
|
|
|
|
g_nid.cbSize = sizeof(g_nid);
|
|
g_nid.hWnd = g_hwnd;
|
|
g_nid.uID = 1;
|
|
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
|
g_nid.uCallbackMessage = WM_TRAY;
|
|
g_nid.hIcon = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(IDI_APP), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
|
|
wcscpy_s(g_nid.szTip, L"Igneum Miner: starting");
|
|
Shell_NotifyIconW(NIM_ADD, &g_nid);
|
|
|
|
if (!startEngine()) {
|
|
g_status = L"igneum-app.exe is missing next to this program. Run the installer again.";
|
|
repaintStatus();
|
|
MessageBoxW(g_hwnd, g_status.c_str(), L"Igneum Miner", MB_OK | MB_ICONERROR);
|
|
}
|
|
initWebView();
|
|
|
|
MSG msg;
|
|
while (GetMessageW(&msg, nullptr, 0, 0)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
if (g_engine) { CloseHandle(g_engine); }
|
|
CloseHandle(once);
|
|
CoUninitialize();
|
|
return 0;
|
|
}
|