Qt Utilities 6.21.3
Common Qt related C++ classes and routines used by my applications such as dialogs, widgets and models
Loading...
Searching...
No Matches
updater.cpp
Go to the documentation of this file.
1#include "./updater.h"
2
3#if defined(QT_UTILITIES_GUI_QTWIDGETS)
5#endif
6
7#include "resources/config.h"
8
9#include <QSettings>
10#include <QTimer>
11
12#include <c++utilities/application/argumentparser.h>
13
14#include <optional>
15
16#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
17#include <c++utilities/io/ansiescapecodes.h>
18#include <c++utilities/io/archive.h>
19
20#include <QCoreApplication>
21#include <QDebug>
22#include <QEventLoop>
23#include <QFile>
24#include <QFileInfo>
25#include <QFutureWatcher>
26#include <QJsonArray>
27#include <QJsonDocument>
28#include <QJsonObject>
29#include <QJsonParseError>
30#include <QList>
31#include <QMap>
32#include <QNetworkAccessManager>
33#include <QNetworkReply>
34#include <QProcess>
35#include <QRegularExpression>
36#include <QStringBuilder>
37#include <QVersionNumber>
38#include <QtConcurrentRun>
39#include <QtGlobal> // for QtProcessorDetection and QtSystemDetection keeping it Qt 5 compatible
40
41#if defined(QT_UTILITIES_GUI_QTWIDGETS)
42#include <QMessageBox>
43#endif
44
45#include <iostream>
46#endif
47
48#if defined(QT_UTILITIES_GUI_QTWIDGETS)
49#include <QCoreApplication>
50#include <QLabel>
51
52#if defined(QT_UTILITIES_SETUP_TOOLS_ENABLED)
53#include "ui_updateoptionpage.h"
54#else
55namespace QtUtilities {
56namespace Ui {
57class UpdateOptionPage {
58public:
59 void setupUi(QWidget *)
60 {
61 }
62 void retranslateUi(QWidget *)
63 {
64 }
65};
66} // namespace Ui
67} // namespace QtUtilities
68#endif
69#endif
70
71#include "resources/config.h"
72
73#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
74#define QT_UTILITIES_VERSION_SUFFIX QString()
75#else
76#define QT_UTILITIES_VERSION_SUFFIX QStringLiteral("-qt5")
77#endif
78
79#if defined(Q_OS_WINDOWS)
80#define QT_UTILITIES_EXE_REGEX "\\.exe"
81#else
82#define QT_UTILITIES_EXE_REGEX ""
83#endif
84
85#if defined(Q_OS_WIN64)
86#if defined(Q_PROCESSOR_X86_64)
87#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-x86_64-w64-mingw32"
88#elif defined(Q_PROCESSOR_ARM_64)
89#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-aarch64-w64-mingw32"
90#endif
91#elif defined(Q_OS_WIN32)
92#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-i686-w64-mingw32"
93#elif defined(__GNUC__) && defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
94#if defined(Q_PROCESSOR_X86_64)
95#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-x86_64-pc-linux-gnu"
96#elif defined(Q_PROCESSOR_ARM_64)
97#define QT_UTILITIES_DOWNLOAD_REGEX "-.*-aarch64-pc-linux-gnu"
98#endif
99#endif
100
101#if defined(Q_OS_WINDOWS) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
102#include <QNtfsPermissionCheckGuard>
103#endif
104
105namespace QtUtilities {
106
107#if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0))
108using VersionSuffixIndex = qsizetype;
109#else
110using VersionSuffixIndex = int;
111#endif
112
113#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
114static QNetworkRequest makeNetworkRequest(const QUrl &url, QNetworkRequest::CacheLoadControl cacheLoadControl)
115{
116 auto request = QNetworkRequest(url);
117 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
118 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, cacheLoadControl);
119 return request;
120}
121
122struct VersionAndSuffix {
123 operator bool() const
124 {
125 return !version.isNull();
126 }
127 bool operator>(const VersionAndSuffix &rhs) const
128 {
129 const auto cmp = QVersionNumber::compare(version, rhs.version);
130 if (cmp > 0) {
131 return true; // lhs is newer
132 } else if (cmp < 0) {
133 return false; // rhs is newer
134 }
135 if (!suffix.isEmpty() && rhs.suffix.isEmpty()) {
136 return false; // lhs is pre-release and rhs is regular release, so rhs is newer
137 }
138 if (suffix.isEmpty() && !rhs.suffix.isEmpty()) {
139 return true; // lhs is regular release and rhs is pre-release, so lhs is newer
140 }
141 // compare pre-release suffix
142 return suffix > rhs.suffix;
143 }
144 QString toString() const
145 {
146 return version.toString() + suffix;
147 }
148 static VersionAndSuffix fromString(const QString &versionString)
149 {
150 auto res = VersionAndSuffix();
151 auto suffixIndex = VersionSuffixIndex(-1);
152 res.version = QVersionNumber::fromString(versionString, &suffixIndex);
153 res.suffix = suffixIndex >= 0 ? versionString.mid(suffixIndex) : QString();
154 // ignore suffixes that are not like "alpha1", "beta2" and "rc3" (so e.g. Git revisions like "3224.493f60f2" are ignored)
155 if (static const auto validSuffixRegex = QRegularExpression(QRegularExpression::anchoredPattern(QStringLiteral("-?\\w+\\d?")));
156 !validSuffixRegex.match(res.suffix).hasMatch()) {
157 res.suffix.clear();
158 }
159 return res;
160 }
161 QVersionNumber version;
162 QString suffix;
163};
164
166 QNetworkRequest makeRequest(const QUrl &url) const
167 {
168 return makeNetworkRequest(url, cacheLoadControl);
169 }
170
171 QNetworkAccessManager *nm = nullptr;
172 CppUtilities::DateTime lastCheck;
174 QNetworkRequest::CacheLoadControl cacheLoadControl = QNetworkRequest::PreferNetwork;
175 VersionAndSuffix currentVersion;
176 QRegularExpression gitHubRegex = QRegularExpression(QStringLiteral(".*/github.com/([^/]+)/([^/]+)(/.*)?"));
177 QRegularExpression gitHubRegex2 = QRegularExpression(QStringLiteral(".*/([^/.]+)\\.github.io/([^/]+)(/.*)?"));
178 QRegularExpression assetRegex = QRegularExpression();
179 QString executableName;
180 QString previouslyFoundNewVersion;
181 QString newVersion;
182 QString latestVersion;
183 QString additionalInfo;
184 QString releaseNotes;
185 QString error;
186 QUrl downloadUrl;
187 QUrl signatureUrl;
188 QUrl releasesUrl;
189 QUrl previousVersionDownloadUrl;
190 QUrl previousVersionSignatureUrl;
191 QList<std::variant<QJsonArray, QString>> previousVersionAssets;
192 bool inProgress = false;
193 bool updateAvailable = false;
194 bool verbose = false;
195};
196#else
198 QString error;
199};
200#endif
201
203 : QObject(parent)
204 , m_p(std::make_unique<UpdateNotifierPrivate>())
205{
206#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
207 return;
208#else
209 m_p->verbose = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_UPDATER_VERBOSE");
210
211 const auto &appInfo = CppUtilities::applicationInfo;
212 const auto url = QString::fromUtf8(appInfo.url);
213 auto gitHubMatch = m_p->gitHubRegex.match(url);
214 if (!gitHubMatch.hasMatch()) {
215 gitHubMatch = m_p->gitHubRegex2.match(url);
216 }
217 const auto gitHubOrga = gitHubMatch.captured(1);
218 const auto gitHubRepo = gitHubMatch.captured(2);
219 if (gitHubOrga.isNull() || gitHubRepo.isNull()) {
220 return;
221 }
222 m_p->executableName = gitHubRepo + QT_UTILITIES_VERSION_SUFFIX;
223 m_p->releasesUrl
224 = QStringLiteral("https://api.github.com/repos/") % gitHubOrga % QChar('/') % gitHubRepo % QStringLiteral("/releases?per_page=25");
225 m_p->currentVersion = VersionAndSuffix::fromString(QString::fromUtf8(appInfo.version));
226#ifdef QT_UTILITIES_DOWNLOAD_REGEX
227 m_p->assetRegex = QRegularExpression(m_p->executableName + QStringLiteral(QT_UTILITIES_DOWNLOAD_REGEX "\\..+"));
228#endif
229 if (m_p->verbose) {
230 qDebug() << "deduced executable name: " << m_p->executableName;
231 qDebug() << "assumed current version: " << m_p->currentVersion.version;
232 qDebug() << "asset regex for current platform: " << m_p->assetRegex;
233 }
234
235 connect(this, &UpdateNotifier::checkedForUpdate, this, &UpdateNotifier::lastCheckNow);
236#endif
237
238#ifdef QT_UTILITIES_FAKE_NEW_VERSION_AVAILABLE
239 QTimer::singleShot(10000, Qt::VeryCoarseTimer, this, [this] { emit updateAvailable(QStringLiteral("foo"), QString()); });
240#endif
241}
242
246
248{
249#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
250 return false;
251#else
252 return !m_p->assetRegex.pattern().isEmpty();
253#endif
254}
255
257{
258#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
259 return false;
260#else
261 return m_p->inProgress;
262#endif
263}
264
266{
267#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
268 return false;
269#else
270 return m_p->updateAvailable;
271#endif
272}
273
275{
276#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
278#else
279 return m_p->flags;
280#endif
281}
282
284{
285#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
286 Q_UNUSED(flags)
287#else
288 m_p->flags = flags;
289#endif
290}
291
292const QString &UpdateNotifier::executableName() const
293{
294#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
295 static const auto v = QString();
296 return v;
297#else
298 return m_p->executableName;
299#endif
300}
301
302const QString &UpdateNotifier::newVersion() const
303{
304#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
305 static const auto v = QString();
306 return v;
307#else
308 return m_p->newVersion;
309#endif
310}
311
312const QString &UpdateNotifier::latestVersion() const
313{
314#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
315 static const auto v = QString();
316 return v;
317#else
318 return m_p->latestVersion;
319#endif
320}
321
322const QString &UpdateNotifier::additionalInfo() const
323{
324#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
325 static const auto v = QString();
326 return v;
327#else
328 return m_p->additionalInfo;
329#endif
330}
331
332const QString &UpdateNotifier::releaseNotes() const
333{
334#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
335 static const auto v = QString();
336 return v;
337#else
338 return m_p->releaseNotes;
339#endif
340}
341
342const QString &UpdateNotifier::error() const
343{
344 return m_p->error;
345}
346
348{
349#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
350 static const auto v = QUrl();
351 return v;
352#else
353 return m_p->downloadUrl;
354#endif
355}
356
358{
359#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
360 static const auto v = QUrl();
361 return v;
362#else
363 return m_p->signatureUrl;
364#endif
365}
366
368{
369#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
370 static const auto v = QUrl();
371 return v;
372#else
373 return m_p->previousVersionDownloadUrl;
374#endif
375}
376
378{
379#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
380 static const auto v = QUrl();
381 return v;
382#else
383 return m_p->previousVersionSignatureUrl;
384#endif
385}
386
387CppUtilities::DateTime UpdateNotifier::lastCheck() const
388{
389#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
390 return CppUtilities::DateTime();
391#else
392 return m_p->lastCheck;
393#endif
394}
395
396void UpdateNotifier::restore(QSettings *settings)
397{
398#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
399 Q_UNUSED(settings)
400#else
401 settings->beginGroup(QStringLiteral("updating"));
402 m_p->newVersion = settings->value("newVersion").toString();
403 m_p->latestVersion = settings->value("latestVersion").toString();
404 m_p->releaseNotes = settings->value("releaseNotes").toString();
405 m_p->downloadUrl = settings->value("downloadUrl").toUrl();
406 m_p->signatureUrl = settings->value("signatureUrl").toUrl();
407 m_p->previousVersionDownloadUrl = settings->value("previousVersionDownloadUrl").toUrl();
408 m_p->previousVersionSignatureUrl = settings->value("previousVersionSignatureUrl").toUrl();
409 m_p->lastCheck = CppUtilities::DateTime(settings->value("lastCheck").toULongLong());
410 m_p->flags = static_cast<UpdateCheckFlags>(settings->value("flags").toULongLong());
411 settings->endGroup();
412#endif
413}
414
415void UpdateNotifier::save(QSettings *settings)
416{
417#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
418 Q_UNUSED(settings)
419#else
420 settings->beginGroup(QStringLiteral("updating"));
421 settings->setValue("newVersion", m_p->newVersion);
422 settings->setValue("latestVersion", m_p->latestVersion);
423 settings->setValue("releaseNotes", m_p->releaseNotes);
424 settings->setValue("downloadUrl", m_p->downloadUrl);
425 settings->setValue("signatureUrl", m_p->signatureUrl);
426 settings->setValue("previousVersionDownloadUrl", m_p->previousVersionDownloadUrl);
427 settings->setValue("previousVersionSignatureUrl", m_p->previousVersionSignatureUrl);
428 settings->setValue("lastCheck", static_cast<qulonglong>(m_p->lastCheck.ticks()));
429 settings->setValue("flags", static_cast<qulonglong>(m_p->flags));
430 settings->endGroup();
431#endif
432}
433
435{
436#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
437 if (m_p->inProgress) {
438 return tr("checking …");
439 }
440#endif
441 if (!m_p->error.isEmpty()) {
442 return tr("unable to check: %1").arg(m_p->error);
443 }
444#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
445 if (!m_p->newVersion.isEmpty()) {
446 return tr("new version available: %1 (last checked: %2)").arg(m_p->newVersion, QString::fromStdString(m_p->lastCheck.toIsoString()));
447 } else if (!m_p->latestVersion.isEmpty()) {
448 return tr("no new version available, latest release is: %1 (last checked: %2)")
449 .arg(m_p->latestVersion, QString::fromStdString(m_p->lastCheck.toIsoString()));
450 }
451#endif
452 return tr("unknown");
453}
454
455void UpdateNotifier::setNetworkAccessManager(QNetworkAccessManager *nm)
456{
457#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
458 Q_UNUSED(nm)
459#else
460 m_p->nm = nm;
461#endif
462}
463
464#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
465void UpdateNotifier::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
466{
467 m_p->cacheLoadControl = cacheLoadControl;
468}
469#endif
470
471void UpdateNotifier::setError(const QString &context, QNetworkReply *reply)
472{
473#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
474 Q_UNUSED(context)
475 Q_UNUSED(reply)
476#else
477 m_p->error = context + reply->errorString();
478 emit checkedForUpdate();
479 emit inProgressChanged(m_p->inProgress = false);
480#endif
481}
482
483void UpdateNotifier::setError(const QString &context, const QJsonParseError &jsonError, const QByteArray &response)
484{
485#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
486 Q_UNUSED(context)
487 Q_UNUSED(jsonError)
488 Q_UNUSED(response)
489#else
490 m_p->error = context % jsonError.errorString() % QChar(' ') % QChar('(') % tr("at offset %1").arg(jsonError.offset) % QChar(')');
491 if (!response.isEmpty()) {
492 m_p->error += QStringLiteral("\nResponse was: ");
493 m_p->error += QString::fromUtf8(response);
494 }
495 emit inProgressChanged(m_p->inProgress = false);
496#endif
497}
498
500{
501#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
502 m_p->error = tr("This build of the application does not support checking for updates.");
503 emit inProgressChanged(false);
504 return;
505#else
506 if (!m_p->nm || m_p->inProgress) {
507 return;
508 }
509 emit inProgressChanged(m_p->inProgress = true);
510 auto *const reply = m_p->nm->get(m_p->makeRequest(m_p->releasesUrl));
511 connect(reply, &QNetworkReply::finished, this, &UpdateNotifier::readReleases);
512#endif
513}
514
516{
517#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
518 m_p->updateAvailable = false;
519 m_p->downloadUrl.clear();
520 m_p->signatureUrl.clear();
521 m_p->latestVersion.clear();
522 m_p->newVersion.clear();
523 m_p->releaseNotes.clear();
524#endif
525}
526
527void UpdateNotifier::lastCheckNow() const
528{
529#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
530 m_p->lastCheck = CppUtilities::DateTime::now();
531#endif
532}
533
534#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
539bool UpdateNotifier::isVersionHigher(const QString &lhs, const QString &rhs)
540{
541 return VersionAndSuffix::fromString(lhs) > VersionAndSuffix::fromString(rhs);
542}
543#endif
544
545void UpdateNotifier::supplyNewReleaseData(const QByteArray &data)
546{
547#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
548 Q_UNUSED(data)
549#else
550 // parse JSON
551 auto jsonError = QJsonParseError();
552 const auto replyDoc = QJsonDocument::fromJson(data, &jsonError);
553 if (jsonError.error != QJsonParseError::NoError) {
554 setError(tr("Unable to parse releases: "), jsonError, data);
555 return;
556 }
558#if !defined(QT_JSON_READONLY)
559 if (m_p->verbose) {
560 qDebug().noquote() << "Update check: found releases: " << QString::fromUtf8(replyDoc.toJson(QJsonDocument::Indented));
561 }
562#endif
563 // determine the release with the highest version (within the current page)
564 const auto replyArray = replyDoc.array();
565 const auto skipPreReleases = !(m_p->flags && UpdateCheckFlags::IncludePreReleases);
566 const auto skipDrafts = !(m_p->flags && UpdateCheckFlags::IncludeDrafts);
567 auto latestVersionFound = VersionAndSuffix();
568 auto latestVersionAssets = QJsonValue();
569 auto latestVersionAssetsUrl = QString();
570 auto latestVersionReleaseNotes = QString();
571 auto previousVersionAssets = QMap<QVersionNumber, std::variant<QJsonArray, QString>>();
572 for (const auto &releaseInfoVal : replyArray) {
573 const auto releaseInfo = releaseInfoVal.toObject();
574 const auto tag = releaseInfo.value(QLatin1String("tag_name")).toString();
575 if ((skipPreReleases && releaseInfo.value(QLatin1String("prerelease")).toBool())
576 || (skipDrafts && releaseInfo.value(QLatin1String("draft")).toBool())) {
577 qDebug() << "Update check: skipping prerelease/draft: " << tag;
578 continue;
579 }
580 const auto versionStr = tag.startsWith(QChar('v')) ? tag.mid(1) : tag;
581 const auto version = VersionAndSuffix::fromString(versionStr);
582 const auto assets = releaseInfo.value(QLatin1String("assets"));
583 const auto assetsUrl = releaseInfo.value(QLatin1String("assets_url")).toString();
584 if (!latestVersionFound || version > latestVersionFound) {
585 latestVersionFound = version;
586 latestVersionAssets = assets;
587 latestVersionAssetsUrl = assetsUrl;
588 latestVersionReleaseNotes = releaseInfo.value(QLatin1String("body")).toString();
589 }
590 if (assets.isArray()) {
591 previousVersionAssets[version.version] = assets.toArray();
592 } else if (!assetsUrl.isEmpty()) {
593 previousVersionAssets[version.version] = assetsUrl;
594 }
595 if (m_p->verbose) {
596 qDebug() << "Update check: skipping release: " << tag;
597 }
598 }
599 if (latestVersionFound) {
600 m_p->latestVersion = latestVersionFound.toString();
601 m_p->releaseNotes = latestVersionReleaseNotes;
602 previousVersionAssets.remove(latestVersionFound.version);
603 }
604 m_p->previousVersionAssets = previousVersionAssets.values();
605 // process assets for latest version
606 const auto foundUpdate = latestVersionFound && latestVersionFound > m_p->currentVersion;
607 if (foundUpdate) {
608 m_p->newVersion = latestVersionFound.toString();
609 }
610 if (latestVersionAssets.isArray()) {
611 return processAssets(latestVersionAssets.toArray(), foundUpdate, false);
612 } else if (foundUpdate) {
613 return queryRelease(latestVersionAssetsUrl, foundUpdate, false);
614 }
615 emit checkedForUpdate();
616 emit inProgressChanged(m_p->inProgress = false);
617#endif
618}
619
620void UpdateNotifier::readReleases()
621{
622#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
623 auto *const reply = static_cast<QNetworkReply *>(sender());
624 reply->deleteLater();
625 switch (reply->error()) {
626 case QNetworkReply::NoError: {
627 supplyNewReleaseData(reply->readAll());
628 break;
629 }
630 case QNetworkReply::OperationCanceledError:
631 emit inProgressChanged(m_p->inProgress = false);
632 return;
633 default:
634 setError(tr("Unable to request releases: "), reply);
635 }
636#endif
637}
638
639void UpdateNotifier::queryRelease(const QUrl &releaseUrl, bool forUpdate, bool forPreviousVersion)
640{
641#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
642 Q_UNUSED(releaseUrl)
643 Q_UNUSED(forUpdate)
644 Q_UNUSED(forPreviousVersion)
645#else
646 auto *const reply = m_p->nm->get(m_p->makeRequest(releaseUrl));
647 reply->setProperty("forUpdate", forUpdate);
648 reply->setProperty("forPreviousVersion", forPreviousVersion);
649 connect(reply, &QNetworkReply::finished, this, &UpdateNotifier::readRelease);
650#endif
651}
652
653void UpdateNotifier::readRelease()
654{
655#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
656 auto *const reply = static_cast<QNetworkReply *>(sender());
657 reply->deleteLater();
658 switch (reply->error()) {
659 case QNetworkReply::NoError: {
660 // parse JSON
661 auto jsonError = QJsonParseError();
662 const auto response = reply->readAll();
663 const auto replyDoc = QJsonDocument::fromJson(response, &jsonError);
664 if (jsonError.error != QJsonParseError::NoError) {
665 setError(tr("Unable to parse release: "), jsonError, response);
666 return;
667 }
668#if !defined(QT_JSON_READONLY)
669 if (m_p->verbose) {
670 qDebug().noquote() << "Update check: found release info: " << QString::fromUtf8(replyDoc.toJson(QJsonDocument::Indented));
671 }
672#endif
673 processAssets(replyDoc.object().value(QLatin1String("assets")).toArray(), reply->property("forUpdate").toBool(),
674 reply->property("forPreviousVersion").toBool());
675 break;
676 }
677 case QNetworkReply::OperationCanceledError:
678 emit inProgressChanged(m_p->inProgress = false);
679 return;
680 default:
681 setError(tr("Unable to request release: "), reply);
682 }
683#endif
684}
685
686void UpdateNotifier::processAssets(const QJsonArray &assets, bool forUpdate, bool forPreviousVersion)
687{
688#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
689 Q_UNUSED(assets)
690 Q_UNUSED(forUpdate)
691 Q_UNUSED(forPreviousVersion)
692#else
693 for (const auto &assetVal : assets) {
694 if (forPreviousVersion ? !m_p->previousVersionDownloadUrl.isEmpty() && !m_p->previousVersionSignatureUrl.isEmpty()
695 : !m_p->downloadUrl.isEmpty() && !m_p->signatureUrl.isEmpty()) {
696 break;
697 }
698 const auto asset = assetVal.toObject();
699 const auto assetName = asset.value(QLatin1String("name")).toString();
700 if (assetName.isEmpty()) {
701 continue;
702 }
703 if (!m_p->assetRegex.match(assetName).hasMatch()) {
704 if (m_p->verbose) {
705 qDebug() << "Update check: skipping asset: " << assetName;
706 }
707 continue;
708 }
709 const auto url = asset.value(QLatin1String("browser_download_url")).toString();
710 if (assetName.endsWith(QLatin1String(".sig"))) {
711 (forPreviousVersion ? m_p->previousVersionSignatureUrl : m_p->signatureUrl) = url;
712 } else {
713 (forPreviousVersion ? m_p->previousVersionDownloadUrl : m_p->downloadUrl) = url;
714 }
715 }
716 if (forUpdate) {
717 m_p->updateAvailable = !m_p->downloadUrl.isEmpty();
718 }
719 if (m_p->downloadUrl.isEmpty() && m_p->previousVersionDownloadUrl.isEmpty() && !m_p->previousVersionAssets.isEmpty()) {
720 auto previousVersionAssets = m_p->previousVersionAssets.takeLast();
721 if (std::holds_alternative<QJsonArray>(previousVersionAssets)) {
722 return processAssets(std::get<QJsonArray>(previousVersionAssets), forUpdate, true);
723 } else {
724 return queryRelease(std::get<QString>(previousVersionAssets), forUpdate, true);
725 }
726 }
727 emit checkedForUpdate();
728 emit inProgressChanged(m_p->inProgress = false);
729 if (forUpdate && m_p->updateAvailable && m_p->newVersion != m_p->previouslyFoundNewVersion) {
730 // emit updateAvailable() only if we not have already previously emitted it for this version
731 m_p->previouslyFoundNewVersion = m_p->newVersion;
732 emit updateAvailable(m_p->newVersion, m_p->additionalInfo);
733 }
734#endif
735}
736
737#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
738struct UpdaterPrivate {
739 QNetworkRequest makeRequest(const QUrl &url) const
740 {
741 return makeNetworkRequest(url, cacheLoadControl);
742 }
743
744 QNetworkAccessManager *nm = nullptr;
745 QFile *fakeDownload = nullptr;
746 QNetworkReply *currentDownload = nullptr;
747 QNetworkReply *signatureDownload = nullptr;
748 QNetworkRequest::CacheLoadControl cacheLoadControl = QNetworkRequest::PreferNetwork;
749 QString error, statusMessage;
750 QByteArray signature;
751 QFutureWatcher<QPair<QString, QString>> watcher;
752 QString executableName;
753 QString signatureExtension;
754 QRegularExpression executableRegex = QRegularExpression();
755 QString storedPath;
756 Updater::VerifyFunction verifyFunction;
757};
758#else
760 QString error;
761};
762#endif
763
764Updater::Updater(const QString &executableName, QObject *parent)
765 : Updater(executableName, QString(), parent)
766{
767}
768
769Updater::Updater(const QString &executableName, const QString &signatureExtension, QObject *parent)
770 : QObject(parent)
771 , m_p(std::make_unique<UpdaterPrivate>())
772{
773#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
774 Q_UNUSED(executableName)
775 Q_UNUSED(signatureExtension)
776#else
777 connect(&m_p->watcher, &QFutureWatcher<void>::finished, this, &Updater::concludeUpdate);
778 m_p->executableName = executableName;
779 m_p->signatureExtension = signatureExtension;
780 const auto signatureRegex = signatureExtension.isEmpty()
781 ? QString()
782 : QString(QStringLiteral("(") % QRegularExpression::escape(signatureExtension) % QStringLiteral(")?"));
783#ifdef QT_UTILITIES_EXE_REGEX
784 m_p->executableRegex = QRegularExpression(executableName % QStringLiteral(QT_UTILITIES_EXE_REGEX) % signatureRegex);
785#endif
786#endif
787}
788
792
794{
795#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
796 return m_p->currentDownload != nullptr || m_p->signatureDownload != nullptr || m_p->watcher.isRunning();
797#else
798 return false;
799#endif
800}
801
803{
804#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
805 return isInProgress() ? tr("Update in progress …") : (m_p->error.isEmpty() ? tr("Update done") : tr("Update failed"));
806#else
807 return QString();
808#endif
809}
810
811const QString &Updater::error() const
812{
813 return m_p->error;
814}
815
817{
818#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
819 return m_p->statusMessage.isEmpty() ? m_p->error : m_p->statusMessage;
820#else
821 return m_p->error;
822#endif
823}
824
826{
827#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
828 return m_p->storedPath;
829#else
830 static const auto empty = QString();
831 return empty;
832#endif
833}
834
835void Updater::setNetworkAccessManager(QNetworkAccessManager *nm)
836{
837#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
838 m_p->nm = nm;
839#else
840 Q_UNUSED(nm)
841#endif
842}
843
845{
846#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
847 m_p->verifyFunction = std::move(verifyFunction);
848#else
849 Q_UNUSED(verifyFunction)
850#endif
851}
852
853#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
854void Updater::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
855{
856 m_p->cacheLoadControl = cacheLoadControl;
857}
858#endif
859
860bool Updater::performUpdate(const QString &downloadUrl, const QString &signatureUrl)
861{
862#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
863 Q_UNUSED(downloadUrl)
864 Q_UNUSED(signatureUrl)
865 setError(tr("This build of the application does not support self-updating."));
866 return false;
867#else
868 if (isInProgress()) {
869 return false;
870 }
871 startDownload(downloadUrl, signatureUrl);
872 return true;
873#endif
874}
875
877{
878#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
879 if (m_p->currentDownload) {
880 m_p->currentDownload->abort();
881 }
882 if (m_p->signatureDownload) {
883 m_p->signatureDownload->abort();
884 }
885 if (m_p->watcher.isRunning()) {
886 m_p->watcher.cancel();
887 }
888#endif
889}
890
891void Updater::setError(const QString &error)
892{
893#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
894 m_p->statusMessage.clear();
895#endif
896 emit updateFailed(m_p->error = error);
897 emit updateStatusChanged(m_p->error);
898 emit updatePercentageChanged(0, 0);
899 emit inProgressChanged(false);
900}
901
902void Updater::startDownload(const QString &downloadUrl, const QString &signatureUrl)
903{
904#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
905 Q_UNUSED(downloadUrl)
906 Q_UNUSED(signatureUrl)
907#else
908 m_p->error.clear();
909 m_p->storedPath.clear();
910 m_p->signature.clear();
911
912 if (const auto fakeDownloadPath = qEnvironmentVariable(PROJECT_VARNAME_UPPER "_UPDATER_FAKE_DOWNLOAD"); !fakeDownloadPath.isEmpty()) {
913 m_p->fakeDownload = new QFile(fakeDownloadPath);
914 if (!m_p->fakeDownload->open(QFile::ReadOnly)) {
915 qWarning() << "Unable to open fake download file from:" << m_p->fakeDownload->errorString();
916 qDebug() << PROJECT_VARNAME_UPPER "_UPDATER_FAKE_DOWNLOAD was set to:" << fakeDownloadPath;
917 }
918 emit inProgressChanged(true);
919 storeExecutable();
920 return;
921 }
922
923 auto request = m_p->makeRequest(QUrl(downloadUrl));
924 m_p->statusMessage = tr("Downloading %1").arg(downloadUrl);
925 m_p->currentDownload = m_p->nm->get(request);
926 emit updateStatusChanged(m_p->statusMessage);
927 emit updatePercentageChanged(0, 0);
928 emit inProgressChanged(true);
929 connect(m_p->currentDownload, &QNetworkReply::finished, this, &Updater::handleDownloadFinished);
930 connect(m_p->currentDownload, &QNetworkReply::downloadProgress, this, &Updater::updatePercentageChanged);
931 if (!signatureUrl.isEmpty()) {
932 request.setUrl(signatureUrl);
933 m_p->signatureDownload = m_p->nm->get(request);
934 connect(m_p->signatureDownload, &QNetworkReply::finished, this, &Updater::handleDownloadFinished);
935 }
936#endif
937}
938
939void Updater::handleDownloadFinished()
940{
941#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
942 if (m_p->signatureDownload && !m_p->signatureDownload->isFinished()) {
943 emit updateStatusChanged(tr("Waiting for signature download …"));
944 emit updatePercentageChanged(0, 0);
945 return;
946 }
947 if (!m_p->currentDownload->isFinished()) {
948 return;
949 }
950
951 if (m_p->signatureDownload) {
952 readSignature();
953 m_p->signatureDownload->deleteLater();
954 m_p->signatureDownload = nullptr;
955 }
956
957 if (m_p->error.isEmpty()) {
958 storeExecutable();
959 } else {
960 m_p->currentDownload->deleteLater();
961 }
962 m_p->currentDownload = nullptr;
963#endif
964}
965
966void Updater::readSignature()
967{
968#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
969 switch (m_p->signatureDownload->error()) {
970 case QNetworkReply::NoError:
971 m_p->signature = m_p->signatureDownload->readAll();
972 break;
973 default:
974 setError(tr("Unable to download signature: ") + m_p->signatureDownload->errorString());
975 }
976#endif
977}
978
979void Updater::storeExecutable()
980{
981#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
982 m_p->statusMessage = tr("Extracting …");
983 emit updateStatusChanged(m_p->statusMessage);
984 emit updatePercentageChanged(0, 0);
985 auto *reply = static_cast<QIODevice *>(m_p->fakeDownload);
986 auto archiveName = QString();
987 auto hasError = false;
988 if (reply) {
989 archiveName = m_p->fakeDownload->fileName();
990 hasError = m_p->fakeDownload->error() != QFileDevice::NoError;
991 } else {
992 reply = m_p->currentDownload;
993 archiveName = m_p->currentDownload->request().url().fileName();
994 hasError = m_p->currentDownload->error() != QNetworkReply::NoError;
995 }
996 if (hasError) {
997 reply->deleteLater();
998 setError(tr("Unable to download update: ") + reply->errorString());
999 return;
1000 }
1001 auto res = QtConcurrent::run([this, reply, archiveName] {
1002 const auto data = reply->readAll();
1003 const auto dataView = std::string_view(data.data(), static_cast<std::size_t>(data.size()));
1004 auto foundExecutable = false, foundSignature = false;
1005 auto error = QString(), storePath = QString();
1006 auto newExeName = std::string(), signatureName = std::string();
1007 auto newExeData = std::string();
1008 auto newExe = QFile();
1009 reply->deleteLater();
1010
1011 // determine current executable path
1012 const auto appDirPath = QCoreApplication::applicationDirPath();
1013 const auto appFilePath = QCoreApplication::applicationFilePath();
1014 if (appDirPath.isEmpty() || appFilePath.isEmpty()) {
1015 error = tr("Unable to determine application path.");
1016 return QPair<QString, QString>(error, storePath);
1017 }
1018
1019 // handle cancellations
1020 const auto checkCancellation = [this, &error] {
1021 if (m_p->watcher.isCanceled()) {
1022 error = tr("Extraction was cancelled.");
1023 return true;
1024 } else {
1025 return false;
1026 }
1027 };
1028 if (checkCancellation()) {
1029 return QPair<QString, QString>(error, storePath);
1030 }
1031
1032 try {
1033 CppUtilities::walkThroughArchiveFromBuffer(
1034 dataView, archiveName.toStdString(),
1035 [this](const char *filePath, const char *fileName, mode_t mode) {
1036 Q_UNUSED(filePath)
1037 Q_UNUSED(mode)
1038 if (m_p->watcher.isCanceled()) {
1039 return true;
1040 }
1041 return m_p->executableRegex.match(QString::fromUtf8(fileName)).hasMatch();
1042 },
1043 [&](std::string_view path, CppUtilities::ArchiveFile &&file) {
1044 Q_UNUSED(path)
1045 if (checkCancellation()) {
1046 return true;
1047 }
1048 if (file.type != CppUtilities::ArchiveFileType::Regular) {
1049 return false;
1050 }
1051
1052 // read signature file
1053 const auto fileName = QString::fromUtf8(file.name.data(), static_cast<QString::size_type>(file.name.size()));
1054 if (!m_p->signatureExtension.isEmpty() && fileName.endsWith(m_p->signatureExtension)) {
1055 m_p->signature = QByteArray::fromStdString(file.content);
1056 foundSignature = true;
1057 signatureName = file.name;
1058 return foundExecutable;
1059 }
1060
1061 // skip signature files (that don't match m_p->signatureExtension but are present anyway)
1062 if (fileName.endsWith(QLatin1String(".sig"))) {
1063 return false;
1064 }
1065
1066 // write executable from archive to disk (using a temporary filename)
1067 foundExecutable = true;
1068 newExeName = file.name;
1069 newExe.setFileName(appDirPath % QChar('/') % fileName % QStringLiteral(".tmp"));
1070 if (!newExe.open(QFile::WriteOnly | QFile::Truncate)) {
1071 error = tr("Unable to create new executable under \"%1\": %2").arg(newExe.fileName(), newExe.errorString());
1072 return true;
1073 }
1074 const auto size = static_cast<qint64>(file.content.size());
1075 if (!(newExe.write(file.content.data(), size) == size) || !newExe.flush()) {
1076 error = tr("Unable to write new executable under \"%1\": %2").arg(newExe.fileName(), newExe.errorString());
1077 return true;
1078 }
1079 if (!newExe.setPermissions(
1080 newExe.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther)) {
1081 error = tr("Unable to make new binary under \"%1\" executable.").arg(newExe.fileName());
1082 return true;
1083 }
1084
1085 storePath = newExe.fileName();
1086 newExeData = std::move(file.content);
1087 return foundSignature || m_p->signatureExtension.isEmpty();
1088 });
1089 } catch (const CppUtilities::ArchiveException &e) {
1090 error = tr("Unable to open downloaded archive: %1").arg(e.what());
1091 }
1092 if (error.isEmpty() && foundExecutable) {
1093 // verify whether downloaded binary is valid if a verify function was assigned
1094 if (m_p->verifyFunction) {
1095 if (const auto verifyError = m_p->verifyFunction(Updater::Update{ .executableName = newExeName,
1096 .signatureName = signatureName,
1097 .data = newExeData,
1098 .signature = std::string_view(m_p->signature.data(), static_cast<std::size_t>(m_p->signature.size())) });
1099 !verifyError.isEmpty()) {
1100 error = tr("Unable to verify whether downloaded binary is valid: %1").arg(verifyError);
1101 return QPair<QString, QString>(error, storePath);
1102 }
1103 }
1104
1105 // rename current executable to keep it as backup
1106 auto currentExeInfo = QFileInfo(appFilePath);
1107 auto currentExe = QFile(appFilePath);
1108 const auto completeSuffix = currentExeInfo.completeSuffix();
1109 const auto suffixWithDot = completeSuffix.isEmpty() ? QString() : QChar('.') + completeSuffix;
1110 for (auto i = 0; i < 100; ++i) {
1111 const auto backupNumber = i ? QString::number(i) : QString();
1112 const auto backupPath = QString(currentExeInfo.path() % QChar('/') % currentExeInfo.baseName() % QStringLiteral("-backup")
1113 % backupNumber % QChar('-') % QString::fromUtf8(CppUtilities::applicationInfo.version) % suffixWithDot);
1114 if (QFile::exists(backupPath)) {
1115 continue;
1116 }
1117 if (!currentExe.rename(backupPath)) {
1118 error = tr("Unable to move current executable to \"%1\": %2").arg(backupPath, currentExe.errorString());
1119 return QPair<QString, QString>(error, storePath);
1120 }
1121 break;
1122 }
1123
1124 // rename new executable to use it in place of current executable
1125 if (!newExe.rename(appFilePath)) {
1126 error = tr("Unable to rename new executable \"%1\" to \"%2\": %3").arg(newExe.fileName(), appFilePath, newExe.errorString());
1127 return QPair<QString, QString>(error, storePath);
1128 }
1129 storePath = newExe.fileName();
1130 }
1131 if (error.isEmpty() && !foundExecutable) {
1132 error = tr("Unable to find executable in downloaded archive.");
1133 }
1134 return QPair<QString, QString>(error, storePath);
1135 });
1136 m_p->watcher.setFuture(std::move(res));
1137#endif
1138}
1139
1140void Updater::concludeUpdate()
1141{
1142#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1143 auto res = m_p->watcher.result();
1144 m_p->error = res.first;
1145 m_p->storedPath = res.second;
1146 if (!m_p->error.isEmpty()) {
1147 m_p->statusMessage.clear();
1148 emit updateFailed(m_p->error);
1149 } else {
1150 m_p->statusMessage = tr("Update stored under: %1").arg(m_p->storedPath);
1151 emit updateStored();
1152 }
1153 emit updateStatusChanged(statusMessage());
1154 emit updatePercentageChanged(0, 0);
1155 emit inProgressChanged(false);
1156#endif
1157}
1158
1160 explicit UpdateHandlerPrivate(const QString &executableName, const QString &signatureExtension)
1161 : updater(executableName.isEmpty() ? notifier.executableName() : executableName, signatureExtension)
1162 {
1163 }
1164
1167 QTimer timer;
1168 QSettings *settings;
1169 std::optional<UpdateHandler::CheckInterval> checkInterval;
1172};
1173
1174UpdateHandler *UpdateHandler::s_mainInstance = nullptr;
1175
1179UpdateHandler::UpdateHandler(QSettings *settings, QNetworkAccessManager *nm, QObject *parent)
1180 : QtUtilities::UpdateHandler(QString(), QString(), settings, nm, parent)
1181{
1182}
1183
1188 const QString &executableName, const QString &signatureExtension, QSettings *settings, QNetworkAccessManager *nm, QObject *parent)
1189 : QObject(parent)
1190 , m_p(std::make_unique<UpdateHandlerPrivate>(executableName, signatureExtension))
1191{
1192 m_p->notifier.setNetworkAccessManager(nm);
1193 m_p->updater.setNetworkAccessManager(nm);
1194 m_p->timer.setSingleShot(true);
1195 m_p->timer.setTimerType(Qt::VeryCoarseTimer);
1196 m_p->settings = settings;
1197 connect(&m_p->timer, &QTimer::timeout, &m_p->notifier, &UpdateNotifier::checkForUpdate);
1198 connect(&m_p->notifier, &UpdateNotifier::checkedForUpdate, this, &UpdateHandler::handleUpdateCheckDone);
1199}
1200
1204
1206{
1207 return &m_p->notifier;
1208}
1209
1210Updater *UpdateHandler::updater()
1211{
1212 return &m_p->updater;
1213}
1214
1216{
1217 if (m_p->checkInterval.has_value()) {
1218 return m_p->checkInterval.value();
1219 }
1220 m_p->settings->beginGroup(QStringLiteral("updating"));
1221 auto &checkInterval = m_p->checkInterval.emplace();
1222 checkInterval.duration = CppUtilities::TimeSpan::fromMilliseconds(m_p->settings->value("checkIntervalMs", 60 * 60 * 1000).toInt());
1223 checkInterval.enabled = m_p->settings->value("automaticChecksEnabled", false).toBool();
1224 m_p->settings->endGroup();
1225 return checkInterval;
1226}
1227
1229{
1230 m_p->checkInterval = checkInterval;
1231 m_p->settings->beginGroup(QStringLiteral("updating"));
1232 m_p->settings->setValue("checkIntervalMs", checkInterval.duration.totalMilliseconds());
1233 m_p->settings->setValue("automaticChecksEnabled", checkInterval.enabled);
1234 m_p->settings->endGroup();
1235#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1236 scheduleNextUpdateCheck();
1237#endif
1238}
1239
1241{
1242 return m_p->considerSeparateSignature;
1243}
1244
1245void UpdateHandler::setConsideringSeparateSignature(bool consideringSeparateSignature)
1246{
1247 m_p->considerSeparateSignature = consideringSeparateSignature;
1248}
1249
1251{
1252 auto error = QString();
1253#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1254 static const auto appDirPath = QCoreApplication::applicationDirPath();
1255 if (appDirPath.isEmpty()) {
1256 return tr("Unable to determine the application directory.");
1257 }
1258#if defined(Q_OS_WINDOWS) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
1259 const auto permissionGuard = QNtfsPermissionCheckGuard();
1260#endif
1261 const auto dirInfo = QFileInfo(appDirPath);
1262 if (!dirInfo.isWritable()) {
1263 return tr("The directory where the executable is stored (%1) is not writable.").arg(appDirPath);
1264 }
1265#endif
1266 return error;
1267}
1268
1269#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1270void UpdateHandler::setCacheLoadControl(QNetworkRequest::CacheLoadControl cacheLoadControl)
1271{
1272 m_p->notifier.setCacheLoadControl(cacheLoadControl);
1273 m_p->updater.setCacheLoadControl(cacheLoadControl);
1274}
1275#endif
1276
1278{
1279#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1280 m_p->notifier.restore(m_p->settings);
1281 scheduleNextUpdateCheck();
1282#endif
1283}
1284
1286{
1287 const auto &downloadUrl = !m_p->notifier.downloadUrl().isEmpty() ? m_p->notifier.downloadUrl() : m_p->notifier.previousVersionDownloadUrl();
1288 const auto &signatureUrl = !m_p->notifier.downloadUrl().isEmpty() ? m_p->notifier.signatureUrl() : m_p->notifier.previousVersionSignatureUrl();
1289 m_p->updater.performUpdate(downloadUrl.toString(), m_p->considerSeparateSignature ? signatureUrl.toString() : QString());
1290}
1291
1293{
1294#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1295 m_p->notifier.save(m_p->settings);
1296#endif
1297}
1298
1299void UpdateHandler::handleUpdateCheckDone()
1300{
1301#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1303 scheduleNextUpdateCheck();
1304#endif
1305}
1306
1307#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1308void UpdateHandler::scheduleNextUpdateCheck()
1309{
1310 m_p->timer.stop();
1311
1312 const auto &interval = checkInterval();
1313 if (!interval.enabled || (interval.duration.isNull() && m_p->hasCheckedOnceSinceStartup)) {
1314 return;
1315 }
1316 const auto timeLeft = interval.duration - (CppUtilities::DateTime::now() - m_p->notifier.lastCheck());
1317 std::cerr << CppUtilities::EscapeCodes::Phrases::Info
1318 << "Check for updates due in: " << timeLeft.toString(CppUtilities::TimeSpanOutputFormat::WithMeasures)
1319 << CppUtilities::EscapeCodes::Phrases::End;
1320 m_p->hasCheckedOnceSinceStartup = true; // the attempt counts
1321 m_p->timer.start(std::max(1000, static_cast<int>(timeLeft.totalMilliseconds())));
1322}
1323#endif
1324
1326{
1327 m_restartRequested = true;
1328#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1329 QCoreApplication::quit();
1330#endif
1331}
1332
1334{
1335#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1336 if (!m_restartRequested) {
1337 return;
1338 }
1339 auto *const process = new QProcess(QCoreApplication::instance());
1340 auto args = QCoreApplication::arguments();
1341 args.removeFirst();
1342 process->setProgram(QCoreApplication::applicationFilePath());
1343 process->setArguments(args);
1344 process->startDetached();
1345#endif
1346}
1347
1348#ifdef QT_UTILITIES_GUI_QTWIDGETS
1349struct UpdateOptionPagePrivate {
1350 UpdateOptionPagePrivate(UpdateHandler *updateHandler)
1351 : updateHandler(updateHandler)
1352 {
1353 }
1354 UpdateHandler *updateHandler = nullptr;
1355 std::function<void()> restartHandler;
1356};
1357
1358UpdateOptionPage::UpdateOptionPage(UpdateHandler *updateHandler, QWidget *parentWidget)
1359 : UpdateOptionPageBase(parentWidget)
1360#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1361 , m_p(std::make_unique<UpdateOptionPagePrivate>(updateHandler))
1362#endif
1363{
1364#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
1365 Q_UNUSED(updateHandler)
1366#endif
1367}
1368
1369UpdateOptionPage::~UpdateOptionPage()
1370{
1371}
1372
1373void UpdateOptionPage::setRestartHandler(std::function<void()> &&handler)
1374{
1375 m_p->restartHandler = std::move(handler);
1376#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1377 if (ui() && m_p->restartHandler) {
1378 QObject::connect(ui()->restartPushButton, &QPushButton::clicked, widget(), m_p->restartHandler);
1379 }
1380#endif
1381}
1382
1383bool UpdateOptionPage::apply()
1384{
1385#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1386 if (!m_p->updateHandler) {
1387 return true;
1388 }
1389 m_p->updateHandler->setCheckInterval(UpdateHandler::CheckInterval{
1390 .duration = CppUtilities::TimeSpan::fromMinutes(ui()->checkIntervalSpinBox->value()), .enabled = ui()->enabledCheckBox->isChecked() });
1391 auto flags = UpdateCheckFlags::None;
1392 CppUtilities::modFlagEnum(flags, UpdateCheckFlags::IncludePreReleases, ui()->preReleasesCheckBox->isChecked());
1393 CppUtilities::modFlagEnum(flags, UpdateCheckFlags::IncludeDrafts, ui()->draftsCheckBox->isChecked());
1394 m_p->updateHandler->notifier()->setFlags(flags);
1395 m_p->updateHandler->saveNotifierState();
1396#endif
1397 return true;
1398}
1399
1400void UpdateOptionPage::reset()
1401{
1402#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1403 if (!m_p->updateHandler) {
1404 return;
1405 }
1406 const auto &checkInterval = m_p->updateHandler->checkInterval();
1407 ui()->checkIntervalSpinBox->setValue(static_cast<int>(checkInterval.duration.totalMinutes()));
1408 ui()->enabledCheckBox->setChecked(checkInterval.enabled);
1409 const auto flags = m_p->updateHandler->notifier()->flags();
1410 ui()->preReleasesCheckBox->setChecked(flags && UpdateCheckFlags::IncludePreReleases);
1411 ui()->draftsCheckBox->setChecked(flags && UpdateCheckFlags::IncludeDrafts);
1412#endif
1413}
1414
1415#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1416static QString formatReleaseNotes(const QString &version, const QString &releaseNotes)
1417{
1418 auto res = QCoreApplication::translate("QtGui::UpdateOptionPage", "**Release notes of version %1:**\n\n").arg(version) + releaseNotes;
1419
1420 // ensure links like "https://github.com/…/compare/v2.0.0...v2.0.1" are not cut short at the first "."
1421 static const auto re = QRegularExpression(R"(https://github\.com/[^\s)]+)");
1422 static constexpr auto replacementLengthDiff = QString::difference_type(2);
1423 auto offset = QString::size_type();
1424 for (auto it = re.globalMatch(res); it.hasNext(); offset += replacementLengthDiff) {
1425 const auto match = it.next();
1426 const auto replacement = QChar('<') % match.captured(0) % QChar('>');
1427 res.replace(match.capturedStart() + offset, match.capturedLength(), replacement);
1428 }
1429
1430 return res;
1431}
1432#endif
1433
1434QWidget *UpdateOptionPage::setupWidget()
1435{
1436#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1437 if (m_p->updateHandler && m_p->updateHandler->notifier()->isSupported()) {
1438 auto *const widget = UpdateOptionPageBase::setupWidget(); // call base implementation first, so ui() is available
1439 ui()->versionInUseValueLabel->setText(QString::fromUtf8(CppUtilities::applicationInfo.version));
1440 ui()->updateWidget->hide();
1441 ui()->releaseNotesPushButton->hide();
1442 updateLatestVersion();
1443 QObject::connect(ui()->checkNowPushButton, &QPushButton::clicked, m_p->updateHandler->notifier(), &UpdateNotifier::checkForUpdate);
1444 QObject::connect(ui()->updatePushButton, &QPushButton::clicked, widget, [this, widget] {
1445 if (const auto preCheckError = m_p->updateHandler->preCheck(); preCheckError.isEmpty()
1446 || QMessageBox::critical(widget, QCoreApplication::applicationName(),
1447 QCoreApplication::translate("QtGui::UpdateOptionPage", "<p>%1</p><p><strong>Try the update nevertheless?</strong></p>")
1448 .arg(preCheckError),
1449 QMessageBox::Yes | QMessageBox::No)
1450 == QMessageBox::Yes) {
1451 m_p->updateHandler->performUpdate();
1452 }
1453 });
1454 QObject::connect(ui()->abortUpdatePushButton, &QPushButton::clicked, m_p->updateHandler->updater(), &Updater::abortUpdate);
1455 if (m_p->restartHandler) {
1456 QObject::connect(ui()->restartPushButton, &QPushButton::clicked, widget, m_p->restartHandler);
1457 }
1458 QObject::connect(ui()->releaseNotesPushButton, &QPushButton::clicked, widget, [this, widget] {
1459 const auto *const notifier = m_p->updateHandler->notifier();
1460 auto infobox = QMessageBox(widget);
1461 infobox.setWindowTitle(QCoreApplication::applicationName());
1462 infobox.setIcon(QMessageBox::Information);
1463 infobox.setText(formatReleaseNotes(notifier->latestVersion(), notifier->releaseNotes()));
1464#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
1465 infobox.setTextFormat(Qt::MarkdownText);
1466#else
1467 infobox.setTextFormat(Qt::PlainText);
1468#endif
1469 infobox.exec();
1470 });
1471 QObject::connect(
1472 m_p->updateHandler->notifier(), &UpdateNotifier::inProgressChanged, widget, [this](bool inProgress) { updateLatestVersion(inProgress); });
1473 QObject::connect(m_p->updateHandler->updater(), &Updater::inProgressChanged, widget, [this](bool inProgress) {
1474 const auto *const updater = m_p->updateHandler->updater();
1475 ui()->updateWidget->setVisible(true);
1476 ui()->updateInProgressLabel->setText(updater->overallStatus());
1477 ui()->updateProgressBar->setVisible(inProgress);
1478 ui()->abortUpdatePushButton->setVisible(inProgress);
1479 ui()->restartPushButton->setVisible(!inProgress && !updater->storedPath().isEmpty() && updater->error().isEmpty());
1480 });
1481 QObject::connect(m_p->updateHandler->updater(), &Updater::updateStatusChanged, widget,
1482 [this](const QString &statusMessage) { ui()->updateStatusLabel->setText(statusMessage); });
1483 QObject::connect(m_p->updateHandler->updater(), &Updater::updatePercentageChanged, widget, [this](qint64 bytesReceived, qint64 bytesTotal) {
1484 if (bytesTotal == 0) {
1485 ui()->updateProgressBar->setMaximum(0);
1486 } else {
1487 ui()->updateProgressBar->setValue(static_cast<int>(bytesReceived * 100 / bytesTotal));
1488 ui()->updateProgressBar->setMaximum(100);
1489 }
1490 });
1491 return widget;
1492 }
1493#endif
1494
1495 auto *const label = new QLabel;
1496 label->setWindowTitle(QCoreApplication::translate("QtGui::UpdateOptionPage", "Updating"));
1497 label->setAlignment(Qt::AlignCenter);
1498 label->setWordWrap(true);
1499#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1500 label->setText(QCoreApplication::translate("QtUtilities::UpdateOptionPage", "Checking for updates is not supported on this platform."));
1501#else
1502 label->setText(QCoreApplication::translate("QtUtilities::UpdateOptionPage",
1503 "This build of %1 has automatic updates disabled. You may update the application in an automated way via your package manager, though.")
1504 .arg(CppUtilities::applicationInfo.name));
1505#endif
1506 return label;
1507}
1508
1509void UpdateOptionPage::updateLatestVersion(bool)
1510{
1511#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1512 if (!m_p->updateHandler) {
1513 return;
1514 }
1515 const auto &notifier = *m_p->updateHandler->notifier();
1516 const auto &downloadUrl = notifier.downloadUrl();
1517 const auto &previousVersionDownloadUrl = notifier.previousVersionDownloadUrl();
1518 const auto downloadUrlEscaped = downloadUrl.toString().toHtmlEscaped();
1519 const auto previousVersionDownloadUrlEscaped = previousVersionDownloadUrl.toString().toHtmlEscaped();
1520 ui()->latestVersionValueLabel->setText(notifier.status());
1521 ui()->downloadUrlLabel->setText(downloadUrl.isEmpty()
1522 ? (notifier.latestVersion().isEmpty()
1523 ? QCoreApplication::translate("QtUtilities::UpdateOptionPage", "no new version available for download")
1524 : (QCoreApplication::translate("QtUtilities::UpdateOptionPage", "latest version provides no build for the current platform yet")
1525 + (previousVersionDownloadUrl.isEmpty()
1526 ? QString()
1527 : QString(QStringLiteral("<br>")
1528 % QCoreApplication::translate("QtUtilities::UpdateOptionPage", "for latest build: ")
1529 % QStringLiteral("<a href=\"") % previousVersionDownloadUrlEscaped % QStringLiteral("\">")
1530 % previousVersionDownloadUrlEscaped % QStringLiteral("</a>")))))
1531 : (QStringLiteral("<a href=\"") % downloadUrlEscaped % QStringLiteral("\">") % downloadUrlEscaped % QStringLiteral("</a>")));
1532 ui()->updatePushButton->setText(!downloadUrl.isEmpty() || previousVersionDownloadUrl.isEmpty()
1533 ? QCoreApplication::translate("QtUtilities::UpdateOptionPage", "Update to latest version")
1534 : QCoreApplication::translate("QtUtilities::UpdateOptionPage", "Update to latest available build"));
1535 ui()->updatePushButton->setDisabled(downloadUrl.isEmpty() && previousVersionDownloadUrl.isEmpty());
1536 ui()->releaseNotesPushButton->setHidden(notifier.releaseNotes().isEmpty());
1537#endif
1538}
1539
1540VerificationErrorMessageBox::VerificationErrorMessageBox()
1541{
1542#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1543 setWindowTitle(QCoreApplication::applicationName());
1544 setStandardButtons(QMessageBox::Cancel | QMessageBox::Ignore);
1545 setDefaultButton(QMessageBox::Cancel);
1546 setIcon(QMessageBox::Critical);
1547#endif
1548}
1549
1550VerificationErrorMessageBox::~VerificationErrorMessageBox()
1551{
1552}
1553
1554int VerificationErrorMessageBox::execForError(QString &errorMessage, const QString &explanation)
1555{
1556#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1557 auto loop = QEventLoop();
1558 QObject::connect(this, &QDialog::finished, &loop, &QEventLoop::exit);
1559 QMetaObject::invokeMethod(this, "openForError", Qt::QueuedConnection, Q_ARG(QString, errorMessage), Q_ARG(QString, explanation));
1560 auto res = loop.exec();
1561 if (res == QMessageBox::Ignore) {
1562 errorMessage.clear();
1563 }
1564 return res;
1565#else
1566 Q_UNUSED(errorMessage)
1567 Q_UNUSED(explanation)
1568 return 0;
1569#endif
1570}
1571
1572void VerificationErrorMessageBox::openForError(const QString &errorMessage, const QString &explanation)
1573{
1574#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
1575 setText(tr("<p>The signature of the downloaded executable could not be verified: %1</p>").arg(errorMessage) + explanation);
1576 open();
1577#else
1578 Q_UNUSED(errorMessage)
1579 Q_UNUSED(explanation)
1580#endif
1581}
1582
1583struct UpdateDialogPrivate {
1584 UpdateOptionPage *updateOptionPage = nullptr;
1585};
1586
1587UpdateDialog::UpdateDialog(QWidget *parent)
1588 : SettingsDialog(parent)
1589 , m_p(std::make_unique<UpdateDialogPrivate>())
1590{
1591 auto *const category = new OptionCategory;
1592 m_p->updateOptionPage = new UpdateOptionPage(UpdateHandler::mainInstance(), this);
1593 category->assignPages({ m_p->updateOptionPage });
1594 setWindowTitle(m_p->updateOptionPage->widget()->windowTitle());
1595 setTabBarAlwaysVisible(false);
1596 setSingleCategory(category);
1597}
1598
1599UpdateDialog::~UpdateDialog()
1600{
1601}
1602
1603UpdateOptionPage *UpdateDialog::page()
1604{
1605 return m_p->updateOptionPage;
1606}
1607
1608const UpdateOptionPage *UpdateDialog::page() const
1609{
1610 return m_p->updateOptionPage;
1611}
1612
1613#endif
1614
1615} // namespace QtUtilities
1616
1617#if defined(QT_UTILITIES_GUI_QTWIDGETS)
1619#endif
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 ...
Definition updater.h:189
bool isConsideringSeparateSignature() const
Definition updater.cpp:1240
static UpdateHandler * mainInstance()
Definition updater.h:239
void setConsideringSeparateSignature(bool consideringSeparateSignature)
Definition updater.cpp:1245
UpdateHandler(QSettings *settings, QNetworkAccessManager *nm, QObject *parent=nullptr)
Handles checking for updates and performing an update of the application if available.
Definition updater.cpp:1179
const CheckInterval & checkInterval() const
Definition updater.cpp:1215
UpdateNotifier * notifier
Definition updater.h:191
QString preCheck() const
Definition updater.cpp:1250
void setCheckInterval(CheckInterval checkInterval)
Definition updater.cpp:1228
The UpdateNotifier class allows checking for new updates.
Definition updater.h:61
void setFlags(UpdateCheckFlags flags)
Definition updater.cpp:283
void setNetworkAccessManager(QNetworkAccessManager *nm)
Definition updater.cpp:455
UpdateNotifier(QObject *parent=nullptr)
Definition updater.cpp:202
void save(QSettings *settings)
Definition updater.cpp:415
bool isUpdateAvailable() const
Definition updater.cpp:265
void inProgressChanged(bool inProgress)
void supplyNewReleaseData(const QByteArray &data)
Definition updater.cpp:545
UpdateCheckFlags flags() const
Definition updater.cpp:274
const QString & latestVersion() const
Definition updater.cpp:312
void restore(QSettings *settings)
Definition updater.cpp:396
CppUtilities::DateTime lastCheck() const
Definition updater.cpp:387
The Updater class allows downloading and applying an update.
Definition updater.h:131
Updater(const QString &executableName, QObject *parent=nullptr)
Definition updater.cpp:764
void updatePercentageChanged(qint64 bytesReceived, qint64 bytesTotal)
void updateStatusChanged(const QString &statusMessage)
void updateFailed(const QString &error)
QString overallStatus
Definition updater.h:134
std::function< QString(const Update &)> VerifyFunction
Definition updater.h:146
QString statusMessage
Definition updater.h:136
bool performUpdate(const QString &downloadUrl, const QString &signatureUrl)
Definition updater.cpp:860
void setVerifier(VerifyFunction &&verifyFunction)
Definition updater.cpp:844
void inProgressChanged(bool inProgress)
void setNetworkAccessManager(QNetworkAccessManager *nm)
Definition updater.cpp:835
~Updater() override
Definition updater.cpp:789
bool isInProgress() const
Definition updater.cpp:793
qsizetype VersionSuffixIndex
Definition updater.cpp:108
#define INSTANTIATE_UI_FILE_BASED_OPTION_PAGE(SomeClass)
Instantiates a class declared with BEGIN_DECLARE_UI_FILE_BASED_OPTION_PAGE in a convenient way.
Definition optionpage.h:250
UpdateHandlerPrivate(const QString &executableName, const QString &signatureExtension)
Definition updater.cpp:1160
std::optional< UpdateHandler::CheckInterval > checkInterval
Definition updater.cpp:1169
The CheckInterval struct specifies whether automatic checks for updates are enabled and of often they...
Definition updater.h:196
#define QT_UTILITIES_EXE_REGEX
Definition updater.cpp:82
#define QT_UTILITIES_VERSION_SUFFIX
Definition updater.cpp:74