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