wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
UpdateController.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QCheckBox>
7#include <QDebug>
8#include <QDesktopServices>
9#include <QDialog>
10#include <QDialogButtonBox>
11#include <QDir>
12#include <QFile>
13#include <QLabel>
14#include <QMessageBox>
15#include <QNetworkAccessManager>
16#include <QNetworkReply>
17#include <QNetworkRequest>
18#include <QPointer>
19#include <QProgressDialog>
20#include <QPushButton>
21#include <QSslError>
22#include <QStandardPaths>
23#include <QVBoxLayout>
24
26#include "App/Core/Settings.h"
28#include "App/Versions.h"
29
31 : QObject(parent)
32 , m_parent(parent)
33{
34}
35
37{
39 return;
40 }
41
42 auto *updateChecker = new UpdateChecker(this);
43 connect(updateChecker, &UpdateChecker::updateAvailable, this, &UpdateController::showUpdateDialog);
44 updateChecker->checkForUpdates();
45}
46
47void UpdateController::showUpdateDialog(const QString &latestVersion, const QUrl &downloadUrl, const QUrl &releaseUrl)
48{
49 QDialog dialog(m_parent);
50 dialog.setWindowTitle(tr("Update Available"));
51 dialog.setWindowModality(Qt::WindowModal);
52
53 auto *layout = new QVBoxLayout(&dialog);
54
55 const bool hasDirectDownload = downloadUrl.isValid() && !downloadUrl.isEmpty();
56 auto *label = new QLabel(
57 (hasDirectDownload
58 ? tr("<b>wiRedPanda %1 is available.</b><br><br>"
59 "You are currently running version %2.<br>"
60 "Click <b>Download</b> to save the new version to your computer.")
61 : tr("<b>wiRedPanda %1 is available.</b><br><br>"
62 "You are currently running version %2.<br>"
63 "Visit the release page to download the new version."))
64 .arg(latestVersion, APP_VERSION),
65 &dialog);
66 label->setTextFormat(Qt::RichText);
67 label->setWordWrap(true);
68 layout->addWidget(label);
69
70 auto *skipCheckBox = new QCheckBox(tr("Don't notify me about this version again"), &dialog);
71 layout->addWidget(skipCheckBox);
72
73 auto *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
74 auto *downloadButton = buttonBox->addButton(tr("Download"), QDialogButtonBox::AcceptRole);
75 connect(downloadButton, &QPushButton::clicked, &dialog, [&dialog] { dialog.accept(); });
76 connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
77 layout->addWidget(buttonBox);
78
79 const bool accepted = dialog.exec() == QDialog::Accepted;
80
81 if (skipCheckBox->isChecked()) {
83 }
84
85 // The check date is recorded by UpdateChecker::onReplyFinished — the
86 // single writer — so no dialog outcome needs to touch it here.
87 if (accepted) {
88 if (hasDirectDownload) {
89 downloadUpdate(latestVersion, downloadUrl);
90 } else {
91 QDesktopServices::openUrl(releaseUrl);
92 }
93 }
94}
95
96void UpdateController::downloadUpdate(const QString &latestVersion, const QUrl &url)
97{
98 const QString fileName = url.fileName();
99 const QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
100 // Not guaranteed to already exist -- e.g. a minimal/headless environment with no
101 // pre-populated XDG user-dirs -- and QFile::open(WriteOnly) doesn't create missing
102 // parent directories.
103 QDir().mkpath(downloadDir);
104 const QString savePath = QDir(downloadDir).filePath(fileName);
105
106 // The progress dialog below is not window-modal, so the main window stays interactive and
107 // Check for Updates can be triggered again mid-download. Raise the existing download
108 // rather than starting a second one onto the same path.
109 if (m_progressDialog) {
110 m_progressDialog->raise();
111 m_progressDialog->activateWindow();
112 return;
113 }
114
115 auto *progress = new QProgressDialog(tr("Downloading wiRedPanda %1…").arg(latestVersion), tr("Cancel"), 0, 100, m_parent);
116 progress->setWindowTitle(tr("Downloading Update"));
117 // Deliberately NOT window-modal. QProgressDialog::setValue() calls processEvents() when the
118 // dialog is modal, so driving it from downloadProgress re-enters the event loop: that nested
119 // pump delivers finished(), which tears this dialog down, and setValue() then resumes on its
120 // own freed QProgressBar. Cancel still works -- it is a button on this dialog, not something
121 // modality provides.
122 progress->setMinimumDuration(0);
123 progress->setValue(0);
124
125 auto *network = new QNetworkAccessManager(this);
126 // QNetworkAccessManager::sslErrors doesn't exist at all when Qt is built with QT_NO_SSL --
127 // the case for every Qt-for-WebAssembly build, since WASM has no native TLS backend and
128 // routes network access through the browser instead (any TLS failure there surfaces as a
129 // generic QNetworkReply error, not through this signal).
130#ifndef QT_NO_SSL
131 connect(network, &QNetworkAccessManager::sslErrors, this, [](QNetworkReply *reply, const QList<QSslError> &errors) {
132 qWarning() << "MainWindow::downloadUpdate: SSL errors, aborting reply:" << errors;
133 reply->abort();
134 });
135#endif
136
137 QNetworkRequest request(url);
138 request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
139 request.setTransferTimeout(60000);
140 QNetworkReply *reply = network->get(request);
141
142 // Belt and braces around the non-modality above: the connection is severed the moment the
143 // download finishes, and the dialog is reached through a QPointer so a delivery after its
144 // destruction is skipped rather than fatal. Neither is sufficient on its own -- a guard
145 // cannot help once the object dies inside the very setValue() call the guard permitted,
146 // which is exactly what modality caused.
147 const QPointer<QProgressDialog> progressGuard(progress);
148 m_progressDialog = progress;
149 const QMetaObject::Connection progressConnection =
150 connect(reply, &QNetworkReply::downloadProgress, progress, [progressGuard](qint64 received, qint64 total) {
151 if (total > 0 && progressGuard) {
152 progressGuard->setValue(static_cast<int>(received * 100 / total));
153 }
154 });
155
156 connect(progress, &QProgressDialog::canceled, reply, &QNetworkReply::abort);
157
158 connect(reply, &QNetworkReply::finished, this, [this, reply, progressGuard, progressConnection, savePath] {
159 // Before the dialog goes away, and before any modal box below opens a nested loop.
160 disconnect(progressConnection);
161 if (progressGuard) {
162 progressGuard->close();
163 progressGuard->deleteLater();
164 }
165
166 if (reply->error() != QNetworkReply::NoError) {
167 if (reply->error() != QNetworkReply::OperationCanceledError) {
168 QMessageBox::warning(m_parent, tr("Download Failed"), tr("Could not download the update:\n%1").arg(reply->errorString()));
169 }
170 reply->deleteLater();
171 return;
172 }
173
174 QFile file(savePath);
175 if (!file.open(QIODevice::WriteOnly)) {
176 QMessageBox::warning(m_parent, tr("Download Failed"), tr("Could not save the file:\n%1").arg(savePath));
177 reply->deleteLater();
178 return;
179 }
180 const QByteArray payload = reply->readAll();
181 if (file.write(payload) != payload.size()) {
182 QMessageBox::warning(m_parent, tr("Download Failed"), tr("Could not write the file:\n%1").arg(savePath));
183 reply->deleteLater();
184 return;
185 }
186 file.close();
187 reply->deleteLater();
188
189 QMessageBox::information(m_parent, tr("Download Complete"),
190 tr("wiRedPanda has been downloaded to:\n%1").arg(savePath));
191 });
192}
Custom QApplication subclass with exception handling and main-window access.
Typed wrappers around QSettings for all application preferences.
Checks the wiRedPanda site's published release data for newer versions.
UpdateController: drives the application's check-for-updates workflow.
File-format version constants and application version accessor.
static bool interactiveMode
Definition Application.h:81
static void setUpdateCheckSkippedVersion(const QString &version)
Definition Settings.cpp:225
Asynchronously queries the site's release data and emits a signal when a newer version is available.
void updateAvailable(const QString &latestVersion, const QUrl &downloadUrl, const QUrl &releaseUrl)
Emitted when a newer release is available and has not been suppressed.
void checkForUpdates()
Starts an asynchronous version check; shows the update dialog if one is available.
UpdateController(QWidget *parent)