Path: blob/master/src/duckstation-qt/autoupdaterdialog.cpp
6233 views
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "autoupdaterdialog.h"4#include "mainwindow.h"5#include "qthost.h"6#include "qtprogresscallback.h"7#include "qtutils.h"8#include "scmversion/scmversion.h"9#include "unzip.h"1011#include "util/http_downloader.h"1213#include "common/assert.h"14#include "common/error.h"15#include "common/file_system.h"16#include "common/log.h"17#include "common/minizip_helpers.h"18#include "common/path.h"19#include "common/string_util.h"2021#include "fmt/format.h"2223#include <QtCore/QCoreApplication>24#include <QtCore/QFileInfo>25#include <QtCore/QJsonArray>26#include <QtCore/QJsonDocument>27#include <QtCore/QJsonObject>28#include <QtCore/QJsonValue>29#include <QtCore/QProcess>30#include <QtCore/QString>31#include <QtCore/QTimer>32#include <QtWidgets/QCheckBox>33#include <QtWidgets/QDialog>34#include <QtWidgets/QMessageBox>35#include <QtWidgets/QProgressDialog>36#include <QtWidgets/QPushButton>3738#include "moc_autoupdaterdialog.cpp"3940// Interval at which HTTP requests are polled.41static constexpr u32 HTTP_POLL_INTERVAL = 10;4243#if defined(_WIN32)44#include "common/windows_headers.h"45#include <shellapi.h>46#elif defined(__APPLE__)47#include "common/cocoa_tools.h"48#else49#include <sys/stat.h>50#endif5152// Logic to detect whether we can use the auto updater.53// Requires that the channel be defined by the buildbot.54#if __has_include("scmversion/tag.h")55#include "scmversion/tag.h"56#else57#define UPDATER_RELEASE_CHANNEL "preview"58#endif5960// Updater asset information.61// clang-format off62#if defined(_WIN32)63#if defined(CPU_ARCH_X64) && defined(CPU_ARCH_SSE41)64#define UPDATER_EXPECTED_EXECUTABLE "duckstation-qt-x64-ReleaseLTCG.exe"65#define UPDATER_ASSET_FILENAME "duckstation-windows-x64-release.zip"66#elif defined(CPU_ARCH_X64)67#define UPDATER_EXPECTED_EXECUTABLE "duckstation-qt-x64-ReleaseLTCG-SSE2.exe"68#define UPDATER_ASSET_FILENAME "duckstation-windows-x64-sse2-release.zip"69#elif defined(CPU_ARCH_ARM64)70#define UPDATER_EXPECTED_EXECUTABLE "duckstation-qt-ARM64-ReleaseLTCG.exe"71#define UPDATER_ASSET_FILENAME "duckstation-windows-arm64-release.zip"72#endif73#elif defined(__APPLE__)74#define UPDATER_ASSET_FILENAME "duckstation-mac-release.zip"75#elif defined(__linux__)76#if defined(CPU_ARCH_X64) && defined(CPU_ARCH_SSE41)77#define UPDATER_ASSET_FILENAME "DuckStation-x64.AppImage"78#elif defined(CPU_ARCH_X64)79#define UPDATER_ASSET_FILENAME "DuckStation-x64-SSE2.AppImage"80#elif defined(CPU_ARCH_ARM64)81#define UPDATER_ASSET_FILENAME "DuckStation-arm64.AppImage"82#elif defined(CPU_ARCH_ARM32)83#define UPDATER_ASSET_FILENAME "DuckStation-armhf.AppImage"84#elif defined(CPU_ARCH_RISCV64)85#define UPDATER_ASSET_FILENAME "DuckStation-riscv64.AppImage"86#endif87#endif88#ifndef UPDATER_ASSET_FILENAME89#error Unsupported platform.90#endif91// clang-format on9293// URLs for downloading updates.94#define LATEST_TAG_URL "https://api.github.com/repos/stenzek/duckstation/tags"95#define LATEST_RELEASE_URL "https://api.github.com/repos/stenzek/duckstation/releases/tags/{}"96#define CHANGES_URL "https://api.github.com/repos/stenzek/duckstation/compare/{}...{}"97#define DOWNLOAD_PAGE_URL "https://github.com/stenzek/duckstation/releases/tag/{}"9899// Update channels.100static constexpr const std::pair<const char*, const char*> s_update_channels[] = {101{"latest", QT_TRANSLATE_NOOP("AutoUpdaterWindow", "Stable Releases")},102{"preview", QT_TRANSLATE_NOOP("AutoUpdaterWindow", "Preview Releases")},103};104105LOG_CHANNEL(Host);106107AutoUpdaterDialog::AutoUpdaterDialog(QWidget* const parent, Error* const error) : QDialog(parent)108{109m_ui.setupUi(this);110QFont title_font(m_ui.titleLabel->font());111title_font.setBold(true);112title_font.setPixelSize(20);113m_ui.titleLabel->setFont(title_font);114setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);115setDownloadSectionVisibility(false);116117connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterDialog::downloadUpdateClicked);118connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterDialog::skipThisUpdateClicked);119connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterDialog::remindMeLaterClicked);120121m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent(), error);122123m_http_poll_timer = new QTimer(this);124m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterDialog::httpPollTimerPoll);125}126127AutoUpdaterDialog::~AutoUpdaterDialog() = default;128129AutoUpdaterDialog* AutoUpdaterDialog::create(QWidget* const parent, Error* const error)130{131AutoUpdaterDialog* const win = new AutoUpdaterDialog(parent, error);132if (!win->m_http)133{134delete win;135return nullptr;136}137138return win;139}140141void AutoUpdaterDialog::warnAboutUnofficialBuild()142{143//144// To those distributing their own builds or packages of DuckStation, and seeing this message:145//146// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.147//148// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.149// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.150// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and151// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and152// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.153//154// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for155// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to156// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream157// changes without attribution, violating copyright.158//159// Thanks, and I hope you understand.160//161162#if !__has_include("scmversion/tag.h") || !UPDATER_RELEASE_IS_OFFICIAL163constexpr const char* CONFIG_SECTION = "UI";164constexpr const char* CONFIG_KEY = "UnofficialBuildWarningConfirmed";165if (166#if !defined(_WIN32) && !defined(__APPLE__)167EmuFolders::AppRoot.starts_with("/home") && // Devbuilds should be in home directory.168#endif169Host::GetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, false))170{171return;172}173174constexpr int DELAY_SECONDS = 10;175176const QString message =177QStringLiteral("<h1>You are not using an official release!</h1><h3>DuckStation is licensed under the terms of "178"CC-BY-NC-ND-4.0, which does not allow modified builds to be distributed.</h3>"179"<p>If you are a developer and using a local build, you can check the box below and continue.</p>"180"<p>Otherwise, you should delete this build and download an official release from "181"<a href=\"https://www.duckstation.org/\">duckstation.org</a>.</p><p>Do you want to exit and "182"open this page now?</p>");183184QMessageBox mbox;185mbox.setIcon(QMessageBox::Warning);186mbox.setWindowTitle(QStringLiteral("Unofficial Build Warning"));187mbox.setWindowIcon(QtHost::GetAppIcon());188mbox.setWindowFlag(Qt::CustomizeWindowHint, true);189mbox.setWindowFlag(Qt::WindowCloseButtonHint, false);190mbox.setTextFormat(Qt::RichText);191mbox.setText(message);192193mbox.addButton(QMessageBox::Yes);194QPushButton* no = mbox.addButton(QMessageBox::No);195const QString orig_no_text = no->text();196no->setEnabled(false);197198QCheckBox* cb = new QCheckBox(&mbox);199cb->setText(tr("Do not show again"));200mbox.setCheckBox(cb);201202int remaining_time = DELAY_SECONDS;203no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));204205QTimer* timer = new QTimer(&mbox);206connect(timer, &QTimer::timeout, &mbox, [no, timer, &remaining_time, &orig_no_text]() {207remaining_time--;208if (remaining_time == 0)209{210no->setText(orig_no_text);211no->setEnabled(true);212timer->stop();213}214else215{216no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));217}218});219timer->start(1000);220221if (mbox.exec() == QMessageBox::Yes)222{223QtUtils::OpenURL(nullptr, "https://duckstation.org/");224QMetaObject::invokeMethod(qApp, &QApplication::quit, Qt::QueuedConnection);225return;226}227228if (cb->isChecked())229Host::SetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, true);230#endif231}232233std::vector<std::pair<QString, QString>> AutoUpdaterDialog::getChannelList()234{235std::vector<std::pair<QString, QString>> ret;236ret.reserve(std::size(s_update_channels));237for (const auto& [name, desc] : s_update_channels)238ret.emplace_back(QString::fromUtf8(name), qApp->translate("AutoUpdaterWindow", desc));239return ret;240}241242std::string AutoUpdaterDialog::getDefaultTag()243{244return UPDATER_RELEASE_CHANNEL;245}246247std::string AutoUpdaterDialog::getCurrentUpdateTag()248{249return Host::GetBaseStringSettingValue("AutoUpdater", "UpdateTag", UPDATER_RELEASE_CHANNEL);250}251252void AutoUpdaterDialog::setDownloadSectionVisibility(bool visible)253{254m_ui.downloadProgress->setVisible(visible);255m_ui.downloadStatus->setVisible(visible);256m_ui.downloadButtonBox->setVisible(visible);257m_ui.downloadAndInstall->setVisible(!visible);258m_ui.skipThisUpdate->setVisible(!visible);259m_ui.remindMeLater->setVisible(!visible);260}261262void AutoUpdaterDialog::reportError(const std::string_view msg)263{264// if we're visible, use ourselves.265QWidget* const parent = (isVisible() ? static_cast<QWidget*>(this) : g_main_window);266const QString full_msg = tr("Failed to retrieve or download update:\n\n%1\n\nYou can manually update DuckStation by "267"re-downloading the latest release. Do you want to open the download page now?")268.arg(QtUtils::StringViewToQString(msg));269QMessageBox* const msgbox = QtUtils::NewMessageBox(parent, QMessageBox::Critical, tr("Updater Error"), full_msg,270QMessageBox::Yes | QMessageBox::No);271msgbox->connect(msgbox, &QMessageBox::accepted, parent,272[parent]() { QtUtils::OpenURL(parent, fmt::format(DOWNLOAD_PAGE_URL, UPDATER_RELEASE_CHANNEL)); });273msgbox->open();274}275276void AutoUpdaterDialog::ensureHttpPollingActive()277{278if (m_http_poll_timer->isActive())279return;280281m_http_poll_timer->setSingleShot(false);282m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL);283m_http_poll_timer->start();284}285286void AutoUpdaterDialog::httpPollTimerPoll()287{288m_http->PollRequests();289290if (!m_http->HasAnyRequests())291{292VERBOSE_LOG("All HTTP requests done.");293m_http_poll_timer->stop();294}295}296297void AutoUpdaterDialog::queueUpdateCheck(bool display_errors)298{299ensureHttpPollingActive();300m_http->CreateRequest(LATEST_TAG_URL,301[this, display_errors](s32 status_code, const Error& error, const std::string& content_type,302std::vector<u8> response) {303getLatestTagComplete(status_code, error, std::move(response), display_errors);304});305}306307void AutoUpdaterDialog::queueGetLatestRelease()308{309ensureHttpPollingActive();310std::string url = fmt::format(LATEST_RELEASE_URL, getCurrentUpdateTag());311m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterDialog::getLatestReleaseComplete, this,312std::placeholders::_1, std::placeholders::_2, std::placeholders::_4));313}314315void AutoUpdaterDialog::getLatestTagComplete(s32 status_code, const Error& error, std::vector<u8> response,316bool display_errors)317{318const std::string selected_tag(getCurrentUpdateTag());319const QString selected_tag_qstr = QString::fromStdString(selected_tag);320321if (status_code == HTTPDownloader::HTTP_STATUS_OK)322{323QJsonParseError parse_error;324const QJsonDocument doc = QJsonDocument::fromJson(325QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);326if (doc.isArray())327{328const QJsonArray doc_array(doc.array());329for (const QJsonValue& val : doc_array)330{331if (!val.isObject())332continue;333334if (val["name"].toString() != selected_tag_qstr)335continue;336337m_latest_sha = val["commit"].toObject()["sha"].toString();338if (m_latest_sha.isEmpty())339continue;340341if (updateNeeded())342{343queueGetLatestRelease();344return;345}346else347{348if (display_errors)349{350QtUtils::AsyncMessageBox(g_main_window, QMessageBox::Information, tr("Automatic Updater"),351tr("No updates are currently available. Please try again later."));352}353354emit updateCheckCompleted(false);355return;356}357}358359if (display_errors)360reportError(fmt::format("{} release not found in JSON", selected_tag));361}362else363{364if (display_errors)365reportError("JSON is not an array");366}367}368else369{370if (display_errors)371reportError(fmt::format("Failed to download latest tag info: {}", error.GetDescription()));372}373374emit updateCheckCompleted(false);375}376377void AutoUpdaterDialog::getLatestReleaseComplete(s32 status_code, const Error& error, std::vector<u8> response)378{379if (status_code == HTTPDownloader::HTTP_STATUS_OK)380{381QJsonParseError parse_error;382const QJsonDocument doc = QJsonDocument::fromJson(383QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);384if (doc.isObject())385{386const QJsonObject doc_object(doc.object());387388// search for the correct file389const QJsonArray assets(doc_object["assets"].toArray());390const QString asset_filename = QStringLiteral(UPDATER_ASSET_FILENAME);391bool asset_found = false;392for (const QJsonValue& asset : assets)393{394const QJsonObject asset_obj(asset.toObject());395if (asset_obj["name"] == asset_filename)396{397m_download_url = asset_obj["browser_download_url"].toString();398if (!m_download_url.isEmpty())399m_download_size = asset_obj["size"].toInt();400asset_found = true;401break;402}403}404405if (!asset_found)406{407reportError("Asset not found");408return;409}410411const QString current_date = QtHost::FormatNumber(412Host::NumberFormatType::ShortDateTime,413static_cast<s64>(414QDateTime::fromString(QString::fromUtf8(g_scm_date_str), Qt::DateFormat::ISODate).toSecsSinceEpoch()));415const QString release_date = QtHost::FormatNumber(416Host::NumberFormatType::ShortDateTime,417static_cast<s64>(418QDateTime::fromString(doc_object["published_at"].toString(), Qt::DateFormat::ISODate).toSecsSinceEpoch()));419420m_ui.currentVersion->setText(tr("%1 (%2)")421.arg(QtUtils::StringViewToQString(422TinyString::from_format("{}/{}", g_scm_version_str, UPDATER_RELEASE_CHANNEL)))423.arg(current_date));424m_ui.newVersion->setText(tr("%1 (%2)").arg(QString::fromStdString(getCurrentUpdateTag())).arg(release_date));425m_ui.downloadSize->setText(tr("%1 MB").arg(static_cast<double>(m_download_size) / 1000000.0, 0, 'f', 2));426427m_ui.downloadAndInstall->setEnabled(true);428m_ui.updateNotes->setText(tr("Loading..."));429queueGetChanges();430emit updateCheckCompleted(true);431return;432}433else434{435reportError("JSON is not an object");436}437}438else439{440reportError(fmt::format("Failed to download latest release info: {}", error.GetDescription()));441}442443emit updateCheckCompleted(false);444}445446void AutoUpdaterDialog::queueGetChanges()447{448ensureHttpPollingActive();449std::string url = fmt::format(CHANGES_URL, g_scm_hash_str, getCurrentUpdateTag());450m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterDialog::getChangesComplete, this, std::placeholders::_1,451std::placeholders::_2, std::placeholders::_4));452}453454void AutoUpdaterDialog::getChangesComplete(s32 status_code, const Error& error, std::vector<u8> response)455{456std::string_view error_message;457458if (status_code == HTTPDownloader::HTTP_STATUS_OK)459{460QJsonParseError parse_error;461const QJsonDocument doc = QJsonDocument::fromJson(462QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);463if (doc.isObject())464{465const QJsonObject doc_object(doc.object());466467QString changes_html = tr("<h2>Changes:</h2>");468changes_html += QStringLiteral("<ul>");469470const QJsonArray commits(doc_object["commits"].toArray());471bool update_will_break_save_states = false;472bool update_increases_settings_version = false;473474for (const QJsonValue& commit : commits)475{476const QJsonObject commit_obj(commit["commit"].toObject());477478QString message = commit_obj["message"].toString();479QString author = commit_obj["author"].toObject()["name"].toString();480const qsizetype first_line_terminator = message.indexOf('\n');481if (first_line_terminator >= 0)482message.remove(first_line_terminator, message.size() - first_line_terminator);483if (!message.isEmpty())484{485changes_html +=486QStringLiteral("<li>%1 <i>(%2)</i></li>").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());487}488489if (message.contains(QStringLiteral("[SAVEVERSION+]")))490update_will_break_save_states = true;491492if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))493update_increases_settings_version = true;494}495496changes_html += "</ul>";497498if (update_will_break_save_states)499{500changes_html.prepend(tr("<h2>Save State Warning</h2><p>Installing this update will make your save states "501"<b>incompatible</b>. Please ensure you have saved your games to memory card "502"before installing this update or you will lose progress.</p>"));503}504505if (update_increases_settings_version)506{507changes_html.prepend(508tr("<h2>Settings Warning</h2><p>Installing this update will reset your program configuration. Please note "509"that you will have to reconfigure your settings after this update.</p>"));510}511512m_ui.updateNotes->setText(changes_html);513return;514}515else516{517error_message = "Change list JSON is not an object";518}519}520else521{522error_message = error.GetDescription();523}524525m_ui.updateNotes->setText(QString::fromStdString(526fmt::format("<h2>Failed to download change list</h2><p>The error was:<pre>{}</pre></p><p>You may be able to "527"install this update anyway. If the download installation fails, you can download the update "528"from:</p><p><a href=\"" DOWNLOAD_PAGE_URL "\">" DOWNLOAD_PAGE_URL "</a></p>",529error_message, UPDATER_RELEASE_CHANNEL, UPDATER_RELEASE_CHANNEL)));530}531532void AutoUpdaterDialog::downloadUpdateClicked()533{534// Prevent multiple clicks of the button.535if (m_download_progress_callback)536return;537538setDownloadSectionVisibility(true);539540m_download_progress_callback = new QtProgressCallback(this);541m_download_progress_callback->connectWidgets(m_ui.downloadStatus, m_ui.downloadProgress,542m_ui.downloadButtonBox->button(QDialogButtonBox::Cancel));543m_download_progress_callback->SetStatusText(TRANSLATE_SV("AutoUpdaterWindow", "Downloading Update..."));544545ensureHttpPollingActive();546m_http->CreateRequest(547m_download_url.toStdString(),548[this](s32 status_code, const Error& error, const std::string&, std::vector<u8> response) {549m_download_progress_callback->SetStatusText(TRANSLATE_SV("AutoUpdaterWindow", "Processing Update..."));550m_download_progress_callback->SetProgressRange(1);551m_download_progress_callback->SetProgressValue(1);552DebugAssert(m_download_progress_callback);553delete m_download_progress_callback;554m_download_progress_callback = nullptr;555556if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)557{558setDownloadSectionVisibility(false);559return;560}561562if (status_code != HTTPDownloader::HTTP_STATUS_OK)563{564reportError(fmt::format("Download failed: {}", error.GetDescription()));565setDownloadSectionVisibility(false);566return;567}568569if (response.empty())570{571reportError("Download failed: Update is empty");572setDownloadSectionVisibility(false);573return;574}575576if (processUpdate(response))577{578// updater started, request exit. can't do it immediately as closing the main window will delete us.579QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit, Qt::QueuedConnection, false);580}581else582{583// allow user to try again584setDownloadSectionVisibility(false);585}586},587m_download_progress_callback);588}589590bool AutoUpdaterDialog::updateNeeded() const591{592QString last_checked_sha = QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion"));593594INFO_LOG("Current SHA: {}", g_scm_hash_str);595INFO_LOG("Latest SHA: {}", m_latest_sha.toUtf8().constData());596INFO_LOG("Last Checked SHA: {}", last_checked_sha.toUtf8().constData());597if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha)598{599INFO_LOG("No update needed.");600return false;601}602603INFO_LOG("Update needed.");604return true;605}606607void AutoUpdaterDialog::skipThisUpdateClicked()608{609Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData());610Host::CommitBaseSettingChanges();611close();612}613614void AutoUpdaterDialog::remindMeLaterClicked()615{616close();617}618619void AutoUpdaterDialog::closeEvent(QCloseEvent* event)620{621emit closed();622QWidget::closeEvent(event);623}624625#ifdef _WIN32626627static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";628static constexpr char UPDATER_ARCHIVE_NAME[] = "update.zip";629630bool AutoUpdaterDialog::doesUpdaterNeedElevation(const std::string& application_dir) const631{632// Try to create a dummy text file in the updater directory. If it fails, we probably won't have write permission.633const std::string dummy_path = Path::Combine(application_dir, "update.txt");634auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");635if (!fp)636return true;637638fp.reset();639FileSystem::DeleteFile(dummy_path.c_str());640return false;641}642643bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)644{645const std::string& application_dir = EmuFolders::AppRoot;646const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);647const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);648649const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();650if (program_path.empty())651{652reportError("Failed to get current application path");653return false;654}655656Error error;657if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFile(update_zip_path.c_str(), &error)))658{659reportError(fmt::format("Removing existing update zip failed:\n{}", error.GetDescription()));660return false;661}662663if (!FileSystem::WriteAtomicRenamedFile(update_zip_path.c_str(), update_data, &error))664{665reportError(fmt::format("Writing update zip to '{}' failed:\n{}", update_zip_path, error.GetDescription()));666return false;667}668669Error updater_extract_error;670if (!extractUpdater(update_zip_path.c_str(), updater_path.c_str(), Path::GetFileName(program_path),671&updater_extract_error))672{673reportError(fmt::format("Extracting updater failed: {}", updater_extract_error.GetDescription()));674return false;675}676677return doUpdate(application_dir, update_zip_path, updater_path, program_path);678}679680bool AutoUpdaterDialog::extractUpdater(const std::string& zip_path, const std::string& destination_path,681const std::string_view check_for_file, Error* error)682{683unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.c_str());684if (!zf)685{686reportError("Failed to open update zip");687return false;688}689690// check the the expected program name is inside the updater zip. if it's not, we're going to launch the old binary691// with a partially updated installation692if (unzLocateFile(zf, std::string(check_for_file).c_str(), 0) != UNZ_OK)693{694if (QtUtils::MessageBoxIcon(695this, QMessageBox::Warning, tr("Updater Warning"),696tr("<h1>Inconsistent Application State</h1><h3>The update zip is missing the current executable:</h3><div "697"align=\"center\"><pre>%1</pre></div><p><strong>This is usually a result of manually renaming the "698"file.</strong> Continuing to install this update may result in a broken installation if the renamed "699"executable is used. The DuckStation executable should be named:<div "700"align=\"center\"><pre>%2</pre></div><p>Do you want to continue anyway?</p>")701.arg(QString::fromStdString(std::string(check_for_file)))702.arg(QStringLiteral(UPDATER_EXPECTED_EXECUTABLE)),703QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton) == QMessageBox::No)704{705Error::SetStringFmt(error, "Update zip is missing expected file: {}", check_for_file);706unzClose(zf);707return false;708}709}710711if (unzLocateFile(zf, UPDATER_EXECUTABLE, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)712{713Error::SetString(error, "Failed to locate updater.exe");714unzClose(zf);715return false;716}717718auto fp = FileSystem::OpenManagedCFile(destination_path.c_str(), "wb", error);719if (!fp)720{721Error::SetString(error, "Failed to open updater.exe for writing");722unzClose(zf);723return false;724}725726static constexpr size_t CHUNK_SIZE = 4096;727char chunk[CHUNK_SIZE];728for (;;)729{730int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);731if (size < 0)732{733Error::SetString(error, "Failed to decompress updater exe");734unzClose(zf);735fp.reset();736FileSystem::DeleteFile(destination_path.c_str());737return false;738}739else if (size == 0)740{741break;742}743744if (std::fwrite(chunk, size, 1, fp.get()) != 1)745{746Error::SetErrno(error, "Failed to write updater exe: fwrite() failed: ", errno);747unzClose(zf);748fp.reset();749FileSystem::DeleteFile(destination_path.c_str());750return false;751}752}753754unzClose(zf);755return true;756}757758bool AutoUpdaterDialog::doUpdate(const std::string& application_dir, const std::string& zip_path,759const std::string& updater_path, const std::string& program_path)760{761const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);762const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);763const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format(764"{} \"{}\" \"{}\" \"{}\"", QCoreApplication::applicationPid(), application_dir, zip_path, program_path));765766const bool needs_elevation = doesUpdaterNeedElevation(application_dir);767768SHELLEXECUTEINFOW sei = {};769sei.cbSize = sizeof(sei);770sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation771sei.lpFile = wupdater_path.c_str();772sei.lpParameters = arguments.c_str();773sei.lpDirectory = wapplication_dir.c_str();774sei.nShow = SW_SHOWNORMAL;775if (!ShellExecuteExW(&sei))776{777reportError(fmt::format("Failed to start {}: {}", needs_elevation ? "elevated updater" : "updater",778Error::CreateWin32(GetLastError()).GetDescription()));779return false;780}781782return true;783}784785void AutoUpdaterDialog::cleanupAfterUpdate()786{787// If we weren't portable, then updater executable gets left in the application directory.788if (EmuFolders::AppRoot == EmuFolders::DataRoot)789return;790791const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);792if (!FileSystem::FileExists(updater_path.c_str()))793return;794795Error error;796if (!FileSystem::DeleteFile(updater_path.c_str(), &error))797{798QtUtils::MessageBoxCritical(799nullptr, tr("Updater Error"),800tr("Failed to remove updater exe after update:\n%1").arg(QString::fromStdString(error.GetDescription())));801return;802}803}804805#elif defined(__APPLE__)806807bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)808{809std::optional<std::string> bundle_path = CocoaTools::GetNonTranslocatedBundlePath();810if (!bundle_path.has_value())811{812reportError("Couldn't obtain non-translocated bundle path.");813return false;814}815816QFileInfo info(QString::fromStdString(bundle_path.value()));817if (!info.isBundle())818{819reportError(fmt::format("Application {} isn't a bundle.", bundle_path.value()));820return false;821}822if (info.suffix() != QStringLiteral("app"))823{824reportError(825fmt::format("Unexpected application suffix {} on {}.", info.suffix().toStdString(), bundle_path.value()));826return false;827}828829// Use the updater from this version to unpack the new version.830const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app");831if (!FileSystem::DirectoryExists(updater_app.c_str()))832{833reportError(fmt::format("Failed to find updater at {}.", updater_app));834return false;835}836837// We use the user data directory to temporarily store the update zip.838const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip");839const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING");840Error error;841if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str(), &error))842{843reportError(fmt::format("Failed to remove old update zip:\n{}", error.GetDescription()));844return false;845}846847// Save update.848if (!FileSystem::WriteAtomicRenamedFile(zip_path.c_str(), update_data, &error))849{850reportError(fmt::format("Writing update zip to '{}' failed:\n{}", zip_path, error.GetDescription()));851return false;852}853854INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",855updater_app, zip_path, staging_directory, bundle_path.value());856857const std::string_view args[] = {858zip_path,859staging_directory,860bundle_path.value(),861};862863// Kick off updater!864CocoaTools::DelayedLaunch(updater_app, args);865return true;866}867868void AutoUpdaterDialog::cleanupAfterUpdate()869{870}871872#elif defined(__linux__)873874bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)875{876const char* appimage_path = std::getenv("APPIMAGE");877if (!appimage_path || !FileSystem::FileExists(appimage_path))878{879reportError("Missing APPIMAGE.");880return false;881}882883if (!FileSystem::FileExists(appimage_path))884{885reportError(fmt::format("Current AppImage does not exist: {}", appimage_path));886return false;887}888889const std::string new_appimage_path = fmt::format("{}.new", appimage_path);890const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);891INFO_LOG("APPIMAGE = {}", appimage_path);892INFO_LOG("Backup AppImage path = {}", backup_appimage_path);893INFO_LOG("New AppImage path = {}", new_appimage_path);894895// Remove old "new" appimage and existing backup appimage.896Error error;897if (FileSystem::FileExists(new_appimage_path.c_str()) && !FileSystem::DeleteFile(new_appimage_path.c_str(), &error))898{899reportError(900fmt::format("Failed to remove old destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));901return false;902}903if (FileSystem::FileExists(backup_appimage_path.c_str()) &&904!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))905{906reportError(907fmt::format("Failed to remove old backup AppImage: {}:\n{}", backup_appimage_path, error.GetDescription()));908return false;909}910911// Write "new" appimage.912{913// We want to copy the permissions from the old appimage to the new one.914static constexpr int permission_mask = S_IRWXU | S_IRWXG | S_IRWXO;915struct stat old_stat;916if (!FileSystem::StatFile(appimage_path, &old_stat, &error))917{918reportError(fmt::format("Failed to get old AppImage {} permissions:\n{}", appimage_path, error.GetDescription()));919return false;920}921922// We do this as a manual write here, rather than using WriteAtomicUpdatedFile(), because we want to write the923// file and set the permissions as one atomic operation.924FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(new_appimage_path.c_str(), "wb", &error);925bool success = static_cast<bool>(fp);926if (fp)927{928if (std::fwrite(update_data.data(), update_data.size(), 1, fp.get()) == 1 && std::fflush(fp.get()) == 0)929{930const int fd = fileno(fp.get());931if (fd >= 0)932{933if (fchmod(fd, old_stat.st_mode & permission_mask) != 0)934{935error.SetErrno("fchmod() failed: ", errno);936success = false;937}938}939else940{941error.SetErrno("fileno() failed: ", errno);942success = false;943}944}945else946{947error.SetErrno("fwrite() failed: ", errno);948success = false;949}950951fp.reset();952if (!success)953FileSystem::DeleteFile(new_appimage_path.c_str());954}955956if (!success)957{958reportError(959fmt::format("Failed to write new destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));960return false;961}962}963964// Rename "old" appimage.965if (!FileSystem::RenamePath(appimage_path, backup_appimage_path.c_str(), &error))966{967reportError(fmt::format("Failed to rename old AppImage to {}:\n{}", backup_appimage_path, error.GetDescription()));968FileSystem::DeleteFile(new_appimage_path.c_str());969return false;970}971972// Rename "new" appimage.973if (!FileSystem::RenamePath(new_appimage_path.c_str(), appimage_path, &error))974{975reportError(fmt::format("Failed to rename new AppImage to {}:\n{}", appimage_path, error.GetDescription()));976return false;977}978979// Execute new appimage.980QProcess* new_process = new QProcess();981new_process->setProgram(QString::fromUtf8(appimage_path));982new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});983if (!new_process->startDetached())984{985reportError("Failed to execute new AppImage.");986return false;987}988989// We exit once we return.990return true;991}992993void AutoUpdaterDialog::cleanupAfterUpdate()994{995// Remove old/backup AppImage.996const char* appimage_path = std::getenv("APPIMAGE");997if (!appimage_path)998return;9991000const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);1001if (!FileSystem::FileExists(backup_appimage_path.c_str()))1002return;10031004Error error;1005INFO_LOG("Removing backup AppImage: {}", backup_appimage_path);1006if (!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))1007ERROR_LOG("Failed to remove backup AppImage {}: {}", backup_appimage_path, error.GetDescription());1008}10091010#else10111012bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)1013{1014return false;1015}10161017void AutoUpdaterDialog::cleanupAfterUpdate()1018{1019}10201021#endif102210231024