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() {
159 CURL* curl = curl_easy_init();
160 if (curl ==
nullptr) {
164 const std::string url =
"https://api.github.com/repos/" + m_repoOwner +
"/" + m_repoName +
"/releases/latest";
166 LOG_INFO(
"Update Checker: Requesting URL: {}", url);
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);
177 const CURLcode res = curl_easy_perform(curl);
179 if (res != CURLE_OK) {
180 LOG_ERROR(
"Update Checker: Request failed: {}", curl_easy_strerror(res));
184 curl_easy_cleanup(curl);
186 LOG_INFO(
"Update Checker: Response received ({} bytes)", result.size());
191UpdateInfo UpdateChecker::ParseReleaseInfo(
const std::string& jsonResponse) {
194 if (jsonResponse.empty()) {
195 LOG_ERROR(
"Update Checker: Empty JSON response");
201 auto j = nlohmann::json::parse(jsonResponse);
204 if (j.contains(
"tag_name") && j[
"tag_name"].is_string()) {
205 std::string tag = j[
"tag_name"].get<std::string>();
207 info.latestVersion = (!tag.empty() && tag[0] ==
'v') ? tag.substr(1) : tag;
208 LOG_INFO(
"Update Checker: Parsed version: {}", info.latestVersion);
210 LOG_ERROR(
"Update Checker: No tag_name in response");
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);
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());
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>();
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());
245 auto parseVersion = [](std::string version) {
247 if (!version.empty() && (version[0] ==
'v' || version[0] ==
'V')) {
248 version = version.substr(1);
251 std::vector<int> parts;
252 std::stringstream ss(version);
255 while (std::getline(ss, part,
'.')) {
258 for (
const char c : part) {
259 if (std::isdigit(c) != 0) {
265 if (!numStr.empty()) {
266 parts.push_back(std::stoi(numStr));
271 while (parts.size() < 3) {
278 auto parts1 = parseVersion(v1);
279 auto parts2 = parseVersion(v2);
281 for (
size_t i = 0; i < (std::min)(parts1.size(), parts2.size()); ++i) {
282 if (parts1[i] < parts2[i]) {
285 if (parts1[i] > parts2[i]) {