Fix and rework client updater logic (#7038)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

* 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 <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
tooomm 2026-10-03 17:57:03 +02:00 • committed by GitHub
parent 0934380a5c
commit 5dc5390880
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 106 additions and 60 deletions

View file

@ -10,6 +10,7 @@
#include <QRegularExpression>
#include <QSysInfo>
#include <QtGlobal>
#include <optional>
#if defined(Q_OS_MACOS)
#include <sys/sysctl.h>
@ -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<int> 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<int> 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<int> 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);

View file

@ -12,6 +12,7 @@
#include <QObject>
#include <QString>
#include <QVariantMap>
#include <optional>
#include <utility>
inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel");
@ -96,7 +97,8 @@ protected:
Release *lastRelease;
protected:
static bool downloadMatchesCurrentOS(const QString &fileName);
static std::optional<int> getTargetVersionForCurrentOS(const QString &fileName);
static QString findBestDownloadUrl(const QVariantList &assets);
[[nodiscard]] virtual QString getReleaseChannelUrl() const = 0;
public: