diff options
| author | saturneric <[email protected]> | 2026-09-24 22:59:10 +0200 |
|---|---|---|
| committer | saturneric <[email protected]> | 2026-09-24 22:59:10 +0200 |
| commit | 4249f0192baf88e33bfb4e47828279ae2b569a8d (patch) | |
| tree | 5eea66cc8f58c6770c9777215a58c7be8f375e1b /src/m_ver_check/UpdateChecker.cpp | |
| parent | docs(README): document instant messaging module and UI actions (diff) | |
| download | modules-4249f0192baf88e33bfb4e47828279ae2b569a8d.tar.gz modules-4249f0192baf88e33bfb4e47828279ae2b569a8d.zip | |
feat(update): add robust GitHub update checking
- replace legacy provider tasks with an injectable update checker
- track release, tag, and commit lookup results independently
- preserve last known good results across failed refreshes
- persist schema-versioned update state and reject stale formats
- add update presentation logic and refresh the update dialog UI
- remove provider selection in favor of GitHub Releases
- add comprehensive release-checking and presentation tests
Diffstat (limited to 'src/m_ver_check/UpdateChecker.cpp')
| -rw-r--r-- | src/m_ver_check/UpdateChecker.cpp | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/src/m_ver_check/UpdateChecker.cpp b/src/m_ver_check/UpdateChecker.cpp new file mode 100644 index 0000000..82f62b3 --- /dev/null +++ b/src/m_ver_check/UpdateChecker.cpp @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2021-2024 Saturneric <[email protected]> + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see <https://www.gnu.org/licenses/>. + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric <[email protected]> starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "UpdateChecker.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QPointer> + +#include "ReleaseCheck.h" + +namespace { + +constexpr int kStorageSchema = 2; + +} // namespace + +UpdateChecker::UpdateChecker(Build build, Fetcher fetch, Store store, + Clock clock, QObject* parent) + : QObject(parent), + build_(std::move(build)), + fetch_(std::move(fetch)), + store_(std::move(store)), + clock_(clock ? std::move(clock) + : [] { return QDateTime::currentDateTime(); }) { + if (store_.load) state_ = FromStorage(store_.load(), build_); +} + +auto UpdateChecker::State() const -> const UpdateSnapshot& { return state_; } + +auto UpdateChecker::IsFresh() const -> bool { + const auto& good = state_.last_good; + return good && IsAuthoritative(*good) && + good->SameBuild(build_.version, build_.commit) && + good->checked_at.secsTo(clock_()) < kFreshSeconds; +} + +void UpdateChecker::Start(CheckMode mode) { + if (state_.checking) return; + if (mode == CheckMode::kIfStale && IsFresh()) return; + + fresh_ = SoftwareVersion{}; + fresh_.current_version = build_.version; + fresh_.local_commit_hash = build_.commit; + + const bool ask_commit = !build_.commit.trimmed().isEmpty(); + // Counted before anything is sent: a fetcher may answer synchronously. + pending_ = ask_commit ? 3 : 2; + state_.checking = true; + emit Changed(); + + const QString base = QString::fromLatin1(kRepoApi); + // Replies can outlive the checker: the module deletes it on deactivation. + QPointer<UpdateChecker> self(this); + + // The list, not /releases/latest: with parallel stable and mainline tracks + // the newest release in the running build's own series is wanted, which a + // single global "latest" cannot express. + fetch_(QUrl(base + "/releases?per_page=100"), [self](int status, + const QByteArray& body) { + if (self.isNull()) return; + self->fresh_.list = ParseReleaseList(status, body, self->build_.version); + self->finish(); + }); + + fetch_(QUrl(base + "/releases/tags/" + build_.version), + [self](int status, const QByteArray& body) { + if (self.isNull()) return; + self->fresh_.tag_fact = ParseTagLookup(status, body); + self->finish(); + }); + + if (ask_commit) { + fetch_(QUrl(base + "/commits/" + build_.commit.trimmed()), + [self](int status, const QByteArray& body) { + if (self.isNull()) return; + self->fresh_.commit_fact = + ParseCommitLookup(status, body, self->build_.commit); + self->finish(); + }); + } +} + +void UpdateChecker::finish() { + if (--pending_ > 0) return; + + const auto now = clock_(); + fresh_.checked_at = now; + fresh_.complete = IsComplete(fresh_); + + state_.checking = false; + state_.last_attempt = now; + state_.last_attempt_failed = !IsUsable(fresh_); + state_.last_good = Merge(state_.last_good, fresh_); + + if (store_.save) store_.save(ToStorage(state_)); + emit Changed(); +} + +auto UpdateChecker::ToStorage(const UpdateSnapshot& s) -> QByteArray { + QJsonObject obj; + obj["schema"] = kStorageSchema; + if (s.last_good) obj["last_good"] = s.last_good->ToJson(); + if (s.last_attempt.isValid()) { + obj["last_attempt"] = s.last_attempt.toSecsSinceEpoch(); + } + obj["last_attempt_failed"] = s.last_attempt_failed; + return QJsonDocument(obj).toJson(QJsonDocument::Compact); +} + +auto UpdateChecker::FromStorage(const QByteArray& data, const Build& build) + -> UpdateSnapshot { + UpdateSnapshot s; + const auto doc = QJsonDocument::fromJson(data); + if (!doc.isObject()) return s; + + const auto obj = doc.object(); + if (obj.value("schema").toInt() != kStorageSchema) return s; + + auto good = SoftwareVersion::FromJson(obj.value("last_good").toObject()); + // What was learned about another build says nothing about this one, and + // neither does how asking about it went. + if (!good || !good->SameBuild(build.version, build.commit)) return s; + + s.last_good = std::move(good); + if (obj.value("last_attempt").isDouble()) { + s.last_attempt = + QDateTime::fromSecsSinceEpoch(obj.value("last_attempt").toInteger()); + } + s.last_attempt_failed = obj.value("last_attempt_failed").toBool(); + return s; +} + +auto ShouldPromptOnStartup(const UpdateSnapshot& s) -> bool { + return !s.checking && s.last_good && + ShouldPromptAtStartup(Decide(*s.last_good)); +} |
