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