mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-20 19:14:39 +08:00
[feat] add background and manual update checks
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
#include "update_checker.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
UpdateChecker::UpdateChecker(std::string current_version, Fetch fetch)
|
||||
: current_version_(std::move(current_version)), fetch_(std::move(fetch)) {}
|
||||
|
||||
UpdateChecker::~UpdateChecker() { Stop(); }
|
||||
|
||||
bool UpdateChecker::RequestCheck(TimePoint now) {
|
||||
if (stopped_ || pending_.valid()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
pending_ = std::async(std::launch::async, fetch_);
|
||||
status_ = Status::Checking;
|
||||
return true;
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Could not start update check: {}", error.what());
|
||||
ScheduleRetry(now);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateChecker::Poll(TimePoint now) {
|
||||
if (stopped_) {
|
||||
return false;
|
||||
}
|
||||
if (!pending_.valid()) {
|
||||
if (now >= next_check_) {
|
||||
RequestCheck(now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (pending_.wait_for(0ms) != std::future_status::ready) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<VersionInfo> result;
|
||||
try {
|
||||
result = pending_.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Update check failed: {}", error.what());
|
||||
} catch (...) {
|
||||
LOG_WARN("Update check failed with an unknown error");
|
||||
}
|
||||
if (!result) {
|
||||
ScheduleRetry(now);
|
||||
return true;
|
||||
}
|
||||
latest_ = std::move(result);
|
||||
update_available_ =
|
||||
IsNewerVersionWithMetadata(current_version_, latest_->version,
|
||||
latest_->release_date, latest_->patch);
|
||||
status_ = update_available_ ? Status::Available : Status::UpToDate;
|
||||
failures_ = 0;
|
||||
next_check_ = now + 6h;
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateChecker::ScheduleRetry(TimePoint now) {
|
||||
constexpr std::chrono::minutes delays[] = {5min, 30min, 60min};
|
||||
next_check_ = now + delays[std::min(failures_, 2u)];
|
||||
failures_ = std::min(failures_ + 1, 3u);
|
||||
status_ = Status::Failed;
|
||||
}
|
||||
|
||||
void UpdateChecker::Stop() {
|
||||
stopped_ = true;
|
||||
// Join before application/logging teardown. The HTTP client uses finite
|
||||
// connection/read/write and overall request timeouts; no worker is detached.
|
||||
if (pending_.valid()) {
|
||||
pending_.wait();
|
||||
pending_ = {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-24
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _UPDATE_CHECKER_H_
|
||||
#define _UPDATE_CHECKER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "version_checker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Owned and polled by the UI thread. Only the fetch function runs on a worker;
|
||||
// it returns a value and never accesses application or UI state.
|
||||
class UpdateChecker {
|
||||
public:
|
||||
// Wall time includes suspend time, so an overdue check runs on the first UI
|
||||
// tick after wake, including on platforms whose steady clock pauses in sleep.
|
||||
using Clock = std::chrono::system_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
using Fetch = std::function<std::optional<VersionInfo>()>;
|
||||
enum class Status { Idle, Checking, UpToDate, Available, Failed };
|
||||
|
||||
explicit UpdateChecker(std::string current_version,
|
||||
Fetch fetch = CheckUpdate);
|
||||
~UpdateChecker();
|
||||
|
||||
// Manual requests bypass the schedule, but share an already running check.
|
||||
bool RequestCheck(TimePoint now = Clock::now());
|
||||
// Starts due checks and consumes ready results without blocking. Returns true
|
||||
// when a result was consumed; failure leaves the last successful info intact.
|
||||
bool Poll(TimePoint now = Clock::now());
|
||||
void Stop();
|
||||
|
||||
Status status() const { return status_; }
|
||||
bool update_available() const { return update_available_; }
|
||||
const std::optional<VersionInfo>& latest() const { return latest_; }
|
||||
|
||||
private:
|
||||
void ScheduleRetry(TimePoint now);
|
||||
|
||||
std::string current_version_;
|
||||
Fetch fetch_;
|
||||
std::future<std::optional<VersionInfo>> pending_;
|
||||
std::optional<VersionInfo> latest_;
|
||||
TimePoint next_check_{};
|
||||
Status status_ = Status::Idle;
|
||||
unsigned int failures_ = 0;
|
||||
bool update_available_ = false;
|
||||
bool stopped_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -23,10 +23,6 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static std::string latest_release_date_ = "";
|
||||
static bool latest_patch_available_ = false;
|
||||
static int latest_patch_ = 0;
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver);
|
||||
|
||||
namespace {
|
||||
@@ -202,18 +198,20 @@ int CompareNumericVersion(const std::vector<int>& current,
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ResetLatestMetadata() {
|
||||
latest_release_date_ = "";
|
||||
latest_patch_available_ = false;
|
||||
latest_patch_ = 0;
|
||||
}
|
||||
|
||||
bool ReadPatchField(const nlohmann::json& json, int* patch) {
|
||||
if (!json.contains("patch")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& patch_value = json["patch"];
|
||||
if (patch_value.is_number_unsigned()) {
|
||||
const auto parsed = patch_value.get<unsigned long long>();
|
||||
if (parsed > static_cast<unsigned long long>(std::numeric_limits<int>::max())) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
if (patch_value.is_number_integer()) {
|
||||
const long long parsed = patch_value.get<long long>();
|
||||
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
|
||||
@@ -353,9 +351,7 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
|
||||
}
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest) {
|
||||
return IsNewerVersionWithMetadata(
|
||||
current, latest, latest_release_date_,
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return IsNewerVersionWithMetadata(current, latest, "", -1);
|
||||
}
|
||||
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
@@ -391,11 +387,54 @@ bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json CheckUpdate() {
|
||||
std::optional<VersionInfo> ParseVersionInfo(const nlohmann::json& json) {
|
||||
if (!json.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto read_string = [&](const char* key) -> std::string {
|
||||
const auto it = json.find(key);
|
||||
return it != json.end() && it->is_string() ? it->get<std::string>()
|
||||
: std::string{};
|
||||
};
|
||||
VersionInfo info;
|
||||
info.version = read_string("latest_version");
|
||||
if (info.version.empty()) {
|
||||
info.version = read_string("version");
|
||||
}
|
||||
if (!info.version.empty() &&
|
||||
(info.version.front() == 'v' || info.version.front() == 'V')) {
|
||||
info.version.erase(0, 1);
|
||||
}
|
||||
// Invalid/missing version data is a failed check, never "up to date".
|
||||
const auto numeric_end = FindNumericEnd(info.version, 0);
|
||||
if (numeric_end == 0 || info.version[numeric_end - 1] == '.' ||
|
||||
(numeric_end < info.version.size() &&
|
||||
(numeric_end + 1 == info.version.size() ||
|
||||
(info.version[numeric_end] != '-' && info.version[numeric_end] != '+')))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::istringstream parts(info.version.substr(0, numeric_end));
|
||||
std::string part;
|
||||
int number = 0;
|
||||
while (std::getline(parts, part, '.')) {
|
||||
if (!TryParseNonNegativeInt(part, &number)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
info.release_name = read_string("releaseName");
|
||||
info.release_notes = read_string("releaseNotes");
|
||||
info.release_date = read_string("releaseDate");
|
||||
ReadPatchField(json, &info.patch);
|
||||
return info;
|
||||
}
|
||||
|
||||
std::optional<VersionInfo> CheckUpdate() {
|
||||
httplib::Client cli("https://version.crossdesk.cn");
|
||||
|
||||
cli.set_connection_timeout(5);
|
||||
cli.set_read_timeout(5);
|
||||
cli.set_write_timeout(5);
|
||||
cli.set_max_timeout(15000);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
@@ -406,33 +445,25 @@ nlohmann::json CheckUpdate() {
|
||||
if (res) {
|
||||
if (res->status == 200) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(res->body);
|
||||
if (j.contains("releaseDate") && j["releaseDate"].is_string()) {
|
||||
latest_release_date_ = j["releaseDate"];
|
||||
auto info = ParseVersionInfo(nlohmann::json::parse(res->body));
|
||||
if (info) {
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
info->version, info->release_date, info->patch);
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LOG_WARN("version.json does not contain a valid version");
|
||||
}
|
||||
latest_patch_ = 0;
|
||||
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
j.value("latest_version", j.value("version", "")),
|
||||
j.value("releaseDate", ""),
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return j;
|
||||
return info;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARN("Failed to parse version.json: {}", e.what());
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
LogHttpError(res);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,21 @@
|
||||
#define _VERSION_CHECKER_H_
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
nlohmann::json CheckUpdate();
|
||||
struct VersionInfo {
|
||||
std::string version;
|
||||
std::string release_name;
|
||||
std::string release_notes;
|
||||
std::string release_date;
|
||||
int patch = -1;
|
||||
};
|
||||
|
||||
std::optional<VersionInfo> ParseVersionInfo(const nlohmann::json& json);
|
||||
std::optional<VersionInfo> CheckUpdate();
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user