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