mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-25 14:47:00 +08:00
[feat] add background and manual update checks
This commit is contained in:
@@ -161,6 +161,7 @@ void ResetMainCaptureState(
|
||||
window->set_about_open(false);
|
||||
window->set_update_open(false);
|
||||
window->set_update_available(false);
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Idle);
|
||||
window->set_reset_password_open(false);
|
||||
window->set_reset_password_invalid(false);
|
||||
window->set_new_password_input("");
|
||||
@@ -213,9 +214,20 @@ void ConfigureMainCapturePage(
|
||||
} else if (options.page == "self-hosted") {
|
||||
window->set_settings_open(true);
|
||||
window->set_self_host_settings_open(true);
|
||||
} else if (options.page == "about") {
|
||||
window->set_current_version("0.0.0");
|
||||
} else if (options.page == "about" || options.page == "about-checking" ||
|
||||
options.page == "about-failed" || options.page == "about-update") {
|
||||
window->set_current_version("v1.3.5-1-20260924");
|
||||
window->set_about_open(true);
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::UpToDate);
|
||||
if (options.page == "about-checking") {
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Checking);
|
||||
} else if (options.page == "about-failed" || options.page == "about-update") {
|
||||
window->set_update_available(true);
|
||||
window->set_latest_version("v1.3.6-1-20260924");
|
||||
window->set_update_check_status(options.page == "about-failed"
|
||||
? crossdesk::ui::UpdateCheckStatus::Failed
|
||||
: crossdesk::ui::UpdateCheckStatus::Available);
|
||||
}
|
||||
} else if (options.page == "update") {
|
||||
window->set_update_available(true);
|
||||
window->set_latest_version("v0.0.1");
|
||||
@@ -444,6 +456,24 @@ int main() {
|
||||
window->set_about_open(true);
|
||||
assert(window->get_settings_open());
|
||||
assert(window->get_about_open());
|
||||
window->set_update_available(true);
|
||||
if (window->get_update_open()) {
|
||||
std::cerr << "Automatic update checks must not open the update dialog\n";
|
||||
return 8;
|
||||
}
|
||||
bool update_check_requested = false;
|
||||
window->on_check_for_updates([&] {
|
||||
update_check_requested = true;
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Checking);
|
||||
});
|
||||
window->invoke_check_for_updates();
|
||||
if (!update_check_requested || window->get_update_check_status() !=
|
||||
crossdesk::ui::UpdateCheckStatus::Checking) {
|
||||
std::cerr << "Manual update check must expose the checking state\n";
|
||||
return 8;
|
||||
}
|
||||
window->set_update_available(false);
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Idle);
|
||||
if (const char *snapshot_path =
|
||||
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
|
||||
window->set_about_open(false);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "update_checker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
using namespace std::chrono_literals;
|
||||
using crossdesk::UpdateChecker;
|
||||
using crossdesk::VersionInfo;
|
||||
using Status = UpdateChecker::Status;
|
||||
using TimePoint = UpdateChecker::TimePoint;
|
||||
|
||||
bool Expect(const char* name, bool condition) {
|
||||
if (!condition) {
|
||||
std::cerr << "Failed: " << name << '\n';
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
bool Complete(UpdateChecker& checker, TimePoint now) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + 5s;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (checker.Poll(now)) {
|
||||
return true;
|
||||
}
|
||||
std::this_thread::sleep_for(1ms);
|
||||
}
|
||||
return Expect("worker finishes without blocking UI polling", false);
|
||||
}
|
||||
|
||||
bool TestAsyncAndSchedule() {
|
||||
std::promise<void> release;
|
||||
const auto gate = release.get_future().share();
|
||||
std::promise<void> started;
|
||||
auto started_future = started.get_future();
|
||||
std::atomic<int> calls{0};
|
||||
UpdateChecker checker("1.0.0", [&]() -> std::optional<VersionInfo> {
|
||||
if (++calls == 1) {
|
||||
started.set_value();
|
||||
gate.wait();
|
||||
}
|
||||
return VersionInfo{"1.0.0"};
|
||||
});
|
||||
const TimePoint now{};
|
||||
bool ok = Expect("initial state has no result", !checker.latest());
|
||||
checker.Poll(now);
|
||||
ok &= Expect("startup check starts asynchronously",
|
||||
checker.status() == Status::Checking &&
|
||||
started_future.wait_for(5s) == std::future_status::ready);
|
||||
ok &= Expect("manual request shares running startup check",
|
||||
!checker.RequestCheck(now));
|
||||
checker.Poll(now + 8h);
|
||||
ok &= Expect("overdue timer cannot overlap active request", calls == 1);
|
||||
release.set_value();
|
||||
ok &= Complete(checker, now);
|
||||
ok &= Expect(
|
||||
"successful current version result",
|
||||
checker.status() == Status::UpToDate && !checker.update_available());
|
||||
checker.Poll(now + 6h - 1ms);
|
||||
ok &=
|
||||
Expect("no check before six hours", checker.status() == Status::UpToDate);
|
||||
checker.Poll(now + 6h);
|
||||
ok &= Complete(checker, now + 6h);
|
||||
ok &= Expect("periodic check runs once", calls == 2);
|
||||
// Simulate the process receiving no ticks while the machine is asleep.
|
||||
checker.Poll(now + 20h);
|
||||
ok &= Complete(checker, now + 20h);
|
||||
ok &= Expect("wake starts one overdue check without replaying missed checks",
|
||||
calls == 3);
|
||||
ok &= Expect("manual check bypasses six-hour interval",
|
||||
checker.RequestCheck(now + 20h + 1min));
|
||||
ok &= Complete(checker, now + 20h + 1min);
|
||||
checker.Stop();
|
||||
checker.Poll(now + 48h);
|
||||
ok &= Expect("stopped checker cannot restart",
|
||||
!checker.RequestCheck(now + 48h) && calls == 4);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestRetryAndCachedResult() {
|
||||
int calls = 0;
|
||||
UpdateChecker checker(
|
||||
"1.3.5-1-20260529", [&]() -> std::optional<VersionInfo> {
|
||||
++calls;
|
||||
if (calls <= 4 || calls == 7) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (calls == 5) {
|
||||
return VersionInfo{"1.3.5", "Patch release", "Fixes", "2026-05-29",
|
||||
2};
|
||||
}
|
||||
if (calls == 6) {
|
||||
throw std::runtime_error("simulated network failure");
|
||||
}
|
||||
return VersionInfo{"1.3.5", "", "", "", 1};
|
||||
});
|
||||
TimePoint now{};
|
||||
checker.Poll(now);
|
||||
bool ok = Complete(checker, now);
|
||||
ok &= Expect("startup failure is not up to date",
|
||||
checker.status() == Status::Failed && !checker.latest());
|
||||
for (const auto delay : {5min, 30min, 60min, 60min}) {
|
||||
checker.Poll(now + delay - 1ms);
|
||||
ok &= Expect("failure does not retry early",
|
||||
checker.status() == Status::Failed);
|
||||
now += delay;
|
||||
checker.Poll(now);
|
||||
ok &= Complete(checker, now);
|
||||
}
|
||||
ok &= Expect("metadata patch detects update after retries",
|
||||
checker.status() == Status::Available &&
|
||||
checker.update_available() && calls == 5);
|
||||
checker.RequestCheck(now + 1min);
|
||||
now += 1min;
|
||||
ok &= Complete(checker, now);
|
||||
ok &= Expect("exception is a failure and preserves previous update details",
|
||||
checker.status() == Status::Failed &&
|
||||
checker.update_available() && checker.latest() &&
|
||||
checker.latest()->release_name == "Patch release" &&
|
||||
checker.latest()->release_notes == "Fixes" &&
|
||||
checker.latest()->patch == 2);
|
||||
checker.Poll(now + 5min - 1ms);
|
||||
ok &= Expect("successful check resets backoff",
|
||||
checker.status() == Status::Failed);
|
||||
now += 5min;
|
||||
checker.Poll(now);
|
||||
ok &= Complete(checker, now);
|
||||
ok &= Expect("retry is due five minutes after failure", calls == 7);
|
||||
checker.RequestCheck(now + 1min);
|
||||
ok &= Complete(checker, now + 1min);
|
||||
ok &= Expect("new successful result replaces stale metadata",
|
||||
checker.status() == Status::UpToDate &&
|
||||
!checker.update_available() &&
|
||||
checker.latest()->release_notes.empty() &&
|
||||
checker.latest()->patch == 1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestStopDuringFetch() {
|
||||
std::atomic<bool> finished{false};
|
||||
UpdateChecker checker("1.0.0", [&]() -> std::optional<VersionInfo> {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
finished = true;
|
||||
return VersionInfo{"2.0.0"};
|
||||
});
|
||||
checker.RequestCheck();
|
||||
checker.Stop();
|
||||
return Expect("shutdown joins the worker and discards pending UI results",
|
||||
finished && !checker.Poll() && !checker.latest() &&
|
||||
!checker.RequestCheck());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = TestAsyncAndSchedule();
|
||||
ok &= TestRetryAndCachedResult();
|
||||
ok &= TestStopDuringFetch();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "version_checker.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -50,5 +52,49 @@ int main() {
|
||||
"v1.3.5-9-20260529", "v1.3.6-1-20260529", "", -1),
|
||||
true);
|
||||
|
||||
const auto info =
|
||||
crossdesk::ParseVersionInfo({{"latest_version", "v1.3.5-20260529"},
|
||||
{"patch", "10"},
|
||||
{"releaseName", "Release"},
|
||||
{"releaseNotes", "Changes"},
|
||||
{"releaseDate", "2026-05-29"}});
|
||||
ok &= ExpectEqual("valid metadata is normalized and retained",
|
||||
info && info->version == "1.3.5-20260529" &&
|
||||
info->patch == 10 && info->release_name == "Release" &&
|
||||
info->release_notes == "Changes" &&
|
||||
info->release_date == "2026-05-29",
|
||||
true);
|
||||
const auto legacy = crossdesk::ParseVersionInfo({{"version", "1.3.6"}});
|
||||
ok &= ExpectEqual("legacy version field is supported",
|
||||
legacy && legacy->version == "1.3.6" && legacy->patch == -1,
|
||||
true);
|
||||
for (const auto& json :
|
||||
std::vector<nlohmann::json>{nullptr,
|
||||
nlohmann::json::array(),
|
||||
nlohmann::json::object(),
|
||||
{{"version", false}},
|
||||
{{"version", ""}},
|
||||
{{"version", "unknown"}},
|
||||
{{"version", "1..2"}},
|
||||
{{"version", ".1.2"}},
|
||||
{{"version", "1.2."}},
|
||||
{{"version", "1.2-"}},
|
||||
{{"version", "999999999999999999999.2"}}}) {
|
||||
ok &= ExpectEqual("invalid response is a failed check: " + json.dump(),
|
||||
crossdesk::ParseVersionInfo(json).has_value(), false);
|
||||
}
|
||||
const auto malformed_optional = crossdesk::ParseVersionInfo(
|
||||
{{"version", "1.3.5"},
|
||||
{"patch", std::numeric_limits<uint64_t>::max()},
|
||||
{"releaseName", nullptr},
|
||||
{"releaseNotes", 123}});
|
||||
ok &= ExpectEqual("bad optional metadata does not leak into later checks",
|
||||
malformed_optional && malformed_optional->patch == -1 &&
|
||||
malformed_optional->release_name.empty() &&
|
||||
malformed_optional->release_notes.empty(),
|
||||
true);
|
||||
ok &= ExpectEqual("plain comparison has no hidden metadata from prior checks",
|
||||
crossdesk::IsNewerVersion("1.3.5", "1.3.5"), false);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user