3#if defined(QT_UTILITIES_GUI_QTWIDGETS)
7#include "resources/config.h"
12#include <c++utilities/application/argumentparser.h>
16#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
17#include <c++utilities/io/ansiescapecodes.h>
18#include <c++utilities/io/archive.h>
20#include <QCoreApplication>
25#include <QFutureWatcher>
27#include <QJsonDocument>
29#include <QJsonParseError>
30#include <QNetworkAccessManager>
31#include <QNetworkReply>
33#include <QRegularExpression>
34#include <QStringBuilder>
35#include <QVersionNumber>
36#include <QtConcurrentRun>
39#if defined(QT_UTILITIES_GUI_QTWIDGETS)
46#if defined(QT_UTILITIES_GUI_QTWIDGETS)
47#include <QCoreApplication>
50#if defined(QT_UTILITIES_SETUP_TOOLS_ENABLED)
51#include "ui_updateoptionpage.h"
55class UpdateOptionPage {
57 void setupUi(QWidget *)
60 void retranslateUi(QWidget *)
69#include "resources/config.h"
71#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
72#define QT_UTILITIES_VERSION_SUFFIX QString()
74#define QT_UTILITIES_VERSION_SUFFIX QStringLiteral("-qt5")
77#if defined(Q_OS_WINDOWS)
78#define QT_UTILITIES_EXE_REGEX "\\.exe"
80#define QT_UTILITIES_EXE_REGEX ""
83#if defined(Q_OS_WIN64)
84#if defined(Q_PROCESSOR_X86_64)
85#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-x86_64-w64-mingw32"
86#elif defined(Q_PROCESSOR_ARM_64)
87#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-aarch64-w64-mingw32"
89#elif defined(Q_OS_WIN32)
90#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-i686-w64-mingw32"
91#elif defined(__GNUC__) && defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
92#if defined(Q_PROCESSOR_X86_64)
93#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-x86_64-pc-linux-gnu"
94#elif defined(Q_PROCESSOR_ARM_64)
95#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-aarch64-pc-linux-gnu"
99#if defined(Q_OS_WINDOWS) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
100#include <QNtfsPermissionCheckGuard>
105#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
111#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
113 QNetworkAccessManager *nm =
nullptr;
114 CppUtilities::DateTime lastCheck;
116 QNetworkRequest::CacheLoadControl cacheLoadControl = QNetworkRequest::PreferNetwork;
117 QVersionNumber currentVersion = QVersionNumber();
118 QString currentVersionSuffix = QString();
119 QRegularExpression gitHubRegex = QRegularExpression(QStringLiteral(
".*/github.com/([^/]+)/([^/]+)(/.*)?"));
120 QRegularExpression gitHubRegex2 = QRegularExpression(QStringLiteral(
".*/([^/.]+)\\.github.io/([^/]+)(/.*)?"));
121 QRegularExpression assetRegex = QRegularExpression();
122 QString executableName;
123 QString previouslyFoundNewVersion;
125 QString latestVersion;
126 QString additionalInfo;
127 QString releaseNotes;
132 bool inProgress =
false;
133 bool updateAvailable =
false;
134 bool verbose =
false;
146#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
149 m_p->verbose = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER
"_UPDATER_VERBOSE");
151 const auto &appInfo = CppUtilities::applicationInfo;
152 const auto url = QString::fromUtf8(appInfo.url);
153 auto gitHubMatch = m_p->gitHubRegex.match(url);
154 if (!gitHubMatch.hasMatch()) {
155 gitHubMatch = m_p->gitHubRegex2.match(url);
157 const auto gitHubOrga = gitHubMatch.captured(1);
158 const auto gitHubRepo = gitHubMatch.captured(2);
159 if (gitHubOrga.isNull() || gitHubRepo.isNull()) {
162 const auto currentVersion = QString::fromUtf8(appInfo.version);
166 = QStringLiteral(
"https://api.github.com/repos/") % gitHubOrga % QChar(
'/') % gitHubRepo % QStringLiteral(
"/releases?per_page=25");
167 m_p->currentVersion = QVersionNumber::fromString(currentVersion, &suffixIndex);
168 m_p->currentVersionSuffix = suffixIndex >= 0 ? currentVersion.mid(suffixIndex) : QString();
169#ifdef QT_UTILITIES_DOWNLOAD_REGEX
170 m_p->assetRegex = QRegularExpression(m_p->executableName + QStringLiteral(QT_UTILITIES_DOWNLOAD_REGEX
"\\..+"));
173 qDebug() <<
"deduced executable name: " << m_p->executableName;
174 qDebug() <<
"assumed current version: " << m_p->currentVersion;
175 qDebug() <<
"asset regex for current platform: " << m_p->assetRegex;
181#ifdef QT_UTILITIES_FAKE_NEW_VERSION_AVAILABLE
182 QTimer::singleShot(10000, Qt::VeryCoarseTimer,
this, [
this] { emit
updateAvailable(QStringLiteral(
"foo"), QString()); });
192#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
195 return !m_p->assetRegex.pattern().isEmpty();
201#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
204 return m_p->inProgress;
210#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
213 return m_p->updateAvailable;
219#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
228#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
237#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
238 static const auto v = QString();
241 return m_p->executableName;
247#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
248 static const auto v = QString();
251 return m_p->newVersion;
257#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
258 static const auto v = QString();
261 return m_p->latestVersion;
267#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
268 static const auto v = QString();
271 return m_p->additionalInfo;
277#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
278 static const auto v = QString();
281 return m_p->releaseNotes;
292#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
293 static const auto v = QUrl();
296 return m_p->downloadUrl;
302#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
303 static const auto v = QUrl();
306 return m_p->signatureUrl;
312#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
313 return CppUtilities::DateTime();
315 return m_p->lastCheck;
321#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
324 settings->beginGroup(QStringLiteral(
"updating"));
325 m_p->newVersion = settings->value(
"newVersion").toString();
326 m_p->latestVersion = settings->value(
"latestVersion").toString();
327 m_p->releaseNotes = settings->value(
"releaseNotes").toString();
328 m_p->downloadUrl = settings->value(
"downloadUrl").toUrl();
329 m_p->signatureUrl = settings->value(
"signatureUrl").toUrl();
330 m_p->lastCheck = CppUtilities::DateTime(settings->value(
"lastCheck").toULongLong());
331 m_p->flags =
static_cast<UpdateCheckFlags>(settings->value(
"flags").toULongLong());
332 settings->endGroup();
338#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
341 settings->beginGroup(QStringLiteral(
"updating"));
342 settings->setValue(
"newVersion", m_p->newVersion);
343 settings->setValue(
"latestVersion", m_p->latestVersion);
344 settings->setValue(
"releaseNotes", m_p->releaseNotes);
345 settings->setValue(
"downloadUrl", m_p->downloadUrl);
346 settings->setValue(
"signatureUrl", m_p->signatureUrl);
347 settings->setValue(
"lastCheck",
static_cast<qulonglong
>(m_p->lastCheck.ticks()));
348 settings->setValue(
"flags",
static_cast<qulonglong
>(m_p->flags));
349 settings->endGroup();
355#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
356 if (m_p->inProgress) {
357 return tr(
"checking …");
360 if (!m_p->error.isEmpty()) {
361 return tr(
"unable to check: %1").arg(m_p->error);
363#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
364 if (!m_p->newVersion.isEmpty()) {
365 return tr(
"new version available: %1 (last checked: %2)").arg(m_p->newVersion, QString::fromStdString(m_p->lastCheck.toIsoString()));
366 }
else if (!m_p->latestVersion.isEmpty()) {
367 return tr(
"no new version available, latest release is: %1 (last checked: %2)")
368 .arg(m_p->latestVersion, QString::fromStdString(m_p->lastCheck.toIsoString()));
371 return tr(
"unknown");
376#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
383#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
384void UpdateNotifier::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
386 m_p->cacheLoadControl = cacheLoadControl;
390void UpdateNotifier::setError(
const QString &context, QNetworkReply *reply)
392#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
396 m_p->error = context + reply->errorString();
402void UpdateNotifier::setError(
const QString &context,
const QJsonParseError &jsonError,
const QByteArray &response)
404#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
409 m_p->error = context % jsonError.errorString() % QChar(
' ') % QChar(
'(') % tr(
"at offset %1").arg(jsonError.offset) % QChar(
')');
410 if (!response.isEmpty()) {
411 m_p->error += QStringLiteral(
"\nResponse was: ");
412 m_p->error += QString::fromUtf8(response);
420#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
421 m_p->error = tr(
"This build of the application does not support checking for updates.");
425 if (!m_p->nm || m_p->inProgress) {
429 auto request = QNetworkRequest(m_p->releasesUrl);
430 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, m_p->cacheLoadControl);
431 auto *
const reply = m_p->nm->get(request);
432 connect(reply, &QNetworkReply::finished,
this, &UpdateNotifier::readReleases);
438#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
439 m_p->updateAvailable =
false;
440 m_p->downloadUrl.clear();
441 m_p->signatureUrl.clear();
442 m_p->latestVersion.clear();
443 m_p->newVersion.clear();
444 m_p->releaseNotes.clear();
448void UpdateNotifier::lastCheckNow()
const
450#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
451 m_p->lastCheck = CppUtilities::DateTime::now();
455#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
457static bool isVersionHigher(
const QVersionNumber &lhs,
const QString &lhsSuffix,
const QVersionNumber &rhs,
const QString &rhsSuffix)
459 const auto cmp = QVersionNumber::compare(lhs, rhs);
462 }
else if (cmp < 0) {
465 if (!lhsSuffix.isEmpty() && rhsSuffix.isEmpty()) {
468 if (lhsSuffix.isEmpty() && !rhsSuffix.isEmpty()) {
472 return lhsSuffix > rhsSuffix;
479#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
483 auto jsonError = QJsonParseError();
484 const auto replyDoc = QJsonDocument::fromJson(data, &jsonError);
485 if (jsonError.error != QJsonParseError::NoError) {
486 setError(tr(
"Unable to parse releases: "), jsonError, data);
490#if !defined(QT_JSON_READONLY)
492 qDebug().noquote() <<
"Update check: found releases: " << QString::fromUtf8(replyDoc.toJson(QJsonDocument::Indented));
496 const auto replyArray = replyDoc.array();
499 auto latestVersionFound = QVersionNumber();
500 auto latestVersionSuffix = QString();
501 auto latestVersionAssets = QJsonValue();
502 auto latestVersionAssetsUrl = QString();
503 auto latestVersionReleaseNotes = QString();
504 for (
const auto &releaseInfoVal : replyArray) {
505 const auto releaseInfo = releaseInfoVal.toObject();
506 const auto tag = releaseInfo.value(QLatin1String(
"tag_name")).toString();
507 if ((skipPreReleases && releaseInfo.value(QLatin1String(
"prerelease")).toBool())
508 || (skipDrafts && releaseInfo.value(QLatin1String(
"draft")).toBool())) {
509 qDebug() <<
"Update check: skipping prerelease/draft: " << tag;
513 const auto versionStr = tag.startsWith(QChar(
'v')) ? tag.mid(1) : tag;
514 const auto version = QVersionNumber::fromString(versionStr, &suffixIndex);
515 const auto suffix = suffixIndex >= 0 ? versionStr.mid(suffixIndex) : QString();
516 if (latestVersionFound.isNull() || isVersionHigher(version, suffix, latestVersionFound, latestVersionSuffix)) {
517 latestVersionFound = version;
518 latestVersionSuffix = suffix;
519 latestVersionAssets = releaseInfo.value(QLatin1String(
"assets"));
520 latestVersionAssetsUrl = releaseInfo.value(QLatin1String(
"assets_url")).toString();
521 latestVersionReleaseNotes = releaseInfo.value(QLatin1String(
"body")).toString();
524 qDebug() <<
"Update check: skipping release: " << tag;
527 if (!latestVersionFound.isNull()) {
528 m_p->latestVersion = latestVersionFound.toString() + latestVersionSuffix;
529 m_p->releaseNotes = latestVersionReleaseNotes;
532 const auto foundUpdate
533 = !latestVersionFound.isNull() && isVersionHigher(latestVersionFound, latestVersionSuffix, m_p->currentVersion, m_p->currentVersionSuffix);
535 m_p->newVersion = latestVersionFound.toString() + latestVersionSuffix;
537 if (latestVersionAssets.isArray()) {
538 return processAssets(latestVersionAssets.toArray(), foundUpdate);
539 }
else if (foundUpdate) {
540 return queryRelease(latestVersionAssetsUrl);
547void UpdateNotifier::readReleases()
549#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
550 auto *
const reply =
static_cast<QNetworkReply *
>(sender());
551 reply->deleteLater();
552 switch (reply->error()) {
553 case QNetworkReply::NoError: {
557 case QNetworkReply::OperationCanceledError:
561 setError(tr(
"Unable to request releases: "), reply);
566void UpdateNotifier::queryRelease(
const QUrl &releaseUrl)
568#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
571 auto request = QNetworkRequest(releaseUrl);
572 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, m_p->cacheLoadControl);
573 auto *
const reply = m_p->nm->get(request);
574 connect(reply, &QNetworkReply::finished,
this, &UpdateNotifier::readRelease);
578void UpdateNotifier::readRelease()
580#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
581 auto *
const reply =
static_cast<QNetworkReply *
>(sender());
582 reply->deleteLater();
583 switch (reply->error()) {
584 case QNetworkReply::NoError: {
586 auto jsonError = QJsonParseError();
587 const auto response = reply->readAll();
588 const auto replyDoc = QJsonDocument::fromJson(response, &jsonError);
589 if (jsonError.error != QJsonParseError::NoError) {
590 setError(tr(
"Unable to parse release: "), jsonError, response);
593#if !defined(QT_JSON_READONLY)
595 qDebug().noquote() <<
"Update check: found release info: " << QString::fromUtf8(replyDoc.toJson(QJsonDocument::Indented));
598 processAssets(replyDoc.object().value(QLatin1String(
"assets")).toArray(),
true);
601 case QNetworkReply::OperationCanceledError:
605 setError(tr(
"Unable to request release: "), reply);
610void UpdateNotifier::processAssets(
const QJsonArray &assets,
bool forUpdate)
612#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
616 for (
const auto &assetVal : assets) {
617 if (!m_p->downloadUrl.isEmpty() && !m_p->signatureUrl.isEmpty()) {
620 const auto asset = assetVal.toObject();
621 const auto assetName = asset.value(QLatin1String(
"name")).toString();
622 if (assetName.isEmpty()) {
625 if (m_p->assetRegex.match(assetName).hasMatch()) {
626 const auto url = asset.value(QLatin1String(
"browser_download_url")).toString();
627 if (assetName.endsWith(QLatin1String(
".sig"))) {
628 m_p->signatureUrl = url;
630 m_p->downloadUrl = url;
635 qDebug() <<
"Update check: skipping asset: " << assetName;
639 m_p->updateAvailable = !m_p->downloadUrl.isEmpty();
643 if (forUpdate && m_p->updateAvailable && m_p->newVersion != m_p->previouslyFoundNewVersion) {
645 m_p->previouslyFoundNewVersion = m_p->newVersion;
651#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
653 QNetworkAccessManager *nm =
nullptr;
654 QFile *fakeDownload =
nullptr;
655 QNetworkReply *currentDownload =
nullptr;
656 QNetworkReply *signatureDownload =
nullptr;
657 QNetworkRequest::CacheLoadControl cacheLoadControl = QNetworkRequest::PreferNetwork;
658 QString
error, statusMessage;
659 QByteArray signature;
660 QFutureWatcher<QPair<QString, QString>> watcher;
661 QString executableName;
662 QString signatureExtension;
663 QRegularExpression executableRegex = QRegularExpression();
674 :
Updater(executableName, QString(), parent)
678Updater::Updater(
const QString &executableName,
const QString &signatureExtension, QObject *parent)
682#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
683 Q_UNUSED(executableName)
684 Q_UNUSED(signatureExtension)
686 connect(&m_p->watcher, &QFutureWatcher<void>::finished,
this, &Updater::concludeUpdate);
687 m_p->executableName = executableName;
688 m_p->signatureExtension = signatureExtension;
689 const auto signatureRegex = signatureExtension.isEmpty()
691 : QString(QStringLiteral(
"(") % QRegularExpression::escape(signatureExtension) % QStringLiteral(
")?"));
692#ifdef QT_UTILITIES_EXE_REGEX
693 m_p->executableRegex = QRegularExpression(executableName % QStringLiteral(
QT_UTILITIES_EXE_REGEX) % signatureRegex);
704#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
705 return m_p->currentDownload !=
nullptr || m_p->signatureDownload !=
nullptr || m_p->watcher.isRunning();
713#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
714 return isInProgress() ? tr(
"Update in progress …") : (m_p->error.isEmpty() ? tr(
"Update done") : tr(
"Update failed"));
727#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
728 return m_p->statusMessage.isEmpty() ? m_p->error : m_p->statusMessage;
736#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
737 return m_p->storedPath;
739 static const auto empty = QString();
746#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
755#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
756 m_p->verifyFunction = std::move(verifyFunction);
758 Q_UNUSED(verifyFunction)
762#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
763void Updater::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
765 m_p->cacheLoadControl = cacheLoadControl;
771#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
772 Q_UNUSED(downloadUrl)
773 Q_UNUSED(signatureUrl)
774 setError(tr(
"This build of the application does not support self-updating."));
780 startDownload(downloadUrl, signatureUrl);
787#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
788 if (m_p->currentDownload) {
789 m_p->currentDownload->abort();
791 if (m_p->signatureDownload) {
792 m_p->signatureDownload->abort();
794 if (m_p->watcher.isRunning()) {
795 m_p->watcher.cancel();
800void Updater::setError(
const QString &error)
802#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
803 m_p->statusMessage.clear();
811void Updater::startDownload(
const QString &downloadUrl,
const QString &signatureUrl)
813#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
814 Q_UNUSED(downloadUrl)
815 Q_UNUSED(signatureUrl)
818 m_p->storedPath.clear();
819 m_p->signature.clear();
821 if (
const auto fakeDownloadPath = qEnvironmentVariable(PROJECT_VARNAME_UPPER
"_UPDATER_FAKE_DOWNLOAD"); !fakeDownloadPath.isEmpty()) {
822 m_p->fakeDownload =
new QFile(fakeDownloadPath);
823 m_p->fakeDownload->open(QFile::ReadOnly);
829 auto request = QNetworkRequest(QUrl(downloadUrl));
830 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, m_p->cacheLoadControl);
831 m_p->statusMessage = tr(
"Downloading %1").arg(downloadUrl);
832 m_p->currentDownload = m_p->nm->get(request);
836 connect(m_p->currentDownload, &QNetworkReply::finished,
this, &Updater::handleDownloadFinished);
838 if (!signatureUrl.isEmpty()) {
839 request.setUrl(signatureUrl);
840 m_p->signatureDownload = m_p->nm->get(request);
841 connect(m_p->signatureDownload, &QNetworkReply::finished,
this, &Updater::handleDownloadFinished);
846void Updater::handleDownloadFinished()
848#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
849 if (m_p->signatureDownload && !m_p->signatureDownload->isFinished()) {
854 if (!m_p->currentDownload->isFinished()) {
858 if (m_p->signatureDownload) {
860 m_p->signatureDownload->deleteLater();
861 m_p->signatureDownload =
nullptr;
864 if (m_p->error.isEmpty()) {
867 m_p->currentDownload->deleteLater();
869 m_p->currentDownload =
nullptr;
873void Updater::readSignature()
875#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
876 switch (m_p->signatureDownload->error()) {
877 case QNetworkReply::NoError:
878 m_p->signature = m_p->signatureDownload->readAll();
881 setError(tr(
"Unable to download signature: ") + m_p->signatureDownload->errorString());
886void Updater::storeExecutable()
888#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
889 m_p->statusMessage = tr(
"Extracting …");
892 auto *reply =
static_cast<QIODevice *
>(m_p->fakeDownload);
893 auto archiveName = QString();
894 auto hasError =
false;
896 archiveName = m_p->fakeDownload->fileName();
897 hasError = m_p->fakeDownload->error() != QFileDevice::NoError;
899 reply = m_p->currentDownload;
900 archiveName = m_p->currentDownload->request().url().fileName();
901 hasError = m_p->currentDownload->error() != QNetworkReply::NoError;
904 reply->deleteLater();
905 setError(tr(
"Unable to download update: ") + reply->errorString());
908 auto res = QtConcurrent::run([
this, reply, archiveName] {
909 const auto data = reply->readAll();
910 const auto dataView = std::string_view(data.data(),
static_cast<std::size_t
>(data.size()));
911 auto foundExecutable =
false, foundSignature =
false;
912 auto error = QString(), storePath = QString();
913 auto newExeName = std::string(), signatureName = std::string();
914 auto newExeData = std::string();
915 auto newExe = QFile();
916 reply->deleteLater();
919 const auto appDirPath = QCoreApplication::applicationDirPath();
920 const auto appFilePath = QCoreApplication::applicationFilePath();
921 if (appDirPath.isEmpty() || appFilePath.isEmpty()) {
922 error = tr(
"Unable to determine application path.");
923 return QPair<QString, QString>(
error, storePath);
927 const auto checkCancellation = [
this, &
error] {
928 if (m_p->watcher.isCanceled()) {
929 error = tr(
"Extraction was cancelled.");
935 if (checkCancellation()) {
936 return QPair<QString, QString>(
error, storePath);
940 CppUtilities::walkThroughArchiveFromBuffer(
941 dataView, archiveName.toStdString(),
942 [
this](
const char *filePath,
const char *fileName, mode_t mode) {
945 if (m_p->watcher.isCanceled()) {
948 return m_p->executableRegex.match(QString::fromUtf8(fileName)).hasMatch();
950 [&](std::string_view path, CppUtilities::ArchiveFile &&file) {
952 if (checkCancellation()) {
955 if (file.type != CppUtilities::ArchiveFileType::Regular) {
960 const auto fileName = QString::fromUtf8(file.name.data(),
static_cast<QString::size_type
>(file.name.size()));
961 if (!m_p->signatureExtension.isEmpty() && fileName.endsWith(m_p->signatureExtension)) {
962 m_p->signature = QByteArray::fromStdString(file.content);
963 foundSignature =
true;
964 signatureName = file.name;
965 return foundExecutable && foundSignature;
969 foundExecutable =
true;
970 newExeName = file.name;
971 newExe.setFileName(appDirPath % QChar(
'/') % fileName % QStringLiteral(
".tmp"));
972 if (!newExe.open(QFile::WriteOnly | QFile::Truncate)) {
973 error = tr(
"Unable to create new executable under \"%1\": %2").arg(newExe.fileName(), newExe.errorString());
976 const auto size =
static_cast<qint64
>(file.content.size());
977 if (!(newExe.write(file.content.data(), size) == size) || !newExe.flush()) {
978 error = tr(
"Unable to write new executable under \"%1\": %2").arg(newExe.fileName(), newExe.errorString());
981 if (!newExe.setPermissions(
982 newExe.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther)) {
983 error = tr(
"Unable to make new binary under \"%1\" executable.").arg(newExe.fileName());
987 storePath = newExe.fileName();
988 newExeData = std::move(file.content);
989 return foundExecutable && foundSignature;
991 }
catch (
const CppUtilities::ArchiveException &e) {
992 error = tr(
"Unable to open downloaded archive: %1").arg(e.what());
994 if (
error.isEmpty() && foundExecutable) {
996 if (m_p->verifyFunction) {
997 if (const auto verifyError = m_p->verifyFunction(Updater::Update{ .executableName = newExeName,
998 .signatureName = signatureName,
1000 .signature = std::string_view(m_p->signature.data(), static_cast<std::size_t>(m_p->signature.size())) });
1001 !verifyError.isEmpty()) {
1002 error = tr(
"Unable to verify whether downloaded binary is valid: %1").arg(verifyError);
1003 return QPair<QString, QString>(error, storePath);
1008 auto currentExeInfo = QFileInfo(appFilePath);
1009 auto currentExe = QFile(appFilePath);
1010 const auto completeSuffix = currentExeInfo.completeSuffix();
1011 const auto suffixWithDot = completeSuffix.isEmpty() ? QString() : QChar(
'.') + completeSuffix;
1012 for (
auto i = 0; i < 100; ++i) {
1013 const auto backupNumber = i ? QString::number(i) : QString();
1014 const auto backupPath = QString(currentExeInfo.path() % QChar(
'/') % currentExeInfo.baseName() % QStringLiteral(
"-backup")
1015 % backupNumber % QChar(
'-') % QString::fromUtf8(CppUtilities::applicationInfo.version) % suffixWithDot);
1016 if (QFile::exists(backupPath)) {
1019 if (!currentExe.rename(backupPath)) {
1020 error = tr(
"Unable to move current executable to \"%1\": %2").arg(backupPath, currentExe.errorString());
1021 return QPair<QString, QString>(
error, storePath);
1027 if (!newExe.rename(appFilePath)) {
1028 error = tr(
"Unable to rename new executable \"%1\" to \"%2\": %3").arg(newExe.fileName(), appFilePath, newExe.errorString());
1029 return QPair<QString, QString>(error, storePath);
1031 storePath = newExe.fileName();
1033 if (error.isEmpty() && !foundExecutable) {
1034 error = tr(
"Unable to find executable in downloaded archive.");
1036 return QPair<QString, QString>(error, storePath);
1038 m_p->watcher.setFuture(std::move(res));
1042void Updater::concludeUpdate()
1044#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1045 auto res = m_p->watcher.result();
1046 m_p->error = res.first;
1047 m_p->storedPath = res.second;
1048 if (!m_p->error.isEmpty()) {
1049 m_p->statusMessage.clear();
1050 emit updateFailed(m_p->error);
1052 m_p->statusMessage = tr(
"Update stored under: %1").arg(m_p->storedPath);
1053 emit updateStored();
1055 emit updateStatusChanged(statusMessage());
1056 emit updatePercentageChanged(0, 0);
1057 emit inProgressChanged(
false);
1063 :
updater(executableName.isEmpty() ?
notifier.executableName() : executableName, signatureExtension)
1090 const QString &executableName,
const QString &signatureExtension, QSettings *settings, QNetworkAccessManager *nm, QObject *parent)
1094 m_p->notifier.setNetworkAccessManager(nm);
1095 m_p->updater.setNetworkAccessManager(nm);
1096 m_p->timer.setSingleShot(
true);
1097 m_p->timer.setTimerType(Qt::VeryCoarseTimer);
1098 m_p->settings = settings;
1109 return &m_p->notifier;
1114 return &m_p->updater;
1119 if (m_p->checkInterval.has_value()) {
1120 return m_p->checkInterval.value();
1122 m_p->settings->beginGroup(QStringLiteral(
"updating"));
1124 checkInterval.duration = CppUtilities::TimeSpan::fromMilliseconds(m_p->settings->value(
"checkIntervalMs", 60 * 60 * 1000).toInt());
1125 checkInterval.enabled = m_p->settings->value(
"automaticChecksEnabled",
false).toBool();
1126 m_p->settings->endGroup();
1133 m_p->settings->beginGroup(QStringLiteral(
"updating"));
1134 m_p->settings->setValue(
"checkIntervalMs",
checkInterval.duration.totalMilliseconds());
1135 m_p->settings->setValue(
"automaticChecksEnabled",
checkInterval.enabled);
1136 m_p->settings->endGroup();
1137#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1138 scheduleNextUpdateCheck();
1144 return m_p->considerSeparateSignature;
1149 m_p->considerSeparateSignature = consideringSeparateSignature;
1154 auto error = QString();
1155#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1156 static const auto appDirPath = QCoreApplication::applicationDirPath();
1157 if (appDirPath.isEmpty()) {
1158 return tr(
"Unable to determine the application directory.");
1160#if defined(Q_OS_WINDOWS) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
1161 const auto permissionGuard = QNtfsPermissionCheckGuard();
1163 const auto dirInfo = QFileInfo(appDirPath);
1164 if (!dirInfo.isWritable()) {
1165 return tr(
"The directory where the executable is stored (%1) is not writable.").arg(appDirPath);
1171#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1172void UpdateHandler::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
1174 m_p->notifier.setCacheLoadControl(cacheLoadControl);
1175 m_p->updater.setCacheLoadControl(cacheLoadControl);
1181#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1182 m_p->notifier.restore(m_p->settings);
1183 scheduleNextUpdateCheck();
1189 m_p->updater.performUpdate(
1190 m_p->notifier.downloadUrl().toString(), m_p->considerSeparateSignature ? m_p->notifier.signatureUrl().toString() : QString());
1195#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1196 m_p->notifier.save(m_p->settings);
1200void UpdateHandler::handleUpdateCheckDone()
1202#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1204 scheduleNextUpdateCheck();
1208#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1209void UpdateHandler::scheduleNextUpdateCheck()
1214 if (!interval.enabled || (interval.duration.isNull() && m_p->hasCheckedOnceSinceStartup)) {
1217 const auto timeLeft = interval.duration - (CppUtilities::DateTime::now() - m_p->notifier.lastCheck());
1218 std::cerr << CppUtilities::EscapeCodes::Phrases::Info
1219 <<
"Check for updates due in: " << timeLeft.toString(CppUtilities::TimeSpanOutputFormat::WithMeasures)
1220 << CppUtilities::EscapeCodes::Phrases::End;
1221 m_p->hasCheckedOnceSinceStartup =
true;
1222 m_p->timer.start(std::max(1000,
static_cast<int>(timeLeft.totalMilliseconds())));
1228 m_restartRequested =
true;
1229#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1230 QCoreApplication::quit();
1236#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1237 if (!m_restartRequested) {
1240 auto *
const process =
new QProcess(QCoreApplication::instance());
1241 auto args = QCoreApplication::arguments();
1243 process->setProgram(QCoreApplication::applicationFilePath());
1244 process->setArguments(args);
1245 process->startDetached();
1249#ifdef QT_UTILITIES_GUI_QTWIDGETS
1250struct UpdateOptionPagePrivate {
1252 : updateHandler(updateHandler)
1255 UpdateHandler *updateHandler =
nullptr;
1256 std::function<void()> restartHandler;
1259UpdateOptionPage::UpdateOptionPage(
UpdateHandler *updateHandler, QWidget *parentWidget)
1260 : UpdateOptionPageBase(parentWidget)
1261#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1262 , m_p(std::make_unique<UpdateOptionPagePrivate>(updateHandler))
1265#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
1266 Q_UNUSED(updateHandler)
1270UpdateOptionPage::~UpdateOptionPage()
1274void UpdateOptionPage::setRestartHandler(std::function<
void()> &&handler)
1276 m_p->restartHandler = std::move(handler);
1277#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1278 if (ui() && m_p->restartHandler) {
1279 QObject::connect(ui()->restartPushButton, &QPushButton::clicked, widget(), m_p->restartHandler);
1284bool UpdateOptionPage::apply()
1286#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1287 if (!m_p->updateHandler) {
1291 .duration = CppUtilities::TimeSpan::fromMinutes(ui()->checkIntervalSpinBox->value()), .enabled = ui()->enabledCheckBox->isChecked() });
1295 m_p->updateHandler->notifier()->setFlags(flags);
1296 m_p->updateHandler->saveNotifierState();
1301void UpdateOptionPage::reset()
1303#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1304 if (!m_p->updateHandler) {
1307 const auto &checkInterval = m_p->updateHandler->checkInterval();
1308 ui()->checkIntervalSpinBox->setValue(
static_cast<int>(checkInterval.duration.totalMinutes()));
1309 ui()->enabledCheckBox->setChecked(checkInterval.enabled);
1310 const auto flags = m_p->updateHandler->notifier()->flags();
1316QWidget *UpdateOptionPage::setupWidget()
1318#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1319 if (m_p->updateHandler && m_p->updateHandler->notifier()->isSupported()) {
1320 auto *
const widget = UpdateOptionPageBase::setupWidget();
1321 ui()->versionInUseValueLabel->setText(QString::fromUtf8(CppUtilities::applicationInfo.version));
1322 ui()->updateWidget->hide();
1323 ui()->releaseNotesPushButton->hide();
1324 updateLatestVersion();
1326 QObject::connect(ui()->updatePushButton, &QPushButton::clicked, widget, [
this, widget] {
1327 if (
const auto preCheckError = m_p->updateHandler->preCheck(); preCheckError.isEmpty()
1328 || QMessageBox::critical(widget, QCoreApplication::applicationName(),
1329 QCoreApplication::translate(
"QtGui::UpdateOptionPage",
"<p>%1</p><p><strong>Try the update nevertheless?</strong></p>")
1330 .arg(preCheckError),
1331 QMessageBox::Yes | QMessageBox::No)
1332 == QMessageBox::Yes) {
1333 m_p->updateHandler->performUpdate();
1336 QObject::connect(ui()->abortUpdatePushButton, &QPushButton::clicked, m_p->updateHandler->updater(), &
Updater::abortUpdate);
1337 if (m_p->restartHandler) {
1338 QObject::connect(ui()->restartPushButton, &QPushButton::clicked, widget, m_p->restartHandler);
1340 QObject::connect(ui()->releaseNotesPushButton, &QPushButton::clicked, widget, [
this, widget] {
1341 const auto *
const notifier = m_p->updateHandler->notifier();
1342 QMessageBox::information(widget, QCoreApplication::applicationName(),
1343 QCoreApplication::translate(
"QtGui::UpdateOptionPage",
"<strong>Release notes of version %1:</strong><br>")
1344 .arg(notifier->latestVersion())
1345 + notifier->releaseNotes());
1350 const auto *const updater = m_p->updateHandler->updater();
1351 ui()->updateWidget->setVisible(true);
1352 ui()->updateInProgressLabel->setText(updater->overallStatus());
1353 ui()->updateProgressBar->setVisible(inProgress);
1354 ui()->abortUpdatePushButton->setVisible(inProgress);
1355 ui()->restartPushButton->setVisible(!inProgress && !updater->storedPath().isEmpty() && updater->error().isEmpty());
1358 [
this](
const QString &statusMessage) { ui()->updateStatusLabel->setText(statusMessage); });
1360 if (bytesTotal == 0) {
1361 ui()->updateProgressBar->setMaximum(0);
1363 ui()->updateProgressBar->setValue(static_cast<int>(bytesReceived * 100 / bytesTotal));
1364 ui()->updateProgressBar->setMaximum(100);
1371 auto *
const label =
new QLabel;
1372 label->setWindowTitle(QCoreApplication::translate(
"QtGui::UpdateOptionPage",
"Updating"));
1373 label->setAlignment(Qt::AlignCenter);
1374 label->setWordWrap(
true);
1375#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1376 label->setText(QCoreApplication::translate(
"QtUtilities::UpdateOptionPage",
"Checking for updates is not supported on this platform."));
1378 label->setText(QCoreApplication::translate(
"QtUtilities::UpdateOptionPage",
1379 "This build of %1 has automatic updates disabled. You may update the application in an automated way via your package manager, though.")
1380 .arg(CppUtilities::applicationInfo.name));
1385void UpdateOptionPage::updateLatestVersion(
bool)
1387#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1388 if (!m_p->updateHandler) {
1391 const auto ¬ifier = *m_p->updateHandler->notifier();
1392 const auto &downloadUrl = notifier.downloadUrl();
1393 const auto downloadUrlEscaped = downloadUrl.toString().toHtmlEscaped();
1394 ui()->latestVersionValueLabel->setText(notifier.status());
1395 ui()->downloadUrlLabel->setText(downloadUrl.isEmpty()
1396 ? (notifier.latestVersion().isEmpty()
1397 ? QCoreApplication::translate(
"QtUtilities::UpdateOptionPage",
"no new version available for download")
1398 : QCoreApplication::translate(
1399 "QtUtilities::UpdateOptionPage",
"new version available but no build for the current platform present yet"))
1400 : (QStringLiteral(
"<a href=\"") % downloadUrlEscaped % QStringLiteral(
"\">") % downloadUrlEscaped % QStringLiteral(
"</a>")));
1401 ui()->updatePushButton->setDisabled(downloadUrl.isEmpty());
1402 ui()->releaseNotesPushButton->setHidden(notifier.releaseNotes().isEmpty());
1406VerificationErrorMessageBox::VerificationErrorMessageBox()
1408#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1409 setWindowTitle(QCoreApplication::applicationName());
1410 setStandardButtons(QMessageBox::Cancel | QMessageBox::Ignore);
1411 setDefaultButton(QMessageBox::Cancel);
1412 setIcon(QMessageBox::Critical);
1416VerificationErrorMessageBox::~VerificationErrorMessageBox()
1420int VerificationErrorMessageBox::execForError(QString &errorMessage,
const QString &explanation)
1422#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1423 auto loop = QEventLoop();
1424 QObject::connect(
this, &QDialog::finished, &loop, &QEventLoop::exit);
1425 QMetaObject::invokeMethod(
this,
"openForError", Qt::QueuedConnection, Q_ARG(QString, errorMessage), Q_ARG(QString, explanation));
1426 auto res = loop.exec();
1427 if (res == QMessageBox::Ignore) {
1428 errorMessage.clear();
1432 Q_UNUSED(errorMessage)
1433 Q_UNUSED(explanation)
1438void VerificationErrorMessageBox::openForError(
const QString &errorMessage,
const QString &explanation)
1440#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1441 setText(tr(
"<p>The signature of the downloaded executable could not be verified: %1</p>").arg(errorMessage) + explanation);
1444 Q_UNUSED(errorMessage)
1445 Q_UNUSED(explanation)
1449struct UpdateDialogPrivate {
1450 UpdateOptionPage *updateOptionPage =
nullptr;
1453UpdateDialog::UpdateDialog(QWidget *parent)
1455 , m_p(std::make_unique<UpdateDialogPrivate>())
1457 auto *
const category =
new OptionCategory;
1459 category->assignPages({ m_p->updateOptionPage });
1460 setWindowTitle(m_p->updateOptionPage->widget()->windowTitle());
1461 setTabBarAlwaysVisible(
false);
1462 setSingleCategory(category);
1465UpdateDialog::~UpdateDialog()
1469UpdateOptionPage *UpdateDialog::page()
1471 return m_p->updateOptionPage;
1474const UpdateOptionPage *UpdateDialog::page()
const
1476 return m_p->updateOptionPage;
1483#if defined(QT_UTILITIES_GUI_QTWIDGETS)
void respawnIfRestartRequested()
The SettingsDialog class provides a framework for creating settings dialogs with different categories...
The UpdateHandler class manages the non-graphical aspects of checking for new updates and performing ...
~UpdateHandler() override
bool isConsideringSeparateSignature() const
static UpdateHandler * mainInstance()
void setConsideringSeparateSignature(bool consideringSeparateSignature)
UpdateHandler(QSettings *settings, QNetworkAccessManager *nm, QObject *parent=nullptr)
Handles checking for updates and performing an update of the application if available.
const CheckInterval & checkInterval() const
UpdateNotifier * notifier
void setCheckInterval(CheckInterval checkInterval)
The UpdateNotifier class allows checking for new updates.
void setFlags(UpdateCheckFlags flags)
void setNetworkAccessManager(QNetworkAccessManager *nm)
UpdateNotifier(QObject *parent=nullptr)
void save(QSettings *settings)
bool isInProgress() const
bool isUpdateAvailable() const
void inProgressChanged(bool inProgress)
void supplyNewReleaseData(const QByteArray &data)
UpdateCheckFlags flags() const
const QString & latestVersion() const
void restore(QSettings *settings)
CppUtilities::DateTime lastCheck() const
~UpdateNotifier() override
The Updater class allows downloading and applying an update.
Updater(const QString &executableName, QObject *parent=nullptr)
void updatePercentageChanged(qint64 bytesReceived, qint64 bytesTotal)
void updateStatusChanged(const QString &statusMessage)
void updateFailed(const QString &error)
std::function< QString(const Update &)> VerifyFunction
bool performUpdate(const QString &downloadUrl, const QString &signatureUrl)
void setVerifier(VerifyFunction &&verifyFunction)
void inProgressChanged(bool inProgress)
void setNetworkAccessManager(QNetworkAccessManager *nm)
bool isInProgress() const
qsizetype VersionSuffixIndex
#define INSTANTIATE_UI_FILE_BASED_OPTION_PAGE(SomeClass)
Instantiates a class declared with BEGIN_DECLARE_UI_FILE_BASED_OPTION_PAGE in a convenient way.
bool considerSeparateSignature
UpdateHandlerPrivate(const QString &executableName, const QString &signatureExtension)
std::optional< UpdateHandler::CheckInterval > checkInterval
bool hasCheckedOnceSinceStartup
The CheckInterval struct specifies whether automatic checks for updates are enabled and of often they...
#define QT_UTILITIES_EXE_REGEX
#define QT_UTILITIES_VERSION_SUFFIX