From 5dc5390880bb919e62b2e01ad1efd841c0e60a05 Mon Sep 17 00:00:00 2001 From: tooomm Date: Sat, 3 Oct 2026 17:57:03 +0200 Subject: [PATCH] Fix and rework client updater logic (#7038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * first version * combined logic + better regex * Simplify, we only support 64bit, installer prevents wrong installs * lint + fix Linux versions (missing declaration) * update regex * improve/add comments * comments * ReleaseChannel: parse non-numeric OS versions on Windows Server QSysInfo::productVersion() returns "Server 2022" on Windows Server, so splitting on '.' and calling toInt() made the version unparseable and disabled the in-client updater there. Extract the first numeric token instead; Server 2022 becomes 2022 and matches the Win10 assets. * ReleaseChannel: don't offer 64-bit assets on 32-bit Windows The old downloadMatchesCurrentOS() had an explicit 32-bit branch that only accepted "32bit" assets. Restore that guard: 32-bit Windows builds return no match instead of being offered the 64-bit Win10 installer that won't run. * ReleaseChannel: only query the OS version on updater platforms The in-client updater only ships assets for macOS and Windows, but the host system version was still being resolved on every platform, including Linux where it is never used. Compute it only under the Q_OS_MACOS / Q_OS_WIN guards. --------- Co-authored-by: Lukas BrĂ¼bach --- .../network/update/client/release_channel.cpp | 162 +++++++++++------- .../network/update/client/release_channel.h | 4 +- 2 files changed, 106 insertions(+), 60 deletions(-) diff --git a/cockatrice/src/client/network/update/client/release_channel.cpp b/cockatrice/src/client/network/update/client/release_channel.cpp index 260167bc8..f401dfd00 100644 --- a/cockatrice/src/client/network/update/client/release_channel.cpp +++ b/cockatrice/src/client/network/update/client/release_channel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #if defined(Q_OS_MACOS) #include @@ -26,6 +27,29 @@ #define GIT_SHORT_HASH_LEN 7 +namespace +{ + +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) +// QSysInfo::productVersion() is not guaranteed to start with a number, e.g. it returns +// "Server 2022" on Windows Server. Use the first numeric token as the major host version. +std::optional getProductVersionMajor() +{ + const QString versionString = QSysInfo::productVersion(); + bool ok = false; + static const QRegularExpression firstNumber(R"((\d+))"); + const auto match = firstNumber.match(versionString); + const int version = match.hasMatch() ? match.captured(1).toInt(&ok) : 0; + if (!ok) { + qCWarning(ReleaseChannelLog) << "Unable to determine OS version from" << versionString; + return std::nullopt; + } + return version; +} +#endif + +} // namespace + ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr) { } @@ -43,55 +67,93 @@ void ReleaseChannel::checkForUpdates() connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished); } -// Different release channel checking functions for different operating systems -bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName) +// Find assets compatible with host platform (Linux is not supported by in-client updater) +std::optional ReleaseChannel::getTargetVersionForCurrentOS(const QString &fileName) { -#if defined(Q_OS_MACOS) - static QRegularExpression version_regex("macOS(\\d+)"); - auto match = version_regex.match(fileName); - if (!match.hasMatch()) { - return false; +#if defined(Q_OS_WIN) && Q_PROCESSOR_WORDSIZE == 4 + // The published Windows assets are 64-bit only + Q_UNUSED(fileName); + return std::nullopt; +#endif + + const QRegularExpression *regex = nullptr; + + // Only platforms shipping updater assets (macOS, Windows) need the host version + static const std::optional systemVersion = +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + getProductVersionMajor(); +#else + std::nullopt; +#endif + + if (!systemVersion) { + return std::nullopt; } - auto getSystemVersion = [] { +#if defined(Q_OS_MACOS) + static const bool isIntel = [] { // QSysInfo does not go through translation layers // We need to use sysctl to reliably detect the underlying architecture char arch[255]; size_t len = sizeof(arch); if (sysctlbyname("machdep.cpu.brand_string", arch, &len, nullptr, 0) == 0) { - // Intel mac is only supported on macOS 13 versions - if (QString::fromUtf8(arch).contains("Intel")) { - return 13; - } + return QString::fromUtf8(arch).contains("Intel"); } + return false; + }(); - return QSysInfo::productVersion().split(".")[0].toInt(); - }; - - // older(smaller) releases are compatible with a newer or the same system version - int sys_maj = getSystemVersion(); - int rel_maj = match.captured(1).toInt(); - return rel_maj == sys_maj; + // Apple (ARM) --> Cockatrice-3.0.0-macOS15.dmg + // Apple (x86) --> Cockatrice-3.0.0-macOS15_Intel.dmg + static const QRegularExpression macIntelRegex(R"(-macOS(\d+)_Intel\.[^.]+$)", + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression macRegex(R"(-macOS(\d+)\.[^.]+$)", QRegularExpression::CaseInsensitiveOption); + regex = isIntel ? &macIntelRegex : &macRegex; #elif defined(Q_OS_WIN) -#if Q_PROCESSOR_WORDSIZE == 4 - return fileName.contains("32bit"); -#elif Q_PROCESSOR_WORDSIZE == 8 - const QString &version = QSysInfo::productVersion(); - if (version.startsWith("7") || version.startsWith("8")) { - return fileName.contains("Win7"); - } else { - return fileName.contains("Win10"); - } -#else + // Windows (x86) --> Cockatrice-3.0.0-Windows10.exe + static const QRegularExpression winRegex(R"(-(?:Win|Windows)(\d+)\.[^.]+$)", + QRegularExpression::CaseInsensitiveOption); + regex = &winRegex; + +#else // if the OS doesn't fit one of the above #defines, then it will never match Q_UNUSED(fileName); - return false; + return std::nullopt; #endif -#else // If the OS doesn't fit one of the above #defines, then it will never match - Q_UNUSED(fileName); - return false; -#endif + // Any asset targeting an OS version up to the current host version works + auto match = regex->match(fileName); + if (!match.hasMatch()) { + return std::nullopt; + } + const int targetVersion = match.captured(1).toInt(); + if (targetVersion > *systemVersion) { + return std::nullopt; + } + return targetVersion; +} + +// Pick newest targeted version (highest number) amongst all compatible ones +QString ReleaseChannel::findBestDownloadUrl(const QVariantList &assets) +{ + QString bestUrl; + int bestVersion = -1; + for (const auto &rawAsset : assets) { + QVariantMap asset = rawAsset.toMap(); + QString name = asset["name"].toString(); + QString url = asset["browser_download_url"].toString(); + auto version = getTargetVersionForCurrentOS(name); + if (!version) { + continue; + } + if (*version > bestVersion) { + bestVersion = *version; + bestUrl = url; + } + } + if (!bestUrl.isEmpty()) { + qCInfo(ReleaseChannelLog) << "Best compatible asset=" << bestUrl; + } + return bestUrl; } QString StableReleaseChannel::getManualDownloadUrl() const @@ -138,16 +200,9 @@ void StableReleaseChannel::releaseListFinished() lastRelease->setPublishDate(resultMap["published_at"].toDate()); if (resultMap.contains("assets")) { - auto rawAssets = resultMap["assets"].toList(); - for (const auto &rawAsset : rawAssets) { - QVariantMap asset = rawAsset.toMap(); - QString name = asset["name"].toString(); - QString url = asset["browser_download_url"].toString(); - - if (downloadMatchesCurrentOS(name)) { - lastRelease->setDownloadUrl(url); - break; - } + auto url = findBestDownloadUrl(resultMap["assets"].toList()); + if (!url.isEmpty()) { + lastRelease->setDownloadUrl(url); } } @@ -289,21 +344,10 @@ void BetaReleaseChannel::fileListFinished() bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0); bool compatibleVersion = false; - QStringList resultUrlList{}; - for (QVariant file : resultList) { - QVariantMap map = file.toMap(); - resultUrlList << map["browser_download_url"].toString(); - } - - resultUrlList.sort(); - // iterate in reverse so the first item is the latest os version - for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) { - if (downloadMatchesCurrentOS(*url)) { - compatibleVersion = true; - lastRelease->setDownloadUrl(*url); - qCInfo(ReleaseChannelLog) << "Found compatible version url=" << *url; - break; - } + QString downloadUrl = findBestDownloadUrl(resultList); + if (!downloadUrl.isEmpty()) { + compatibleVersion = true; + lastRelease->setDownloadUrl(downloadUrl); } emit finishedCheck(needToUpdate, compatibleVersion, lastRelease); diff --git a/cockatrice/src/client/network/update/client/release_channel.h b/cockatrice/src/client/network/update/client/release_channel.h index c56d0cfce..e038cb2f4 100644 --- a/cockatrice/src/client/network/update/client/release_channel.h +++ b/cockatrice/src/client/network/update/client/release_channel.h @@ -12,6 +12,7 @@ #include #include #include +#include #include inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel"); @@ -96,7 +97,8 @@ protected: Release *lastRelease; protected: - static bool downloadMatchesCurrentOS(const QString &fileName); + static std::optional getTargetVersionForCurrentOS(const QString &fileName); + static QString findBestDownloadUrl(const QVariantList &assets); [[nodiscard]] virtual QString getReleaseChannelUrl() const = 0; public: