MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
UpdateChecker.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "UpdateChecker.h"
20
21#include "Logger.h"
22#include "version.h"
23
24#include <nlohmann/json.hpp>
25
26#include <algorithm>
27#include <cctype>
28#include <sstream>
29
30// HTTP requests using libcurl (cross-platform)
31#include <curl/curl.h>
32
33namespace MetaImGUI {
34
35UpdateChecker::UpdateChecker(std::string repoOwner, std::string repoName)
36 : m_repoOwner(std::move(repoOwner)), m_repoName(std::move(repoName)), m_checking(false) {}
37
39 // C++20: std::jthread automatically joins on destruction
40 Cancel();
41}
42
43void UpdateChecker::CheckForUpdatesAsync(std::function<void(const UpdateInfo&)> callback) {
44 const std::lock_guard<std::mutex> lock(m_threadMutex);
45
46 if (m_checking) {
47 LOG_INFO("Update Checker: Check already in progress, skipping");
48 return; // Already checking
49 }
50
51 m_checking = true;
52
53 // C++20: std::jthread with stop_token for clean cancellation
54 m_stopSource = std::stop_source();
55 m_checkThread = std::jthread([this, callback](const std::stop_token& stopToken) {
56 const UpdateInfo info = CheckForUpdatesImpl(stopToken);
57
58 m_checking = false;
59
60 // Only invoke callback if not cancelled
61 if (!stopToken.stop_requested() && callback) {
62 try {
63 callback(info);
64 } catch (const std::exception& e) {
65 LOG_ERROR("Update Checker: Callback threw exception: {}", e.what());
66 } catch (...) {
67 LOG_ERROR("Update Checker: Callback threw unknown exception");
68 }
69 }
70 });
71 // C++20: jthread automatically joins on destruction, no detach() needed
72}
73
75 m_checking = true;
76 // For synchronous calls, use a default stop_token that never stops
77 UpdateInfo info = CheckForUpdatesImpl(std::stop_token{});
78 m_checking = false;
79 return info;
80}
81
83 const std::lock_guard<std::mutex> lock(m_threadMutex);
84
85 // C++20: Request stop using stop_source
86 m_stopSource.request_stop();
87
88 // jthread automatically joins, so we just need to request stop
89 LOG_INFO("Update Checker: Cancellation requested");
90}
91
93 return m_checking;
94}
95
96UpdateInfo UpdateChecker::CheckForUpdatesImpl(const std::stop_token& stopToken) {
97 // C++20: Using designated initializers for clear initialization
98 UpdateInfo info{.updateAvailable = false,
99 .latestVersion = "",
100 .currentVersion = Version::VERSION,
101 .releaseUrl = "",
102 .releaseNotes = "",
103 .downloadUrl = ""};
104
105 try {
106 const std::string jsonResponse = FetchLatestReleaseInfo();
107 if (stopToken.stop_requested()) {
108 LOG_INFO("Update Checker: Check cancelled by user");
109 return info;
110 }
111
112 if (jsonResponse.empty()) {
113 LOG_ERROR("Update Checker: Empty response from server");
114 return info;
115 }
116
117 info = ParseReleaseInfo(jsonResponse);
118 info.currentVersion = Version::VERSION;
119
120 // Compare versions
121 if (!info.latestVersion.empty()) {
122 const int cmp = CompareVersions(info.currentVersion, info.latestVersion);
123 info.updateAvailable = (cmp < 0);
124
125 if (info.updateAvailable) {
126 LOG_INFO("Update Checker: Update available - {} -> {}", info.currentVersion, info.latestVersion);
127 } else {
128 LOG_INFO("Update Checker: No update available (current: {})", info.currentVersion);
129 }
130 } else {
131 LOG_ERROR("Update Checker: Could not parse latest version from response");
132 }
133 } catch (const std::bad_alloc& e) {
134 LOG_ERROR("Update Checker: Memory allocation failed: {}", e.what());
135 info.updateAvailable = false;
136 } catch (const std::exception& e) {
137 LOG_ERROR("Update Checker: Check failed: {}", e.what());
138 info.updateAvailable = false;
139 } catch (...) {
140 LOG_ERROR("Update Checker: Unknown error during update check");
141 info.updateAvailable = false;
142 }
143
144 return info;
145}
146
147// Unified cross-platform implementation using libcurl
148
149namespace {
150size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
151 static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
152 return size * nmemb;
153}
154} // namespace
155
156std::string UpdateChecker::FetchLatestReleaseInfo() {
157 std::string result;
158
159 // RAII-wrap CURL handle to prevent leaks on exceptions
160 const std::unique_ptr<CURL, decltype(&curl_easy_cleanup)> curl(curl_easy_init(), curl_easy_cleanup);
161 if (!curl) {
162 return result;
163 }
164
165 const std::string url = "https://api.github.com/repos/" + m_repoOwner + "/" + m_repoName + "/releases/latest";
166
167 LOG_INFO("Update Checker: Requesting URL: {}", url);
168
169 curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
170 curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback);
171 curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &result);
172 curl_easy_setopt(curl.get(), CURLOPT_USERAGENT, "UpdateChecker/1.0");
173 curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L);
174 curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 10L);
175 curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L);
176 curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L);
177
178 const CURLcode res = curl_easy_perform(curl.get());
179
180 if (res != CURLE_OK) {
181 LOG_ERROR("Update Checker: Request failed: {}", curl_easy_strerror(res));
182 result.clear();
183 }
184
185 LOG_INFO("Update Checker: Response received ({} bytes)", result.size());
186
187 return result;
188}
189
190UpdateInfo UpdateChecker::ParseReleaseInfo(const std::string& jsonResponse) {
191 UpdateInfo info;
192
193 if (jsonResponse.empty()) {
194 LOG_ERROR("Update Checker: Empty JSON response");
195 return info;
196 }
197
198 try {
199 // Parse JSON using nlohmann/json library
200 auto j = nlohmann::json::parse(jsonResponse);
201
202 // Extract tag_name
203 if (j.contains("tag_name") && j["tag_name"].is_string()) {
204 std::string tag = j["tag_name"].get<std::string>();
205 // Remove 'v' prefix if present
206 info.latestVersion = (!tag.empty() && tag[0] == 'v') ? tag.substr(1) : tag;
207 LOG_INFO("Update Checker: Parsed version: {}", info.latestVersion);
208 } else {
209 LOG_ERROR("Update Checker: No tag_name in response");
210 }
211
212 // Extract html_url
213 if (j.contains("html_url") && j["html_url"].is_string()) {
214 info.releaseUrl = j["html_url"].get<std::string>();
215 LOG_INFO("Update Checker: Release URL: {}", info.releaseUrl);
216 }
217
218 // Extract body (release notes)
219 if (j.contains("body") && j["body"].is_string()) {
220 info.releaseNotes = j["body"].get<std::string>();
221 LOG_INFO("Update Checker: Release notes: {} chars", info.releaseNotes.length());
222 }
223
224 // Extract download_url (if available)
225 if (j.contains("assets") && j["assets"].is_array() && !j["assets"].empty()) {
226 auto& firstAsset = j["assets"][0];
227 if (firstAsset.contains("browser_download_url") && firstAsset["browser_download_url"].is_string()) {
228 info.downloadUrl = firstAsset["browser_download_url"].get<std::string>();
229 }
230 }
231
232 } catch (const nlohmann::json::parse_error& e) {
233 LOG_ERROR("Update Checker: JSON parse error: {} at byte {}", e.what(), e.byte);
234 } catch (const nlohmann::json::type_error& e) {
235 LOG_ERROR("Update Checker: JSON type error: {}", e.what());
236 } catch (const std::exception& e) {
237 LOG_ERROR("Update Checker: Unexpected error parsing JSON: {}", e.what());
238 }
239
240 return info;
241}
242
243int UpdateChecker::CompareVersions(const std::string& v1, const std::string& v2) {
244 auto parseVersion = [](std::string version) {
245 // Remove leading 'v' or 'V' if present
246 if (!version.empty() && (version[0] == 'v' || version[0] == 'V')) {
247 version = version.substr(1);
248 }
249
250 std::vector<int> parts;
251 std::stringstream ss(version);
252 std::string part;
253
254 while (std::getline(ss, part, '.')) {
255 // Extract numeric part only
256 std::string numStr;
257 for (const char c : part) {
258 if (std::isdigit(c) != 0) {
259 numStr += c;
260 } else {
261 break;
262 }
263 }
264 if (!numStr.empty()) {
265 parts.push_back(std::stoi(numStr));
266 }
267 }
268
269 // Ensure at least 3 parts (major.minor.patch)
270 while (parts.size() < 3) {
271 parts.push_back(0);
272 }
273
274 return parts;
275 };
276
277 auto parts1 = parseVersion(v1);
278 auto parts2 = parseVersion(v2);
279
280 for (size_t i = 0; i < (std::min)(parts1.size(), parts2.size()); ++i) {
281 if (parts1[i] < parts2[i]) {
282 return -1;
283 }
284 if (parts1[i] > parts2[i]) {
285 return 1;
286 }
287 }
288
289 return 0;
290}
291
292} // namespace MetaImGUI
#define LOG_INFO(...)
Definition Logger.h:218
#define LOG_ERROR(...)
Definition Logger.h:220
static int CompareVersions(const std::string &v1, const std::string &v2)
void CheckForUpdatesAsync(std::function< void(const UpdateInfo &)> callback)
UpdateChecker(std::string repoOwner, std::string repoName)