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 CURL* curl = curl_easy_init();
160 if (curl == nullptr) {
161 return result;
162 }
163
164 const std::string url = "https://api.github.com/repos/" + m_repoOwner + "/" + m_repoName + "/releases/latest";
165
166 LOG_INFO("Update Checker: Requesting URL: {}", url);
167
168 curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
169 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
170 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
171 curl_easy_setopt(curl, CURLOPT_USERAGENT, "UpdateChecker/1.0");
172 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
173 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
174 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
175 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
176
177 const CURLcode res = curl_easy_perform(curl);
178
179 if (res != CURLE_OK) {
180 LOG_ERROR("Update Checker: Request failed: {}", curl_easy_strerror(res));
181 result.clear();
182 }
183
184 curl_easy_cleanup(curl);
185
186 LOG_INFO("Update Checker: Response received ({} bytes)", result.size());
187
188 return result;
189}
190
191UpdateInfo UpdateChecker::ParseReleaseInfo(const std::string& jsonResponse) {
192 UpdateInfo info;
193
194 if (jsonResponse.empty()) {
195 LOG_ERROR("Update Checker: Empty JSON response");
196 return info;
197 }
198
199 try {
200 // Parse JSON using nlohmann/json library
201 auto j = nlohmann::json::parse(jsonResponse);
202
203 // Extract tag_name
204 if (j.contains("tag_name") && j["tag_name"].is_string()) {
205 std::string tag = j["tag_name"].get<std::string>();
206 // Remove 'v' prefix if present
207 info.latestVersion = (!tag.empty() && tag[0] == 'v') ? tag.substr(1) : tag;
208 LOG_INFO("Update Checker: Parsed version: {}", info.latestVersion);
209 } else {
210 LOG_ERROR("Update Checker: No tag_name in response");
211 }
212
213 // Extract html_url
214 if (j.contains("html_url") && j["html_url"].is_string()) {
215 info.releaseUrl = j["html_url"].get<std::string>();
216 LOG_INFO("Update Checker: Release URL: {}", info.releaseUrl);
217 }
218
219 // Extract body (release notes)
220 if (j.contains("body") && j["body"].is_string()) {
221 info.releaseNotes = j["body"].get<std::string>();
222 LOG_INFO("Update Checker: Release notes: {} chars", info.releaseNotes.length());
223 }
224
225 // Extract download_url (if available)
226 if (j.contains("assets") && j["assets"].is_array() && !j["assets"].empty()) {
227 auto& firstAsset = j["assets"][0];
228 if (firstAsset.contains("browser_download_url") && firstAsset["browser_download_url"].is_string()) {
229 info.downloadUrl = firstAsset["browser_download_url"].get<std::string>();
230 }
231 }
232
233 } catch (const nlohmann::json::parse_error& e) {
234 LOG_ERROR("Update Checker: JSON parse error: {} at byte {}", e.what(), e.byte);
235 } catch (const nlohmann::json::type_error& e) {
236 LOG_ERROR("Update Checker: JSON type error: {}", e.what());
237 } catch (const std::exception& e) {
238 LOG_ERROR("Update Checker: Unexpected error parsing JSON: {}", e.what());
239 }
240
241 return info;
242}
243
244int UpdateChecker::CompareVersions(const std::string& v1, const std::string& v2) {
245 auto parseVersion = [](std::string version) {
246 // Remove leading 'v' or 'V' if present
247 if (!version.empty() && (version[0] == 'v' || version[0] == 'V')) {
248 version = version.substr(1);
249 }
250
251 std::vector<int> parts;
252 std::stringstream ss(version);
253 std::string part;
254
255 while (std::getline(ss, part, '.')) {
256 // Extract numeric part only
257 std::string numStr;
258 for (const char c : part) {
259 if (std::isdigit(c) != 0) {
260 numStr += c;
261 } else {
262 break;
263 }
264 }
265 if (!numStr.empty()) {
266 parts.push_back(std::stoi(numStr));
267 }
268 }
269
270 // Ensure at least 3 parts (major.minor.patch)
271 while (parts.size() < 3) {
272 parts.push_back(0);
273 }
274
275 return parts;
276 };
277
278 auto parts1 = parseVersion(v1);
279 auto parts2 = parseVersion(v2);
280
281 for (size_t i = 0; i < (std::min)(parts1.size(), parts2.size()); ++i) {
282 if (parts1[i] < parts2[i]) {
283 return -1;
284 }
285 if (parts1[i] > parts2[i]) {
286 return 1;
287 }
288 }
289
290 return 0;
291}
292
293} // 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)