Cockatrice/cockatrice/src/interface/widgets/dialogs/dlg_update.cpp
Lukas Brübach 561cadd217
[Client] Only quit when the main window close is accepted
Wait for MainWindow::close() to be accepted before quitting after the
update installer is launched. When the close is vetoed (a running card DB
update, open games, or an unsaved deck), keep running and tell the user
the installer is already waiting, instead of exiting over their answer.

Also fix the comment so it does not claim settings are saved on the vetoed
path.
2026-09-21 09:13:20 +02:00

276 lines
11 KiB
C++

#include "dlg_update.h"
#include "../../../client/settings/cache_settings.h"
#include "../client/network/update/client/client_update_checker.h"
#include "../client/network/update/client/release_channel.h"
#include "../interface/window_main.h"
#include <QCoreApplication>
#include <QDesktopServices>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <QtNetwork>
#include <version_string.h>
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
{
// Handle layout
statusLabel = new QLabel(this);
statusLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
statusLabel->setWordWrap(true);
descriptionLabel =
new QLabel(tr("Current release channel") +
QString(": %1").arg(tr(SettingsCache::instance().getUpdateReleaseChannel()->getName().toUtf8())),
this);
progress = new QProgressBar(this);
buttonBox = new QDialogButtonBox(this);
buttonBox->setFixedWidth(350);
ok = new QPushButton("Close", this);
manualDownload = new QPushButton(tr("Reinstall"), this);
stopDownload = new QPushButton(tr("Cancel Download"), this);
gotoDownload = new QPushButton(tr("Open Download Page"), this);
addStopDownloadAndRemoveOthers(false); // Add all buttons to box
enableUpdateButton(false); // Unless we know there's an update available, you can't install
buttonBox->addButton(ok, QDialogButtonBox::AcceptRole);
connect(gotoDownload, &QPushButton::clicked, this, &DlgUpdate::gotoDownloadPage);
//! \todo Make reinstall button actually do something when clicked.
// connect(manualDownload, &QPushButton::clicked, this, &DlgUpdate::downloadUpdate);
connect(stopDownload, &QPushButton::clicked, this, &DlgUpdate::cancelDownload);
connect(ok, &QPushButton::clicked, this, &DlgUpdate::closeDialog);
auto *parentLayout = new QVBoxLayout(this);
parentLayout->addWidget(descriptionLabel);
parentLayout->addWidget(statusLabel);
parentLayout->addWidget(progress);
parentLayout->addWidget(buttonBox);
setLayout(parentLayout);
setWindowTitle(tr("Check for Client Updates"));
setFixedHeight(this->sizeHint().height());
setFixedWidth(this->sizeHint().width());
// Check for SSL (this probably isn't necessary)
if (!QSslSocket::supportsSsl()) {
enableUpdateButton(false);
QMessageBox::critical(this, tr("Error"),
tr("Cockatrice was not built with SSL support, therefore you cannot download updates "
"automatically! \nPlease visit the download page to update manually."));
}
// Initialize the checker and downloader class
uDownloader = new UpdateDownloader(this);
connect(uDownloader, &UpdateDownloader::downloadSuccessful, this, &DlgUpdate::downloadSuccessful);
connect(uDownloader, &UpdateDownloader::progressMade, this, &DlgUpdate::downloadProgressMade);
connect(uDownloader, &UpdateDownloader::error, this, &DlgUpdate::downloadError);
// Check for updates
beginUpdateCheck();
}
void DlgUpdate::closeDialog()
{
accept();
}
void DlgUpdate::gotoDownloadPage()
{
QDesktopServices::openUrl(SettingsCache::instance().getUpdateReleaseChannel()->getManualDownloadUrl());
}
void DlgUpdate::downloadUpdate(const QString &releaseName)
{
setLabel(tr("Downloading update: %1").arg(releaseName));
addStopDownloadAndRemoveOthers(true); // Will remove all other buttons
uDownloader->beginDownload(updateUrl);
}
void DlgUpdate::cancelDownload()
{
emit uDownloader->stopDownload();
setLabel("Download canceled");
addStopDownloadAndRemoveOthers(false);
downloadProgressMade(0, 1);
}
void DlgUpdate::beginUpdateCheck()
{
progress->setMinimum(0);
progress->setMaximum(0);
setLabel(tr("Checking for updates..."));
auto checker = new ClientUpdateChecker(this);
connect(checker, &ClientUpdateChecker::finishedCheck, this, &DlgUpdate::finishedUpdateCheck);
connect(checker, &ClientUpdateChecker::error, this, &DlgUpdate::updateCheckError);
checker->check();
}
void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Release *release)
{
QString publishDate, versionName;
// Update the UI to say we've finished
progress->setMaximum(100);
setLabel(tr("Finished checking for updates"));
// If there are no available builds, then they can't auto update.
enableUpdateButton(isCompatible);
if (isCompatible) {
// If there is an update, save its URL and work out its name
updateUrl = release->getDownloadUrl();
}
// Give the user the appropriate message
if (!needToUpdate) {
// If there's no need to update, tell them that. However we still allow them to run the
// downloader themselves if there's a compatible build
QMessageBox::information(
window(), tr("No Update Available"),
tr("Cockatrice is up to date!") + "<br><br>" +
tr("You are already running the latest version available in the chosen release channel.") + "<br>" +
"<b>" + tr("Current version") + QString(":</b> %1<br>").arg(VERSION_STRING) + "<b>" +
tr("Selected release channel") +
QString(":</b> %1").arg(tr(SettingsCache::instance().getUpdateReleaseChannel()->getName().toUtf8())));
return;
}
publishDate = release->getPublishDate().toString(QLocale().dateFormat(QLocale::LongFormat));
if (isCompatible) {
int reply;
reply = QMessageBox::question(
window(), tr("Update Available"),
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +
"</a>)<br><br>" + tr("Do you want to update now?"),
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
downloadUpdate(release->getName());
} else {
closeDialog();
}
} else {
QMessageBox::information(
window(), tr("Update Available"),
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +
"</a>)<br><br>" +
tr("Unfortunately, the automatic updater failed to find a compatible download. \nYou may have to "
"manually download the new version.") +
"<br><br>" +
tr("Please check the <a href=\"%1\">releases page</a> on our Github and download the build for your "
"system.")
.arg(release->getDescriptionUrl()));
}
}
void DlgUpdate::enableUpdateButton(bool enable)
{
manualDownload->setEnabled(enable);
}
void DlgUpdate::addStopDownloadAndRemoveOthers(bool enable)
{
if (enable) {
buttonBox->addButton(stopDownload, QDialogButtonBox::ActionRole);
buttonBox->removeButton(manualDownload);
buttonBox->removeButton(gotoDownload);
} else {
buttonBox->removeButton(stopDownload);
buttonBox->addButton(manualDownload, QDialogButtonBox::ActionRole);
buttonBox->addButton(gotoDownload, QDialogButtonBox::ActionRole);
}
}
void DlgUpdate::enableOkButton(bool enable)
{
ok->setEnabled(enable);
}
void DlgUpdate::setLabel(const QString &newText)
{
statusLabel->setText(newText);
}
void DlgUpdate::updateCheckError(const QString &errorString)
{
setLabel(tr("Error"));
QMessageBox::critical(this, tr("Update Error"),
tr("An error occurred while checking for updates:") + QString(" ") + errorString);
}
void DlgUpdate::downloadError(const QString &errorString)
{
setLabel(tr("Error"));
enableUpdateButton(true);
QMessageBox::critical(this, tr("Update Error"),
tr("An error occurred while downloading an update:") + QString(" ") + errorString);
}
void DlgUpdate::downloadSuccessful(const QUrl &filepath)
{
setLabel(tr("Installing..."));
QString installerPath = filepath.toLocalFile();
QString appDir = QDir::toNativeSeparators(QCoreApplication::applicationDirPath());
QProcess process;
process.setProgram(installerPath);
// NSIS needs the /D= argument to be an UNQUOTED string, even if it contains spaces. Qt likes to quote arguments if
// they contain spaces, so we use the windows exclusive QProcess::setNativeArguments in the only case where this is
// relevant, which preserves the argument unquoted.
#ifdef Q_OS_WIN
process.setNativeArguments(QString("/R /D=%1").arg(appDir));
#else
// Linux/macOS: normal argument passing (not relevant since they update differently.)
process.setArguments({"/R", QString("/D=%1").arg(appDir)});
#endif
// Try to open the installer. If it opens, quit Cockatrice
if (process.startDetached()) {
qCInfo(DlgUpdateLog) << "Opened downloaded update file successfully - closing Cockatrice";
// Close the main window synchronously so file locks are released before the NSIS installer
// (already launched) starts replacing files. This also flushes settings and shuts down the
// tabs, but only when the close is actually accepted: MainWindow may veto it for a running
// card DB update, an open game, or an unsaved deck. In that case keep running so the user
// can resolve the blocker, and tell them the installer is already waiting.
if (auto *window = qobject_cast<MainWindow *>(parent())) {
if (window->close()) {
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
} else {
QMessageBox::warning(this, tr("Update"),
tr("The update installer is already running and will finish the update once "
"Cockatrice closes. Cockatrice is still busy, so it stays open for now."));
}
} else {
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
}
close();
} else {
setLabel(tr("Error"));
QMessageBox::critical(this, tr("Update Error"),
tr("Cockatrice is unable to open the installer.") + "<br><br>" +
tr("Try to update manually by closing Cockatrice and running the installer.") +
"<br>" + tr("Download location") + QString(": %1").arg(filepath.toLocalFile()));
}
}
void DlgUpdate::downloadProgressMade(qint64 bytesRead, qint64 totalBytes)
{
progress->setMaximum(totalBytes);
progress->setValue(bytesRead);
}