25#include <nlohmann/json.hpp>
35 : m_repoOwner(std::move(repoOwner)), m_repoName(std::move(repoName)), m_checking(false) {}
46 bool expected =
false;
47 if (!m_checking.compare_exchange_strong(expected,
true, std::memory_order_acq_rel)) {
48 LOG_INFO(
"Update Checker: Check already in progress, skipping");
54 const std::lock_guard<std::mutex> lock(m_threadMutex);
56 m_checkThread = std::jthread([
this, callback](
const std::stop_token& stopToken) {
57 const UpdateInfo info = CheckForUpdatesImpl(stopToken);
61 m_checking.store(
false, std::memory_order_release);
63 if (!stopToken.stop_requested() && callback) {
66 }
catch (
const std::exception& e) {
67 LOG_ERROR(
"Update Checker: Callback threw exception: {}", e.what());
69 LOG_ERROR(
"Update Checker: Callback threw unknown exception");
77 bool expected =
false;
78 if (!m_checking.compare_exchange_strong(expected,
true, std::memory_order_acq_rel)) {
85 UpdateInfo info = CheckForUpdatesImpl(std::stop_token{});
86 m_checking.store(
false, std::memory_order_release);
92 m_checkThread.request_stop();
93 LOG_INFO(
"Update Checker: Cancellation requested");
97 return m_checking.load(std::memory_order_acquire);
100UpdateInfo UpdateChecker::CheckForUpdatesImpl(
const std::stop_token& stopToken) {
103 .currentVersion = Version::VERSION,
110 const HttpClient http;
111 const std::string url =
"https://api.github.com/repos/" + m_repoOwner +
"/" + m_repoName +
"/releases/latest";
112 LOG_INFO(
"Update Checker: Requesting URL: {}", url);
116 const HttpRequest request{
117 .url = url, .userAgent =
"UpdateChecker/1.0", .timeout = std::chrono::seconds{10}, .maxRetries = 2};
118 const HttpResponse response = http.Get(request, stopToken);
120 switch (response.status) {
122 LOG_INFO(
"Update Checker: Check cancelled by user");
126 LOG_WARNING(
"Update Checker: GitHub API rate limit hit");
130 LOG_ERROR(
"Update Checker: Network error fetching release info");
137 if (response.body.empty()) {
138 LOG_ERROR(
"Update Checker: Empty response from server");
143 LOG_INFO(
"Update Checker: Response received ({} bytes)", response.body.size());
144 info = ParseReleaseInfo(response.body);
145 info.currentVersion = Version::VERSION;
147 if (info.latestVersion.empty()) {
148 LOG_ERROR(
"Update Checker: Could not parse latest version from response");
153 const int cmp =
CompareVersions(info.currentVersion, info.latestVersion);
154 info.updateAvailable = (cmp < 0);
157 if (info.updateAvailable) {
158 LOG_INFO(
"Update Checker: Update available - {} -> {}", info.currentVersion, info.latestVersion);
160 LOG_INFO(
"Update Checker: No update available (current: {})", info.currentVersion);
162 }
catch (
const std::bad_alloc& e) {
163 LOG_ERROR(
"Update Checker: Memory allocation failed: {}", e.what());
165 }
catch (
const std::exception& e) {
166 LOG_ERROR(
"Update Checker: Check failed: {}", e.what());
169 LOG_ERROR(
"Update Checker: Unknown error during update check");
176UpdateInfo UpdateChecker::ParseReleaseInfo(
const std::string& jsonResponse) {
179 if (jsonResponse.empty()) {
180 LOG_ERROR(
"Update Checker: Empty JSON response");
185 auto j = nlohmann::json::parse(jsonResponse);
187 if (j.contains(
"tag_name") && j[
"tag_name"].is_string()) {
188 std::string tag = j[
"tag_name"].get<std::string>();
189 info.latestVersion = (!tag.empty() && tag[0] ==
'v') ? tag.substr(1) : tag;
190 LOG_INFO(
"Update Checker: Parsed version: {}", info.latestVersion);
192 LOG_ERROR(
"Update Checker: No tag_name in response");
195 if (j.contains(
"html_url") && j[
"html_url"].is_string()) {
196 info.releaseUrl = j[
"html_url"].get<std::string>();
199 if (j.contains(
"body") && j[
"body"].is_string()) {
200 info.releaseNotes = j[
"body"].get<std::string>();
203 if (j.contains(
"assets") && j[
"assets"].is_array() && !j[
"assets"].empty()) {
204 auto& firstAsset = j[
"assets"][0];
205 if (firstAsset.contains(
"browser_download_url") && firstAsset[
"browser_download_url"].is_string()) {
206 info.downloadUrl = firstAsset[
"browser_download_url"].get<std::string>();
210 }
catch (
const nlohmann::json::parse_error& e) {
211 LOG_ERROR(
"Update Checker: JSON parse error: {} at byte {}", e.what(), e.byte);
212 }
catch (
const nlohmann::json::type_error& e) {
213 LOG_ERROR(
"Update Checker: JSON type error: {}", e.what());
214 }
catch (
const std::exception& e) {
215 LOG_ERROR(
"Update Checker: Unexpected error parsing JSON: {}", e.what());
230struct ParsedVersion {
235bool IsAllDigits(
const std::string& s) {
239 return std::all_of(s.begin(), s.end(), [](
unsigned char c) { return std::isdigit(c) != 0; });
242ParsedVersion ParseSemVer(std::string version) {
243 ParsedVersion parsed;
245 if (!version.empty() && (version[0] ==
'v' || version[0] ==
'V')) {
246 version = version.substr(1);
250 const size_t plus = version.find(
'+');
251 if (plus != std::string::npos) {
252 version = version.substr(0, plus);
255 std::string corePart = version;
256 std::string preReleasePart;
257 const size_t dash = version.find(
'-');
258 if (dash != std::string::npos) {
259 corePart = version.substr(0, dash);
260 preReleasePart = version.substr(dash + 1);
264 std::stringstream ss(corePart);
266 while (std::getline(ss, part,
'.')) {
268 for (
const char c : part) {
269 if (std::isdigit(
static_cast<unsigned char>(c)) != 0) {
275 if (!digits.empty()) {
276 parsed.core.push_back(std::stoi(digits));
280 while (parsed.core.size() < 3) {
281 parsed.core.push_back(0);
284 if (!preReleasePart.empty()) {
285 std::stringstream ss(preReleasePart);
287 while (std::getline(ss,
id,
'.')) {
288 parsed.preRelease.push_back(
id);
295int CompareIdentifiers(
const std::string& a,
const std::string& b) {
296 const bool aDigits = IsAllDigits(a);
297 const bool bDigits = IsAllDigits(b);
298 if (aDigits && bDigits) {
299 const int ai = std::stoi(a);
300 const int bi = std::stoi(b);
310 if (aDigits && !bDigits) {
313 if (!aDigits && bDigits) {
322 const ParsedVersion p1 = ParseSemVer(v1);
323 const ParsedVersion p2 = ParseSemVer(v2);
325 for (
size_t i = 0; i < std::min(p1.core.size(), p2.core.size()); ++i) {
326 if (p1.core[i] < p2.core[i]) {
329 if (p1.core[i] > p2.core[i]) {
335 const bool p1Pre = !p1.preRelease.empty();
336 const bool p2Pre = !p2.preRelease.empty();
337 if (!p1Pre && !p2Pre) {
340 if (p1Pre && !p2Pre) {
343 if (!p1Pre && p2Pre) {
347 const size_t n = std::min(p1.preRelease.size(), p2.preRelease.size());
348 for (
size_t i = 0; i < n; ++i) {
349 const int c = CompareIdentifiers(p1.preRelease[i], p2.preRelease[i]);
354 if (p1.preRelease.size() < p2.preRelease.size()) {
357 if (p1.preRelease.size() > p2.preRelease.size()) {
std::vector< std::string > preRelease