Path: blob/master/src/duckstation-qt/autoupdaterwindow.cpp
4802 views
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "autoupdaterwindow.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_autoupdaterwindow.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#define UPDATE_CHECKER_SUPPORTED57#ifdef SCM_RELEASE_ASSET58#define AUTO_UPDATER_SUPPORTED59#endif60#endif6162#ifdef UPDATE_CHECKER_SUPPORTED6364static const char* LATEST_TAG_URL = "https://api.github.com/repos/stenzek/duckstation/tags";65static const char* LATEST_RELEASE_URL = "https://api.github.com/repos/stenzek/duckstation/releases/tags/{}";66static const char* CHANGES_URL = "https://api.github.com/repos/stenzek/duckstation/compare/{}...{}";67static const char* DOWNLOAD_PAGE_URL = "https://github.com/stenzek/duckstation/releases/tag/{}";68static const char* UPDATE_TAGS[] = SCM_RELEASE_TAGS;69static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG;7071#ifdef AUTO_UPDATER_SUPPORTED72static const char* UPDATE_ASSET_FILENAME = SCM_RELEASE_ASSET;73#endif7475#endif7677LOG_CHANNEL(Host);7879AutoUpdaterWindow::AutoUpdaterWindow(QWidget* parent /* = nullptr */) : QWidget(parent)80{81m_ui.setupUi(this);82setWindowIcon(QtHost::GetAppIcon());83setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);8485connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterWindow::downloadUpdateClicked);86connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterWindow::skipThisUpdateClicked);87connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterWindow::remindMeLaterClicked);8889Error error;90m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent(), &error);91if (!m_http)92ERROR_LOG("Failed to create HTTP downloader, auto updater will not be available:\n{}", error.GetDescription());93}9495AutoUpdaterWindow::~AutoUpdaterWindow() = default;9697bool AutoUpdaterWindow::isSupported()98{99#ifdef UPDATE_CHECKER_SUPPORTED100return true;101#else102return false;103#endif104}105106bool AutoUpdaterWindow::canInstallUpdate()107{108#ifndef AUTO_UPDATER_SUPPORTED109return false;110#elif defined(__linux__)111// Linux Flatpak is a wrapper of the AppImage, which will have the AUTO_UPDATER_SUPPORTED flag set.112// Redirect to the download page instead if not running under an AppImage.113return (std::getenv("APPIMAGE") != nullptr);114#else115return true;116#endif117}118119bool AutoUpdaterWindow::isOfficialBuild()120{121#if !__has_include("scmversion/tag.h")122return false;123#else124return true;125#endif126}127128void AutoUpdaterWindow::warnAboutUnofficialBuild()129{130//131// To those distributing their own builds or packages of DuckStation, and seeing this message:132//133// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.134//135// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.136// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.137// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and138// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and139// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.140//141// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for142// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to143// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream144// changes without attribution, violating copyright.145//146// Thanks, and I hope you understand.147//148149#if !__has_include("scmversion/tag.h")150constexpr const char* CONFIG_SECTION = "UI";151constexpr const char* CONFIG_KEY = "UnofficialBuildWarningConfirmed";152if (153#ifndef _WIN32154!StringUtil::StartsWithNoCase(EmuFolders::AppRoot, "/usr") &&155#endif156Host::GetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, false))157{158return;159}160161constexpr int DELAY_SECONDS = 5;162163const QString message =164QStringLiteral("<h1>You are not using an official release!</h1><h3>DuckStation is licensed under the terms of "165"CC-BY-NC-ND-4.0, which does not allow modified builds to be distributed.</h3>"166"<p>If you are a developer and using a local build, you can check the box below and continue.</p>"167"<p>Otherwise, you should delete this build and download an official release from "168"<a href=\"https://www.duckstation.org/\">duckstation.org</a>.</p><p>Do you want to exit and "169"open this page now?</p>");170171QMessageBox mbox;172mbox.setIcon(QMessageBox::Warning);173mbox.setWindowTitle(QStringLiteral("Unofficial Build Warning"));174mbox.setWindowIcon(QtHost::GetAppIcon());175mbox.setWindowFlag(Qt::CustomizeWindowHint, true);176mbox.setWindowFlag(Qt::WindowCloseButtonHint, false);177mbox.setTextFormat(Qt::RichText);178mbox.setText(message);179180mbox.addButton(QMessageBox::Yes);181QPushButton* no = mbox.addButton(QMessageBox::No);182const QString orig_no_text = no->text();183no->setEnabled(false);184185QCheckBox* cb = new QCheckBox(&mbox);186cb->setText(tr("Do not show again"));187mbox.setCheckBox(cb);188189int remaining_time = DELAY_SECONDS;190no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));191192QTimer* timer = new QTimer(&mbox);193connect(timer, &QTimer::timeout, &mbox, [no, timer, &remaining_time, &orig_no_text]() {194remaining_time--;195if (remaining_time == 0)196{197no->setText(orig_no_text);198no->setEnabled(true);199timer->stop();200}201else202{203no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));204}205});206timer->start(1000);207208if (mbox.exec() == QMessageBox::Yes)209{210QtUtils::OpenURL(nullptr, "https://duckstation.org/");211QMetaObject::invokeMethod(qApp, &QApplication::quit, Qt::QueuedConnection);212return;213}214215if (cb->isChecked())216Host::SetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, true);217#endif218}219220QStringList AutoUpdaterWindow::getTagList()221{222#ifdef UPDATE_CHECKER_SUPPORTED223return QStringList(std::begin(UPDATE_TAGS), std::end(UPDATE_TAGS));224#else225return QStringList();226#endif227}228229std::string AutoUpdaterWindow::getDefaultTag()230{231#ifdef UPDATE_CHECKER_SUPPORTED232return THIS_RELEASE_TAG;233#else234return {};235#endif236}237238std::string AutoUpdaterWindow::getCurrentUpdateTag() const239{240#ifdef UPDATE_CHECKER_SUPPORTED241return Host::GetBaseStringSettingValue("AutoUpdater", "UpdateTag", THIS_RELEASE_TAG);242#else243return {};244#endif245}246247void AutoUpdaterWindow::reportError(const std::string_view msg)248{249QMessageBox::critical(this, tr("Updater Error"), QtUtils::StringViewToQString(msg));250}251252bool AutoUpdaterWindow::ensureHttpReady()253{254if (!m_http)255return false;256257if (!m_http_poll_timer)258{259m_http_poll_timer = new QTimer(this);260m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterWindow::httpPollTimerPoll);261}262263if (!m_http_poll_timer->isActive())264{265m_http_poll_timer->setSingleShot(false);266m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL);267m_http_poll_timer->start();268}269270return true;271}272273void AutoUpdaterWindow::httpPollTimerPoll()274{275Assert(m_http);276m_http->PollRequests();277278if (!m_http->HasAnyRequests())279{280VERBOSE_LOG("All HTTP requests done.");281m_http_poll_timer->stop();282}283}284285void AutoUpdaterWindow::queueUpdateCheck(bool display_errors)286{287#ifdef UPDATE_CHECKER_SUPPORTED288if (!ensureHttpReady())289{290emit updateCheckCompleted();291return;292}293294m_http->CreateRequest(LATEST_TAG_URL,295[this, display_errors](s32 status_code, const Error& error, const std::string& content_type,296std::vector<u8> response) {297getLatestTagComplete(status_code, error, std::move(response), display_errors);298});299#else300emit updateCheckCompleted();301#endif302}303304void AutoUpdaterWindow::queueGetLatestRelease()305{306#ifdef UPDATE_CHECKER_SUPPORTED307if (!ensureHttpReady())308{309emit updateCheckCompleted();310return;311}312313std::string url = fmt::format(fmt::runtime(LATEST_RELEASE_URL), getCurrentUpdateTag());314m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterWindow::getLatestReleaseComplete, this,315std::placeholders::_1, std::placeholders::_2, std::placeholders::_4));316#endif317}318319void AutoUpdaterWindow::getLatestTagComplete(s32 status_code, const Error& error, std::vector<u8> response,320bool display_errors)321{322#ifdef UPDATE_CHECKER_SUPPORTED323const std::string selected_tag(getCurrentUpdateTag());324const QString selected_tag_qstr = QString::fromStdString(selected_tag);325326if (status_code == HTTPDownloader::HTTP_STATUS_OK)327{328QJsonParseError parse_error;329const QJsonDocument doc = QJsonDocument::fromJson(330QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);331if (doc.isArray())332{333const QJsonArray doc_array(doc.array());334for (const QJsonValue& val : doc_array)335{336if (!val.isObject())337continue;338339if (val["name"].toString() != selected_tag_qstr)340continue;341342m_latest_sha = val["commit"].toObject()["sha"].toString();343if (m_latest_sha.isEmpty())344continue;345346if (updateNeeded())347{348queueGetLatestRelease();349return;350}351else352{353if (display_errors)354{355QMessageBox::information(this, tr("Automatic Updater"),356tr("No updates are currently available. Please try again later."));357}358359emit updateCheckCompleted();360return;361}362}363364if (display_errors)365reportError(fmt::format("{} release not found in JSON", selected_tag));366}367else368{369if (display_errors)370reportError("JSON is not an array");371}372}373else374{375if (display_errors)376reportError(fmt::format("Failed to download latest tag info: {}", error.GetDescription()));377}378379emit updateCheckCompleted();380#endif381}382383void AutoUpdaterWindow::getLatestReleaseComplete(s32 status_code, const Error& error, std::vector<u8> response)384{385#ifdef UPDATE_CHECKER_SUPPORTED386if (status_code == HTTPDownloader::HTTP_STATUS_OK)387{388QJsonParseError parse_error;389const QJsonDocument doc = QJsonDocument::fromJson(390QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);391if (doc.isObject())392{393const QJsonObject doc_object(doc.object());394395#ifdef AUTO_UPDATER_SUPPORTED396// search for the correct file397const QJsonArray assets(doc_object["assets"].toArray());398const QString asset_filename(UPDATE_ASSET_FILENAME);399bool asset_found = false;400for (const QJsonValue& asset : assets)401{402const QJsonObject asset_obj(asset.toObject());403if (asset_obj["name"] == asset_filename)404{405m_download_url = asset_obj["browser_download_url"].toString();406if (!m_download_url.isEmpty())407m_download_size = asset_obj["size"].toInt();408asset_found = true;409break;410}411}412413if (!asset_found)414{415reportError("Asset not found");416return;417}418#endif419420const QString current_date = QtHost::FormatNumber(421Host::NumberFormatType::ShortDateTime,422static_cast<s64>(423QDateTime::fromString(QString::fromUtf8(g_scm_date_str), Qt::DateFormat::ISODate).toSecsSinceEpoch()));424const QString release_date = QtHost::FormatNumber(425Host::NumberFormatType::ShortDateTime,426static_cast<s64>(427QDateTime::fromString(doc_object["published_at"].toString(), Qt::DateFormat::ISODate).toSecsSinceEpoch()));428429m_ui.currentVersion->setText(430tr("Current Version: %1 (%2)")431.arg(QtUtils::StringViewToQString(TinyString::from_format("{}/{}", g_scm_version_str, THIS_RELEASE_TAG)))432.arg(current_date));433m_ui.newVersion->setText(434tr("New Version: %1 (%2)").arg(QString::fromStdString(getCurrentUpdateTag())).arg(release_date));435m_ui.downloadSize->setText(436tr("Download Size: %1 MB").arg(static_cast<double>(m_download_size) / 1000000.0, 0, 'f', 2));437438if (!canInstallUpdate())439{440// Just display the version and a download link.441m_ui.downloadAndInstall->setText(tr("Download..."));442}443444m_ui.downloadAndInstall->setEnabled(true);445m_ui.updateNotes->setText(tr("Loading..."));446queueGetChanges();447QtUtils::ShowOrRaiseWindow(this);448return;449}450else451{452reportError("JSON is not an object");453}454}455else456{457reportError(fmt::format("Failed to download latest release info: {}", error.GetDescription()));458}459460emit updateCheckCompleted();461#endif462}463464void AutoUpdaterWindow::queueGetChanges()465{466#ifdef UPDATE_CHECKER_SUPPORTED467if (!ensureHttpReady())468return;469470std::string url = fmt::format(fmt::runtime(CHANGES_URL), g_scm_hash_str, getCurrentUpdateTag());471m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterWindow::getChangesComplete, this, std::placeholders::_1,472std::placeholders::_2, std::placeholders::_4));473#endif474}475476void AutoUpdaterWindow::getChangesComplete(s32 status_code, const Error& error, std::vector<u8> response)477{478#ifdef UPDATE_CHECKER_SUPPORTED479if (status_code == HTTPDownloader::HTTP_STATUS_OK)480{481QJsonParseError parse_error;482const QJsonDocument doc = QJsonDocument::fromJson(483QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);484if (doc.isObject())485{486const QJsonObject doc_object(doc.object());487488QString changes_html = tr("<h2>Changes:</h2>");489changes_html += QStringLiteral("<ul>");490491const QJsonArray commits(doc_object["commits"].toArray());492bool update_will_break_save_states = false;493bool update_increases_settings_version = false;494495for (const QJsonValue& commit : commits)496{497const QJsonObject commit_obj(commit["commit"].toObject());498499QString message = commit_obj["message"].toString();500QString author = commit_obj["author"].toObject()["name"].toString();501const int first_line_terminator = message.indexOf('\n');502if (first_line_terminator >= 0)503message.remove(first_line_terminator, message.size() - first_line_terminator);504if (!message.isEmpty())505{506changes_html +=507QStringLiteral("<li>%1 <i>(%2)</i></li>").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());508}509510if (message.contains(QStringLiteral("[SAVEVERSION+]")))511update_will_break_save_states = true;512513if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))514update_increases_settings_version = true;515}516517changes_html += "</ul>";518519if (update_will_break_save_states)520{521changes_html.prepend(tr("<h2>Save State Warning</h2><p>Installing this update will make your save states "522"<b>incompatible</b>. Please ensure you have saved your games to memory card "523"before installing this update or you will lose progress.</p>"));524}525526if (update_increases_settings_version)527{528changes_html.prepend(529tr("<h2>Settings Warning</h2><p>Installing this update will reset your program configuration. Please note "530"that you will have to reconfigure your settings after this update.</p>"));531}532533m_ui.updateNotes->setText(changes_html);534}535else536{537reportError("Change list JSON is not an object");538}539}540else541{542reportError(fmt::format("Failed to download change list: {}", error.GetDescription()));543}544#endif545}546547void AutoUpdaterWindow::downloadUpdateClicked()548{549#ifdef UPDATE_CHECKER_SUPPORTED550if (!canInstallUpdate())551{552QtUtils::OpenURL(this, fmt::format(fmt::runtime(DOWNLOAD_PAGE_URL), getCurrentUpdateTag()));553return;554}555#endif556#ifdef AUTO_UPDATER_SUPPORTED557// Prevent multiple clicks of the button.558if (!m_ui.downloadAndInstall->isEnabled())559return;560m_ui.downloadAndInstall->setEnabled(false);561562std::optional<bool> download_result;563QtModalProgressCallback progress(this);564progress.SetTitle(tr("Automatic Updater").toUtf8().constData());565progress.SetStatusText(tr("Downloading %1...").arg(m_latest_sha).toUtf8().constData());566progress.GetDialog().setWindowIcon(windowIcon());567progress.SetCancellable(true);568progress.MakeVisible();569570m_http->CreateRequest(571m_download_url.toStdString(),572[this, &download_result](s32 status_code, const Error& error, const std::string&, std::vector<u8> response) {573if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)574return;575576if (status_code != HTTPDownloader::HTTP_STATUS_OK)577{578reportError(fmt::format("Download failed: {}", error.GetDescription()));579download_result = false;580return;581}582583if (response.empty())584{585reportError("Download failed: Update is empty");586download_result = false;587return;588}589590download_result = processUpdate(response);591},592&progress);593594// Since we're going to block, don't allow the timer to poll, otherwise the progress callback can cause the timer595// to run, and recursively poll again.596m_http_poll_timer->stop();597598// Block until completion.599QtUtils::ProcessEventsWithSleep(600QEventLoop::AllEvents,601[this]() {602m_http->PollRequests();603return m_http->HasAnyRequests();604},605HTTP_POLL_INTERVAL);606607if (download_result.value_or(false))608{609// updater started. since we're a modal on the main window, we have to queue this.610QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit, Qt::QueuedConnection, false);611close();612}613else614{615// update failed, re-enable download button616m_ui.downloadAndInstall->setEnabled(true);617}618#endif619}620621bool AutoUpdaterWindow::updateNeeded() const622{623QString last_checked_sha = QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion"));624625INFO_LOG("Current SHA: {}", g_scm_hash_str);626INFO_LOG("Latest SHA: {}", m_latest_sha.toUtf8().constData());627INFO_LOG("Last Checked SHA: {}", last_checked_sha.toUtf8().constData());628if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha)629{630INFO_LOG("No update needed.");631return false;632}633634INFO_LOG("Update needed.");635return true;636}637638void AutoUpdaterWindow::skipThisUpdateClicked()639{640Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData());641Host::CommitBaseSettingChanges();642close();643}644645void AutoUpdaterWindow::remindMeLaterClicked()646{647close();648}649650void AutoUpdaterWindow::closeEvent(QCloseEvent* event)651{652emit updateCheckCompleted();653QWidget::closeEvent(event);654}655656#ifdef _WIN32657658static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";659static constexpr char UPDATER_ARCHIVE_NAME[] = "update.zip";660661bool AutoUpdaterWindow::doesUpdaterNeedElevation(const std::string& application_dir) const662{663// Try to create a dummy text file in the updater directory. If it fails, we probably won't have write permission.664const std::string dummy_path = Path::Combine(application_dir, "update.txt");665auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");666if (!fp)667return true;668669fp.reset();670FileSystem::DeleteFile(dummy_path.c_str());671return false;672}673674bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)675{676const std::string& application_dir = EmuFolders::AppRoot;677const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);678const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);679680Error error;681if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFile(update_zip_path.c_str(), &error)))682{683reportError(fmt::format("Removing existing update zip failed:\n{}", error.GetDescription()));684return false;685}686687if (!FileSystem::WriteAtomicRenamedFile(update_zip_path.c_str(), update_data, &error))688{689reportError(fmt::format("Writing update zip to '{}' failed:\n{}", update_zip_path, error.GetDescription()));690return false;691}692693Error updater_extract_error;694if (!extractUpdater(update_zip_path.c_str(), updater_path.c_str(), &updater_extract_error))695{696reportError(fmt::format("Extracting updater failed: {}", updater_extract_error.GetDescription()));697return false;698}699700return doUpdate(application_dir, update_zip_path, updater_path);701}702703bool AutoUpdaterWindow::extractUpdater(const std::string& zip_path, const std::string& destination_path, Error* error)704{705unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.c_str());706if (!zf)707{708reportError("Failed to open update zip");709return false;710}711712if (unzLocateFile(zf, UPDATER_EXECUTABLE, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)713{714Error::SetString(error, "Failed to locate updater.exe");715unzClose(zf);716return false;717}718719auto fp = FileSystem::OpenManagedCFile(destination_path.c_str(), "wb", error);720if (!fp)721{722Error::SetString(error, "Failed to open updater.exe for writing");723unzClose(zf);724return false;725}726727static constexpr size_t CHUNK_SIZE = 4096;728char chunk[CHUNK_SIZE];729for (;;)730{731int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);732if (size < 0)733{734Error::SetString(error, "Failed to decompress updater exe");735unzClose(zf);736fp.reset();737FileSystem::DeleteFile(destination_path.c_str());738return false;739}740else if (size == 0)741{742break;743}744745if (std::fwrite(chunk, size, 1, fp.get()) != 1)746{747Error::SetErrno(error, "Failed to write updater exe: fwrite() failed: ", errno);748unzClose(zf);749fp.reset();750FileSystem::DeleteFile(destination_path.c_str());751return false;752}753}754755unzClose(zf);756return true;757}758759bool AutoUpdaterWindow::doUpdate(const std::string& application_dir, const std::string& zip_path,760const std::string& updater_path)761{762const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();763if (program_path.empty())764{765reportError("Failed to get current application path");766return false;767}768769const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);770const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);771const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format(772"{} \"{}\" \"{}\" \"{}\"", QCoreApplication::applicationPid(), application_dir, zip_path, program_path));773774const bool needs_elevation = doesUpdaterNeedElevation(application_dir);775776SHELLEXECUTEINFOW sei = {};777sei.cbSize = sizeof(sei);778sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation779sei.lpFile = wupdater_path.c_str();780sei.lpParameters = arguments.c_str();781sei.lpDirectory = wapplication_dir.c_str();782sei.nShow = SW_SHOWNORMAL;783if (!ShellExecuteExW(&sei))784{785reportError(fmt::format("Failed to start {}: {}", needs_elevation ? "elevated updater" : "updater",786Error::CreateWin32(GetLastError()).GetDescription()));787return false;788}789790return true;791}792793void AutoUpdaterWindow::cleanupAfterUpdate()794{795// If we weren't portable, then updater executable gets left in the application directory.796if (EmuFolders::AppRoot == EmuFolders::DataRoot)797return;798799const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);800if (!FileSystem::FileExists(updater_path.c_str()))801return;802803Error error;804if (!FileSystem::DeleteFile(updater_path.c_str(), &error))805{806QMessageBox::critical(807nullptr, tr("Updater Error"),808tr("Failed to remove updater exe after update:\n%1").arg(QString::fromStdString(error.GetDescription())));809return;810}811}812813#elif defined(__APPLE__)814815bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)816{817std::optional<std::string> bundle_path = CocoaTools::GetNonTranslocatedBundlePath();818if (!bundle_path.has_value())819{820reportError("Couldn't obtain non-translocated bundle path.");821return false;822}823824QFileInfo info(QString::fromStdString(bundle_path.value()));825if (!info.isBundle())826{827reportError(fmt::format("Application {} isn't a bundle.", bundle_path.value()));828return false;829}830if (info.suffix() != QStringLiteral("app"))831{832reportError(833fmt::format("Unexpected application suffix {} on {}.", info.suffix().toStdString(), bundle_path.value()));834return false;835}836837// Use the updater from this version to unpack the new version.838const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app");839if (!FileSystem::DirectoryExists(updater_app.c_str()))840{841reportError(fmt::format("Failed to find updater at {}.", updater_app));842return false;843}844845// We use the user data directory to temporarily store the update zip.846const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip");847const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING");848Error error;849if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str(), &error))850{851reportError(fmt::format("Failed to remove old update zip:\n{}", error.GetDescription()));852return false;853}854855// Save update.856if (!FileSystem::WriteAtomicRenamedFile(zip_path.c_str(), update_data, &error))857{858reportError(fmt::format("Writing update zip to '{}' failed:\n{}", zip_path, error.GetDescription()));859return false;860}861862INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",863updater_app, zip_path, staging_directory, bundle_path.value());864865const std::string_view args[] = {866zip_path,867staging_directory,868bundle_path.value(),869};870871// Kick off updater!872CocoaTools::DelayedLaunch(updater_app, args);873return true;874}875876void AutoUpdaterWindow::cleanupAfterUpdate()877{878}879880#elif defined(__linux__)881882bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)883{884const char* appimage_path = std::getenv("APPIMAGE");885if (!appimage_path || !FileSystem::FileExists(appimage_path))886{887reportError("Missing APPIMAGE.");888return false;889}890891if (!FileSystem::FileExists(appimage_path))892{893reportError(fmt::format("Current AppImage does not exist: {}", appimage_path));894return false;895}896897const std::string new_appimage_path = fmt::format("{}.new", appimage_path);898const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);899INFO_LOG("APPIMAGE = {}", appimage_path);900INFO_LOG("Backup AppImage path = {}", backup_appimage_path);901INFO_LOG("New AppImage path = {}", new_appimage_path);902903// Remove old "new" appimage and existing backup appimage.904Error error;905if (FileSystem::FileExists(new_appimage_path.c_str()) && !FileSystem::DeleteFile(new_appimage_path.c_str(), &error))906{907reportError(908fmt::format("Failed to remove old destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));909return false;910}911if (FileSystem::FileExists(backup_appimage_path.c_str()) &&912!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))913{914reportError(915fmt::format("Failed to remove old backup AppImage: {}:\n{}", backup_appimage_path, error.GetDescription()));916return false;917}918919// Write "new" appimage.920{921// We want to copy the permissions from the old appimage to the new one.922static constexpr int permission_mask = S_IRWXU | S_IRWXG | S_IRWXO;923struct stat old_stat;924if (!FileSystem::StatFile(appimage_path, &old_stat, &error))925{926reportError(fmt::format("Failed to get old AppImage {} permissions:\n{}", appimage_path, error.GetDescription()));927return false;928}929930// We do this as a manual write here, rather than using WriteAtomicUpdatedFile(), because we want to write the file931// and set the permissions as one atomic operation.932FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(new_appimage_path.c_str(), "wb", &error);933bool success = static_cast<bool>(fp);934if (fp)935{936if (std::fwrite(update_data.data(), update_data.size(), 1, fp.get()) == 1 && std::fflush(fp.get()) == 0)937{938const int fd = fileno(fp.get());939if (fd >= 0)940{941if (fchmod(fd, old_stat.st_mode & permission_mask) != 0)942{943error.SetErrno("fchmod() failed: ", errno);944success = false;945}946}947else948{949error.SetErrno("fileno() failed: ", errno);950success = false;951}952}953else954{955error.SetErrno("fwrite() failed: ", errno);956success = false;957}958959fp.reset();960if (!success)961FileSystem::DeleteFile(new_appimage_path.c_str());962}963964if (!success)965{966reportError(967fmt::format("Failed to write new destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));968return false;969}970}971972// Rename "old" appimage.973if (!FileSystem::RenamePath(appimage_path, backup_appimage_path.c_str(), &error))974{975reportError(fmt::format("Failed to rename old AppImage to {}:\n{}", backup_appimage_path, error.GetDescription()));976FileSystem::DeleteFile(new_appimage_path.c_str());977return false;978}979980// Rename "new" appimage.981if (!FileSystem::RenamePath(new_appimage_path.c_str(), appimage_path, &error))982{983reportError(fmt::format("Failed to rename new AppImage to {}:\n{}", appimage_path, error.GetDescription()));984return false;985}986987// Execute new appimage.988QProcess* new_process = new QProcess();989new_process->setProgram(QString::fromUtf8(appimage_path));990new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});991if (!new_process->startDetached())992{993reportError("Failed to execute new AppImage.");994return false;995}996997// We exit once we return.998return true;999}10001001void AutoUpdaterWindow::cleanupAfterUpdate()1002{1003// Remove old/backup AppImage.1004const char* appimage_path = std::getenv("APPIMAGE");1005if (!appimage_path)1006return;10071008const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);1009if (!FileSystem::FileExists(backup_appimage_path.c_str()))1010return;10111012Error error;1013INFO_LOG("Removing backup AppImage: {}", backup_appimage_path);1014if (!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))1015ERROR_LOG("Failed to remove backup AppImage {}: {}", backup_appimage_path, error.GetDescription());1016}10171018#else10191020bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)1021{1022return false;1023}10241025void AutoUpdaterWindow::cleanupAfterUpdate()1026{1027}10281029#endif103010311032