24#include <nlohmann/json.hpp>
36 : m_repoOwner(std::move(repoOwner)), m_repoName(std::move(repoName)), m_checking(false) {}
44 const std::lock_guard<std::mutex> lock(m_threadMutex);
47 LOG_INFO(
"Update Checker: Check already in progress, skipping");
54 m_stopSource = std::stop_source();
55 m_checkThread = std::jthread([
this, callback](
const std::stop_token& stopToken) {
56 const UpdateInfo info = CheckForUpdatesImpl(stopToken);
61 if (!stopToken.stop_requested() && callback) {
64 }
catch (
const std::exception& e) {
65 LOG_ERROR(
"Update Checker: Callback threw exception: {}", e.what());
67 LOG_ERROR(
"Update Checker: Callback threw unknown exception");
77 UpdateInfo info = CheckForUpdatesImpl(std::stop_token{});
83 const std::lock_guard<std::mutex> lock(m_threadMutex);
86 m_stopSource.request_stop();
89 LOG_INFO(
"Update Checker: Cancellation requested");
96UpdateInfo UpdateChecker::CheckForUpdatesImpl(
const std::stop_token& stopToken) {
100 .currentVersion = Version::VERSION,
106 const std::string jsonResponse = FetchLatestReleaseInfo();
107 if (stopToken.stop_requested()) {
108 LOG_INFO(
"Update Checker: Check cancelled by user");
112 if (jsonResponse.empty()) {
113 LOG_ERROR(
"Update Checker: Empty response from server");
117 info = ParseReleaseInfo(jsonResponse);
118 info.currentVersion = Version::VERSION;
121 if (!info.latestVersion.empty()) {
122 const int cmp =
CompareVersions(info.currentVersion, info.latestVersion);
123 info.updateAvailable = (cmp < 0);
125 if (info.updateAvailable) {
126 LOG_INFO(
"Update Checker: Update available - {} -> {}", info.currentVersion, info.latestVersion);
128 LOG_INFO(
"Update Checker: No update available (current: {})", info.currentVersion);
131 LOG_ERROR(
"Update Checker: Could not parse latest version from response");
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;
140 LOG_ERROR(
"Update Checker: Unknown error during update check");
141 info.updateAvailable =
false;
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);
156std::string UpdateChecker::FetchLatestReleaseInfo() {
160 const std::unique_ptr<CURL,
decltype(&curl_easy_cleanup)> curl(curl_easy_init(), curl_easy_cleanup);
165 const std::string url =
"https://api.github.com/repos/" + m_repoOwner +
"/" + m_repoName +
"/releases/latest";
167 LOG_INFO(
"Update Checker: Requesting URL: {}", url);
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);
178 const CURLcode res = curl_easy_perform(curl.get());
180 if (res != CURLE_OK) {
181 LOG_ERROR(
"Update Checker: Request failed: {}", curl_easy_strerror(res));
185 LOG_INFO(
"Update Checker: Response received ({} bytes)", result.size());
190UpdateInfo UpdateChecker::ParseReleaseInfo(
const std::string& jsonResponse) {
193 if (jsonResponse.empty()) {
194 LOG_ERROR(
"Update Checker: Empty JSON response");
200 auto j = nlohmann::json::parse(jsonResponse);
203 if (j.contains(
"tag_name") && j[
"tag_name"].is_string()) {
204 std::string tag = j[
"tag_name"].get<std::string>();
206 info.latestVersion = (!tag.empty() && tag[0] ==
'v') ? tag.substr(1) : tag;
207 LOG_INFO(
"Update Checker: Parsed version: {}", info.latestVersion);
209 LOG_ERROR(
"Update Checker: No tag_name in response");
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);
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());
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>();
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());
244 auto parseVersion = [](std::string version) {
246 if (!version.empty() && (version[0] ==
'v' || version[0] ==
'V')) {
247 version = version.substr(1);
250 std::vector<int> parts;
251 std::stringstream ss(version);
254 while (std::getline(ss, part,
'.')) {
257 for (
const char c : part) {
258 if (std::isdigit(c) != 0) {
264 if (!numStr.empty()) {
265 parts.push_back(std::stoi(numStr));
270 while (parts.size() < 3) {
277 auto parts1 = parseVersion(v1);
278 auto parts2 = parseVersion(v2);
280 for (
size_t i = 0; i < (std::min)(parts1.size(), parts2.size()); ++i) {
281 if (parts1[i] < parts2[i]) {
284 if (parts1[i] > parts2[i]) {