From c4702b36528a8690b9152d03e11c06d132fe366b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=B6glinger-Stelzer?= Date: Sat, 21 Mar 2026 20:21:20 +0100 Subject: [PATCH 1/3] Added self-update check mechanism --- CMakeLists.txt | 2 + src/main.cpp | 80 ++++++++++- src/update_check.cpp | 308 +++++++++++++++++++++++++++++++++++++++++++ src/update_check.h | 27 ++++ 4 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 src/update_check.cpp create mode 100644 src/update_check.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a0e5759..20984a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,8 @@ target_link_libraries(MultiPadTester PRIVATE dxguid.lib setupapi.lib runtimeobject.lib + winhttp.lib + version.lib ) target_compile_definitions(MultiPadTester PRIVATE diff --git a/src/main.cpp b/src/main.cpp index e949ce6..a8860c7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -30,6 +33,7 @@ #include "hidhide_probe.h" #include "libwdi_probe.h" #include "resource.h" +#include "update_check.h" #define IDM_ABOUT 0xF200 #define IDM_PREFERENCES 0xF210 @@ -43,6 +47,8 @@ struct AppPrefs int windowW = 0; // 0 = use default position/size int windowH = 0; int lastTabIndex = 0; // backend tab index to restore on launch + /** UTC Unix seconds when the user dismissed the update dialog; 0 = never. Suppresses checks for 24h. */ + int64_t updateDismissedUnix = 0; }; /** @@ -69,7 +75,7 @@ static std::wstring GetConfigPath() * * Reads the config file located in the application's AppData folder and applies recognized keys * from the [Settings] section into the provided AppPrefs structure. Supported keys: - * RefreshRate, VSync, WindowX, WindowY, WindowW, WindowH, LastTabIndex. + * RefreshRate, VSync, WindowX, WindowY, WindowW, WindowH, LastTabIndex, UpdateDismissedUnix. * * - If the config file is missing or unreadable, the function leaves prefs unchanged. * - RefreshRate is accepted only if it equals 0, 60, 75, 120, or 144; other values are ignored. @@ -143,6 +149,10 @@ static void LoadConfig(AppPrefs& prefs) { try { prefs.lastTabIndex = std::stoi(val); } catch (...) {} } + else if (key == "UpdateDismissedUnix") + { + try { prefs.updateDismissedUnix = std::stoll(val); } catch (...) {} + } } // Treat invalid dimensions as "not set" if (prefs.windowW <= 0 || prefs.windowH <= 0) @@ -160,6 +170,7 @@ static void LoadConfig(AppPrefs& prefs) * - vsync: vertical sync enabled flag * - windowX, windowY, windowW, windowH: saved window position and size * - lastTabIndex: backend tab index to restore on launch + * - updateDismissedUnix: UTC Unix time when update dialog was dismissed */ static void SaveConfig(const AppPrefs& prefs) { @@ -177,6 +188,7 @@ static void SaveConfig(const AppPrefs& prefs) f << "WindowW=" << prefs.windowW << "\n"; f << "WindowH=" << prefs.windowH << "\n"; f << "LastTabIndex=" << prefs.lastTabIndex << "\n"; + f << "UpdateDismissedUnix=" << prefs.updateDismissedUnix << "\n"; } /** @@ -231,6 +243,11 @@ static std::vector g_libwdiUsbInstanceIdsUtf8; static std::string g_libwdiUsbProbeErrorUtf8; static AppPrefs g_prefs; +static std::atomic g_updateCheckHwndSlot{nullptr}; +static bool g_showUpdateAvailable = false; +static std::string g_updateLocalVerUtf8; +static std::string g_updateRemoteVerUtf8; + static std::string WideToUtf8(const std::wstring_view w) { if (w.empty()) @@ -275,6 +292,18 @@ static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) switch (msg) { + case WM_UPDATE_CHECK_READY: + { + std::string loc; + std::string rem; + if (UpdateCheck_PopResultForUi(loc, rem)) + { + g_updateLocalVerUtf8 = std::move(loc); + g_updateRemoteVerUtf8 = std::move(rem); + g_showUpdateAvailable = true; + } + } + return 0; case WM_SIZE: if (g_d3d.device && wParam != SIZE_MINIMIZED) g_d3d.Resize(lParam); @@ -294,6 +323,7 @@ static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) return 0; break; case WM_DESTROY: + g_updateCheckHwndSlot.store(nullptr, std::memory_order_release); { RECT r; if (GetWindowRect(hWnd, &r)) @@ -458,6 +488,9 @@ int APIENTRY wWinMain( ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); + g_updateCheckHwndSlot.store(hwnd, std::memory_order_release); + StartBackgroundUpdateCheck(hwnd, &g_updateCheckHwndSlot, g_prefs.updateDismissedUnix); + constexpr float clearColor[4] = {0.06f, 0.06f, 0.07f, 1.0f}; MSG msg{}; @@ -694,6 +727,51 @@ int APIENTRY wWinMain( ImGui::EndPopup(); } + const char* const kUpdateAvailablePopupId = "Update available"; + if (g_showUpdateAvailable) + ImGui::OpenPopup(kUpdateAvailablePopupId); + + const bool updatePopupActive = + g_showUpdateAvailable || ImGui::IsPopupOpen(kUpdateAvailablePopupId, ImGuiPopupFlags_None); + if (updatePopupActive) + { + const float updateMinW = 400.f, updateMinH = 140.f; + ImGui::SetNextWindowSizeConstraints(ImVec2(updateMinW, updateMinH), + ImVec2(FLT_MAX, FLT_MAX)); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + } + if (ImGui::BeginPopupModal( + kUpdateAvailablePopupId, + &g_showUpdateAvailable, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped("A newer version of MultiPad Tester is available."); + ImGui::Spacing(); + ImGui::Text("Installed version: %s", g_updateLocalVerUtf8.c_str()); + ImGui::Text("Latest version: %s", g_updateRemoteVerUtf8.c_str()); + ImGui::Spacing(); + if (ImGui::Button("Download update", ImVec2(140, 0))) + { + ShellExecuteW( + nullptr, + L"open", + L"https://buildbot.nefarius.at/builds/MultiPadTester/latest/MultiPadTester.zip", + nullptr, + nullptr, + SW_SHOWNORMAL); + } + ImGui::SameLine(); + if (ImGui::Button("Not today", ImVec2(130, 0))) + { + g_prefs.updateDismissedUnix = static_cast(std::time(nullptr)); + SaveConfig(g_prefs); + ImGui::CloseCurrentPopup(); + g_showUpdateAvailable = false; + } + ImGui::EndPopup(); + } + const char* const kHidHideActivePopupId = "HidHide Active Warning"; if (g_showHidHideWarning) ImGui::OpenPopup(kHidHideActivePopupId); diff --git a/src/update_check.cpp b/src/update_check.cpp new file mode 100644 index 0000000..c911cb8 --- /dev/null +++ b/src/update_check.cpp @@ -0,0 +1,308 @@ +#include "update_check.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr wchar_t kJsonHost[] = L"buildbot.nefarius.at"; +constexpr wchar_t kJsonPath[] = + L"/builds/MultiPadTester/latest/.MultiPadTester/.MultiPadTester.exe.json"; +constexpr DWORD kResolveMs = 10'000; +constexpr DWORD kConnectMs = 10'000; +constexpr DWORD kSendMs = 30'000; +constexpr DWORD kReceiveMs = 30'000; + +constexpr int64_t kSuppressSeconds = 24 * 60 * 60; + +struct VersionQuad +{ + uint16_t a = 0, b = 0, c = 0, d = 0; +}; + +std::mutex g_resultMutex; +std::string g_pendingLocal; +std::string g_pendingRemote; +bool g_havePending = false; + +bool ParseVersionString(std::string_view s, VersionQuad& out) +{ + out = {}; + int part = 0; + uint32_t cur = 0; + for (char ch : s) + { + if (ch == '.') + { + if (part >= 4) + return false; + if (part == 0) + out.a = static_cast(cur); + else if (part == 1) + out.b = static_cast(cur); + else if (part == 2) + out.c = static_cast(cur); + ++part; + cur = 0; + continue; + } + if (ch < '0' || ch > '9') + return false; + cur = cur * 10u + static_cast(ch - '0'); + if (cur > 65535u) + return false; + } + if (part == 0) + out.a = static_cast(cur); + else if (part == 1) + out.b = static_cast(cur); + else if (part == 2) + out.c = static_cast(cur); + else if (part == 3) + out.d = static_cast(cur); + else + return false; + return true; +} + +int CompareVersionQuad(const VersionQuad& l, const VersionQuad& r) +{ + if (l.a != r.a) + return (l.a < r.a) ? -1 : 1; + if (l.b != r.b) + return (l.b < r.b) ? -1 : 1; + if (l.c != r.c) + return (l.c < r.c) ? -1 : 1; + if (l.d != r.d) + return (l.d < r.d) ? -1 : 1; + return 0; +} + +bool GetLocalExeVersion(VersionQuad& quad, std::string& displayUtf8) +{ + wchar_t path[MAX_PATH]{}; + if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) + return false; + DWORD handle = 0; + const DWORD verSize = GetFileVersionInfoSizeW(path, &handle); + if (verSize == 0) + return false; + std::vector buf(verSize); + if (!GetFileVersionInfoW(path, 0, verSize, buf.data())) + return false; + VS_FIXEDFILEINFO* ffi = nullptr; + UINT ffiLen = 0; + if (!VerQueryValueW(buf.data(), L"\\", reinterpret_cast(&ffi), &ffiLen) || !ffi || + ffiLen < sizeof(VS_FIXEDFILEINFO)) + return false; + const uint32_t ms = ffi->dwFileVersionMS; + const uint32_t ls = ffi->dwFileVersionLS; + quad.a = HIWORD(ms); + quad.b = LOWORD(ms); + quad.c = HIWORD(ls); + quad.d = LOWORD(ls); + displayUtf8 = std::format("{}.{}.{}.{}", quad.a, quad.b, quad.c, quad.d); + return true; +} + +bool ExtractJsonFileVersion(std::string_view json, std::string& outVer) +{ + const std::string_view key = "\"FileVersion\""; + const size_t pos = json.find(key); + if (pos == std::string_view::npos) + return false; + size_t i = pos + key.size(); + while (i < json.size() && (json[i] == ' ' || json[i] == '\t' || json[i] == '\r' || json[i] == '\n')) + ++i; + if (i >= json.size() || json[i] != ':') + return false; + ++i; + while (i < json.size() && (json[i] == ' ' || json[i] == '\t' || json[i] == '\r' || json[i] == '\n')) + ++i; + if (i >= json.size() || json[i] != '"') + return false; + ++i; + const size_t start = i; + while (i < json.size() && json[i] != '"') + ++i; + if (i <= start) + return false; + outVer.assign(json.data() + start, i - start); + return !outVer.empty(); +} + +bool HttpGetUtf8(const wchar_t* host, INTERNET_PORT port, const wchar_t* path, std::string& bodyOut) +{ + bodyOut.clear(); + HINTERNET hSession = WinHttpOpen( + L"MultiPadTester/1.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + if (!hSession) + return false; + auto closeSession = [hSession]() { WinHttpCloseHandle(hSession); }; + (void)WinHttpSetTimeouts(hSession, static_cast(kResolveMs), static_cast(kConnectMs), + static_cast(kSendMs), static_cast(kReceiveMs)); + + HINTERNET hConnect = WinHttpConnect(hSession, host, port, 0); + if (!hConnect) + { + closeSession(); + return false; + } + HINTERNET hRequest = WinHttpOpenRequest( + hConnect, + L"GET", + path, + nullptr, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_SECURE); + if (!hRequest) + { + WinHttpCloseHandle(hConnect); + closeSession(); + return false; + } + + const BOOL sent = WinHttpSendRequest( + hRequest, + WINHTTP_NO_ADDITIONAL_HEADERS, + 0, + WINHTTP_NO_REQUEST_DATA, + 0, + 0, + 0); + if (!sent) + { + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + closeSession(); + return false; + } + if (!WinHttpReceiveResponse(hRequest, nullptr)) + { + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + closeSession(); + return false; + } + + DWORD status = 0; + DWORD statusSize = sizeof(status); + if (!WinHttpQueryHeaders( + hRequest, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &status, + &statusSize, + WINHTTP_NO_HEADER_INDEX) || + status != 200) + { + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + closeSession(); + return false; + } + + for (;;) + { + char buf[8192]; + DWORD read = 0; + if (!WinHttpReadData(hRequest, buf, sizeof(buf), &read)) + break; + if (read == 0) + break; + bodyOut.append(buf, read); + } + + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + closeSession(); + return !bodyOut.empty(); +} + +void RunUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismissedUnix) +{ + const std::time_t now = std::time(nullptr); + if (dismissedUnix > 0) + { + const int64_t elapsed = static_cast(now) - dismissedUnix; + if (elapsed >= 0 && elapsed < kSuppressSeconds) + return; + } + + std::string json; + if (!HttpGetUtf8(kJsonHost, INTERNET_DEFAULT_HTTPS_PORT, kJsonPath, json)) + return; + + std::string remoteVerStr; + if (!ExtractJsonFileVersion(json, remoteVerStr)) + return; + + VersionQuad remote{}; + if (!ParseVersionString(remoteVerStr, remote)) + return; + + VersionQuad local{}; + std::string localDisplay; + if (!GetLocalExeVersion(local, localDisplay)) + return; + + if (CompareVersionQuad(remote, local) <= 0) + return; + + const HWND slotHwnd = hwndSlot ? hwndSlot->load(std::memory_order_acquire) : notifyHwnd; + if (slotHwnd != notifyHwnd || !IsWindow(notifyHwnd)) + return; + + { + std::lock_guard lock(g_resultMutex); + g_pendingLocal = std::move(localDisplay); + g_pendingRemote = std::move(remoteVerStr); + g_havePending = true; + } + + const HWND slot2 = hwndSlot ? hwndSlot->load(std::memory_order_acquire) : notifyHwnd; + if (slot2 != notifyHwnd || !IsWindow(notifyHwnd)) + { + std::lock_guard lock(g_resultMutex); + g_havePending = false; + g_pendingLocal.clear(); + g_pendingRemote.clear(); + return; + } + + PostMessageW(notifyHwnd, WM_UPDATE_CHECK_READY, 0, 0); +} +} // namespace + +void StartBackgroundUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismissedUnix) +{ + std::thread([notifyHwnd, hwndSlot, dismissedUnix]() { + RunUpdateCheck(notifyHwnd, hwndSlot, dismissedUnix); + }).detach(); +} + +bool UpdateCheck_PopResultForUi(std::string& localVersionOut, std::string& remoteVersionOut) +{ + std::lock_guard lock(g_resultMutex); + if (!g_havePending) + return false; + localVersionOut = g_pendingLocal; + remoteVersionOut = g_pendingRemote; + g_havePending = false; + g_pendingLocal.clear(); + g_pendingRemote.clear(); + return true; +} diff --git a/src/update_check.h b/src/update_check.h new file mode 100644 index 0000000..86e8ffe --- /dev/null +++ b/src/update_check.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include + +/** Posted to `notifyHwnd` when a newer build is available (worker verified HWND is still current). */ +constexpr UINT WM_UPDATE_CHECK_READY = WM_APP + 42; + +/** + * Starts a detached background thread: optional 24h suppression, HTTPS GET of build metadata, + * compare remote FileVersion to local EXE version. On success only, posts WM_UPDATE_CHECK_READY. + * Any failure or "no update" is silent. + * + * @param notifyHwnd Window to receive WM_UPDATE_CHECK_READY. + * @param hwndSlot If non-null, PostMessage is skipped unless this equals notifyHwnd (cleared on WM_DESTROY). + * @param updateDismissedUnix UTC Unix seconds when user dismissed the dialog; 0 = never. + */ +void StartBackgroundUpdateCheck( + HWND notifyHwnd, + std::atomic* hwndSlot, + int64_t updateDismissedUnix); + +/** Call from the main thread when handling WM_UPDATE_CHECK_READY; copies version strings for the UI. */ +bool UpdateCheck_PopResultForUi(std::string& localVersionOut, std::string& remoteVersionOut); From 9b69a5cd1b1b53bbb6dd11c3e1447766ffb6cb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=B6glinger-Stelzer?= Date: Sat, 21 Mar 2026 20:46:51 +0100 Subject: [PATCH 2/3] CA fixes --- src/main.cpp | 2 +- src/update_check.cpp | 19 +++++++++++++++++-- src/update_check.h | 3 +++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a8860c7..1c97529 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -756,7 +756,7 @@ int APIENTRY wWinMain( ShellExecuteW( nullptr, L"open", - L"https://buildbot.nefarius.at/builds/MultiPadTester/latest/MultiPadTester.zip", + UpdateCheck_GetLatestDownloadUrlW(), nullptr, nullptr, SW_SHOWNORMAL); diff --git a/src/update_check.cpp b/src/update_check.cpp index c911cb8..f971d2e 100644 --- a/src/update_check.cpp +++ b/src/update_check.cpp @@ -11,6 +11,9 @@ #include #include +static constexpr wchar_t kDownloadUrl[] = + L"https://buildbot.nefarius.at/builds/MultiPadTester/latest/MultiPadTester.zip"; + namespace { constexpr wchar_t kJsonHost[] = L"buildbot.nefarius.at"; @@ -115,6 +118,7 @@ bool GetLocalExeVersion(VersionQuad& quad, std::string& displayUtf8) bool ExtractJsonFileVersion(std::string_view json, std::string& outVer) { + outVer.clear(); const std::string_view key = "\"FileVersion\""; const size_t pos = json.find(key); if (pos == std::string_view::npos) @@ -133,7 +137,7 @@ bool ExtractJsonFileVersion(std::string_view json, std::string& outVer) const size_t start = i; while (i < json.size() && json[i] != '"') ++i; - if (i <= start) + if (i >= json.size()) return false; outVer.assign(json.data() + start, i - start); return !outVer.empty(); @@ -220,7 +224,13 @@ bool HttpGetUtf8(const wchar_t* host, INTERNET_PORT port, const wchar_t* path, s char buf[8192]; DWORD read = 0; if (!WinHttpReadData(hRequest, buf, sizeof(buf), &read)) - break; + { + bodyOut.clear(); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + closeSession(); + return false; + } if (read == 0) break; bodyOut.append(buf, read); @@ -287,6 +297,11 @@ void RunUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismis } } // namespace +const wchar_t* UpdateCheck_GetLatestDownloadUrlW() +{ + return kDownloadUrl; +} + void StartBackgroundUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismissedUnix) { std::thread([notifyHwnd, hwndSlot, dismissedUnix]() { diff --git a/src/update_check.h b/src/update_check.h index 86e8ffe..a65c4af 100644 --- a/src/update_check.h +++ b/src/update_check.h @@ -25,3 +25,6 @@ void StartBackgroundUpdateCheck( /** Call from the main thread when handling WM_UPDATE_CHECK_READY; copies version strings for the UI. */ bool UpdateCheck_PopResultForUi(std::string& localVersionOut, std::string& remoteVersionOut); + +/** HTTPS URL of the latest portable ZIP (build artifact); single source of truth with the update checker. */ +const wchar_t* UpdateCheck_GetLatestDownloadUrlW(); From 03f417d46e7b5ca37a1c7eaa853ff4459b1c956e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=B6glinger-Stelzer?= Date: Sat, 21 Mar 2026 21:01:54 +0100 Subject: [PATCH 3/3] CA fixes --- src/main.cpp | 16 +++++--- src/update_check.cpp | 92 ++++++++++++++++++++++++++++++++++++-------- src/update_check.h | 30 +++++++++------ 3 files changed, 106 insertions(+), 32 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 1c97529..6767dcb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -243,7 +242,7 @@ static std::vector g_libwdiUsbInstanceIdsUtf8; static std::string g_libwdiUsbProbeErrorUtf8; static AppPrefs g_prefs; -static std::atomic g_updateCheckHwndSlot{nullptr}; +static std::unique_ptr g_updateCheckSession; static bool g_showUpdateAvailable = false; static std::string g_updateLocalVerUtf8; static std::string g_updateRemoteVerUtf8; @@ -323,7 +322,7 @@ static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) return 0; break; case WM_DESTROY: - g_updateCheckHwndSlot.store(nullptr, std::memory_order_release); + g_updateCheckSession.reset(); { RECT r; if (GetWindowRect(hWnd, &r)) @@ -488,8 +487,13 @@ int APIENTRY wWinMain( ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); - g_updateCheckHwndSlot.store(hwnd, std::memory_order_release); - StartBackgroundUpdateCheck(hwnd, &g_updateCheckHwndSlot, g_prefs.updateDismissedUnix); + try + { + g_updateCheckSession = std::make_unique(hwnd, g_prefs.updateDismissedUnix); + } + catch (...) + { + } constexpr float clearColor[4] = {0.06f, 0.06f, 0.07f, 1.0f}; @@ -912,6 +916,8 @@ int APIENTRY wWinMain( g_d3d.Present(g_prefs.vsync); } + g_updateCheckSession.reset(); + g_backends = nullptr; ImGui_ImplDX11_Shutdown(); diff --git a/src/update_check.cpp b/src/update_check.cpp index f971d2e..daa741d 100644 --- a/src/update_check.cpp +++ b/src/update_check.cpp @@ -36,6 +36,14 @@ std::string g_pendingLocal; std::string g_pendingRemote; bool g_havePending = false; +void ClearPendingResult() +{ + std::lock_guard lock(g_resultMutex); + g_havePending = false; + g_pendingLocal.clear(); + g_pendingRemote.clear(); +} + bool ParseVersionString(std::string_view s, VersionQuad& out) { out = {}; @@ -143,9 +151,11 @@ bool ExtractJsonFileVersion(std::string_view json, std::string& outVer) return !outVer.empty(); } -bool HttpGetUtf8(const wchar_t* host, INTERNET_PORT port, const wchar_t* path, std::string& bodyOut) +bool HttpGetUtf8(std::stop_token st, const wchar_t* host, INTERNET_PORT port, const wchar_t* path, std::string& bodyOut) { bodyOut.clear(); + if (st.stop_requested()) + return false; HINTERNET hSession = WinHttpOpen( L"MultiPadTester/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, @@ -221,6 +231,14 @@ bool HttpGetUtf8(const wchar_t* host, INTERNET_PORT port, const wchar_t* path, s for (;;) { + if (st.stop_requested()) + { + bodyOut.clear(); + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + closeSession(); + return false; + } char buf[8192]; DWORD read = 0; if (!WinHttpReadData(hRequest, buf, sizeof(buf), &read)) @@ -242,8 +260,15 @@ bool HttpGetUtf8(const wchar_t* host, INTERNET_PORT port, const wchar_t* path, s return !bodyOut.empty(); } -void RunUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismissedUnix) +void RunUpdateCheck( + std::stop_token st, + std::atomic& hwndSlot, + HWND expectedHwnd, + int64_t dismissedUnix) { + if (st.stop_requested()) + return; + const std::time_t now = std::time(nullptr); if (dismissedUnix > 0) { @@ -252,8 +277,14 @@ void RunUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismis return; } + if (st.stop_requested()) + return; + std::string json; - if (!HttpGetUtf8(kJsonHost, INTERNET_DEFAULT_HTTPS_PORT, kJsonPath, json)) + if (!HttpGetUtf8(st, kJsonHost, INTERNET_DEFAULT_HTTPS_PORT, kJsonPath, json)) + return; + + if (st.stop_requested()) return; std::string remoteVerStr; @@ -272,8 +303,14 @@ void RunUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismis if (CompareVersionQuad(remote, local) <= 0) return; - const HWND slotHwnd = hwndSlot ? hwndSlot->load(std::memory_order_acquire) : notifyHwnd; - if (slotHwnd != notifyHwnd || !IsWindow(notifyHwnd)) + if (st.stop_requested()) + return; + + const HWND slotHwnd = hwndSlot.load(std::memory_order_acquire); + if (slotHwnd != expectedHwnd || !IsWindow(expectedHwnd)) + return; + + if (st.stop_requested()) return; { @@ -283,17 +320,26 @@ void RunUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismis g_havePending = true; } - const HWND slot2 = hwndSlot ? hwndSlot->load(std::memory_order_acquire) : notifyHwnd; - if (slot2 != notifyHwnd || !IsWindow(notifyHwnd)) + if (st.stop_requested()) { - std::lock_guard lock(g_resultMutex); - g_havePending = false; - g_pendingLocal.clear(); - g_pendingRemote.clear(); + ClearPendingResult(); + return; + } + + const HWND slot2 = hwndSlot.load(std::memory_order_acquire); + if (slot2 != expectedHwnd || !IsWindow(expectedHwnd)) + { + ClearPendingResult(); + return; + } + + if (st.stop_requested()) + { + ClearPendingResult(); return; } - PostMessageW(notifyHwnd, WM_UPDATE_CHECK_READY, 0, 0); + PostMessageW(expectedHwnd, WM_UPDATE_CHECK_READY, 0, 0); } } // namespace @@ -302,11 +348,25 @@ const wchar_t* UpdateCheck_GetLatestDownloadUrlW() return kDownloadUrl; } -void StartBackgroundUpdateCheck(HWND notifyHwnd, std::atomic* hwndSlot, int64_t dismissedUnix) +UpdateCheckSession::UpdateCheckSession(HWND notifyHwnd, int64_t updateDismissedUnix) +{ + hwnd_.store(notifyHwnd, std::memory_order_release); + try + { + worker_.emplace([this, notifyHwnd, updateDismissedUnix](std::stop_token st) { + RunUpdateCheck(st, hwnd_, notifyHwnd, updateDismissedUnix); + }); + } + catch (...) + { + hwnd_.store(nullptr, std::memory_order_release); + } +} + +UpdateCheckSession::~UpdateCheckSession() { - std::thread([notifyHwnd, hwndSlot, dismissedUnix]() { - RunUpdateCheck(notifyHwnd, hwndSlot, dismissedUnix); - }).detach(); + hwnd_.store(nullptr, std::memory_order_release); + worker_.reset(); } bool UpdateCheck_PopResultForUi(std::string& localVersionOut, std::string& remoteVersionOut) diff --git a/src/update_check.h b/src/update_check.h index a65c4af..c36db84 100644 --- a/src/update_check.h +++ b/src/update_check.h @@ -4,24 +4,32 @@ #include #include +#include #include +#include /** Posted to `notifyHwnd` when a newer build is available (worker verified HWND is still current). */ constexpr UINT WM_UPDATE_CHECK_READY = WM_APP + 42; /** - * Starts a detached background thread: optional 24h suppression, HTTPS GET of build metadata, - * compare remote FileVersion to local EXE version. On success only, posts WM_UPDATE_CHECK_READY. - * Any failure or "no update" is silent. - * - * @param notifyHwnd Window to receive WM_UPDATE_CHECK_READY. - * @param hwndSlot If non-null, PostMessage is skipped unless this equals notifyHwnd (cleared on WM_DESTROY). - * @param updateDismissedUnix UTC Unix seconds when user dismissed the dialog; 0 = never. + * Owns the background update worker (`std::jthread`): requests stop and joins on destruction so shutdown + * does not race `PostMessage` or namespace-scope result state. The constructor swallows thread start failures. */ -void StartBackgroundUpdateCheck( - HWND notifyHwnd, - std::atomic* hwndSlot, - int64_t updateDismissedUnix); +class UpdateCheckSession +{ +public: + explicit UpdateCheckSession(HWND notifyHwnd, int64_t updateDismissedUnix); + ~UpdateCheckSession(); + + UpdateCheckSession(const UpdateCheckSession&) = delete; + UpdateCheckSession& operator=(const UpdateCheckSession&) = delete; + UpdateCheckSession(UpdateCheckSession&&) = delete; + UpdateCheckSession& operator=(UpdateCheckSession&&) = delete; + +private: + std::atomic hwnd_{nullptr}; + std::optional worker_; +}; /** Call from the main thread when handling WM_UPDATE_CHECK_READY; copies version strings for the UI. */ bool UpdateCheck_PopResultForUi(std::string& localVersionOut, std::string& remoteVersionOut);