Path: blob/master/src/duckstation-qt/autoupdaterwindow.cpp
4246 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* UPDATE_TAGS[] = SCM_RELEASE_TAGS;68static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG;6970#ifdef AUTO_UPDATER_SUPPORTED71static const char* UPDATE_ASSET_FILENAME = SCM_RELEASE_ASSET;72#else73static const char* DOWNLOAD_PAGE_URL = "https://github.com/stenzek/duckstation/releases/tag/{}";74#endif7576#endif7778LOG_CHANNEL(Host);7980AutoUpdaterWindow::AutoUpdaterWindow(QWidget* parent /* = nullptr */) : QWidget(parent)81{82m_ui.setupUi(this);83setWindowIcon(QtHost::GetAppIcon());84setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);8586connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterWindow::downloadUpdateClicked);87connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterWindow::skipThisUpdateClicked);88connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterWindow::remindMeLaterClicked);8990Error error;91m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent(), &error);92if (!m_http)93ERROR_LOG("Failed to create HTTP downloader, auto updater will not be available:\n{}", error.GetDescription());94}9596AutoUpdaterWindow::~AutoUpdaterWindow() = default;9798bool AutoUpdaterWindow::isSupported()99{100#ifdef UPDATE_CHECKER_SUPPORTED101return true;102#else103return false;104#endif105}106107bool AutoUpdaterWindow::isOfficialBuild()108{109#if !__has_include("scmversion/tag.h")110return false;111#else112return true;113#endif114}115116void AutoUpdaterWindow::warnAboutUnofficialBuild()117{118//119// To those distributing their own builds or packages of DuckStation, and seeing this message:120//121// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.122//123// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.124// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.125// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and126// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and127// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.128//129// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for130// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to131// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream132// changes without attribution, violating copyright.133//134// Thanks, and I hope you understand.135//136137#if !__has_include("scmversion/tag.h")138constexpr const char* CONFIG_SECTION = "UI";139constexpr const char* CONFIG_KEY = "UnofficialBuildWarningConfirmed";140if (141#ifndef _WIN32142!StringUtil::StartsWithNoCase(EmuFolders::AppRoot, "/usr") &&143#endif144Host::GetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, false))145{146return;147}148149constexpr int DELAY_SECONDS = 5;150151const QString message =152QStringLiteral("<h1>You are not using an official release!</h1><h3>DuckStation is licensed under the terms of "153"CC-BY-NC-ND-4.0, which does not allow modified builds to be distributed.</h3>"154"<p>If you are a developer and using a local build, you can check the box below and continue.</p>"155"<p>Otherwise, you should delete this build and download an official release from "156"<a href=\"https://www.duckstation.org/\">duckstation.org</a>.</p><p>Do you want to exit and "157"open this page now?</p>");158159QMessageBox mbox;160mbox.setIcon(QMessageBox::Warning);161mbox.setWindowTitle(QStringLiteral("Unofficial Build Warning"));162mbox.setWindowIcon(QtHost::GetAppIcon());163mbox.setWindowFlag(Qt::CustomizeWindowHint, true);164mbox.setWindowFlag(Qt::WindowCloseButtonHint, false);165mbox.setTextFormat(Qt::RichText);166mbox.setText(message);167168mbox.addButton(QMessageBox::Yes);169QPushButton* no = mbox.addButton(QMessageBox::No);170const QString orig_no_text = no->text();171no->setEnabled(false);172173QCheckBox* cb = new QCheckBox(&mbox);174cb->setText(tr("Do not show again"));175mbox.setCheckBox(cb);176177int remaining_time = DELAY_SECONDS;178no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));179180QTimer* timer = new QTimer(&mbox);181connect(timer, &QTimer::timeout, &mbox, [no, timer, &remaining_time, &orig_no_text]() {182remaining_time--;183if (remaining_time == 0)184{185no->setText(orig_no_text);186no->setEnabled(true);187timer->stop();188}189else190{191no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));192}193});194timer->start(1000);195196if (mbox.exec() == QMessageBox::Yes)197{198QtUtils::OpenURL(nullptr, "https://duckstation.org/");199QMetaObject::invokeMethod(qApp, &QApplication::quit, Qt::QueuedConnection);200return;201}202203if (cb->isChecked())204Host::SetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, true);205#endif206}207208QStringList AutoUpdaterWindow::getTagList()209{210#ifdef UPDATE_CHECKER_SUPPORTED211return QStringList(std::begin(UPDATE_TAGS), std::end(UPDATE_TAGS));212#else213return QStringList();214#endif215}216217std::string AutoUpdaterWindow::getDefaultTag()218{219#ifdef UPDATE_CHECKER_SUPPORTED220return THIS_RELEASE_TAG;221#else222return {};223#endif224}225226std::string AutoUpdaterWindow::getCurrentUpdateTag() const227{228#ifdef UPDATE_CHECKER_SUPPORTED229return Host::GetBaseStringSettingValue("AutoUpdater", "UpdateTag", THIS_RELEASE_TAG);230#else231return {};232#endif233}234235void AutoUpdaterWindow::reportError(const std::string_view msg)236{237QMessageBox::critical(this, tr("Updater Error"), QtUtils::StringViewToQString(msg));238}239240bool AutoUpdaterWindow::ensureHttpReady()241{242if (!m_http)243return false;244245if (!m_http_poll_timer)246{247m_http_poll_timer = new QTimer(this);248m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterWindow::httpPollTimerPoll);249}250251if (!m_http_poll_timer->isActive())252{253m_http_poll_timer->setSingleShot(false);254m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL);255m_http_poll_timer->start();256}257258return true;259}260261void AutoUpdaterWindow::httpPollTimerPoll()262{263Assert(m_http);264m_http->PollRequests();265266if (!m_http->HasAnyRequests())267{268VERBOSE_LOG("All HTTP requests done.");269m_http_poll_timer->stop();270}271}272273void AutoUpdaterWindow::queueUpdateCheck(bool display_errors)274{275#ifdef UPDATE_CHECKER_SUPPORTED276if (!ensureHttpReady())277{278emit updateCheckCompleted();279return;280}281282m_http->CreateRequest(LATEST_TAG_URL,283[this, display_errors](s32 status_code, const Error& error, const std::string& content_type,284std::vector<u8> response) {285getLatestTagComplete(status_code, error, std::move(response), display_errors);286});287#else288emit updateCheckCompleted();289#endif290}291292void AutoUpdaterWindow::queueGetLatestRelease()293{294#ifdef UPDATE_CHECKER_SUPPORTED295if (!ensureHttpReady())296{297emit updateCheckCompleted();298return;299}300301std::string url = fmt::format(fmt::runtime(LATEST_RELEASE_URL), getCurrentUpdateTag());302m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterWindow::getLatestReleaseComplete, this,303std::placeholders::_1, std::placeholders::_2, std::placeholders::_4));304#endif305}306307void AutoUpdaterWindow::getLatestTagComplete(s32 status_code, const Error& error, std::vector<u8> response,308bool display_errors)309{310#ifdef UPDATE_CHECKER_SUPPORTED311const std::string selected_tag(getCurrentUpdateTag());312const QString selected_tag_qstr = QString::fromStdString(selected_tag);313314if (status_code == HTTPDownloader::HTTP_STATUS_OK)315{316QJsonParseError parse_error;317const QJsonDocument doc = QJsonDocument::fromJson(318QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);319if (doc.isArray())320{321const QJsonArray doc_array(doc.array());322for (const QJsonValue& val : doc_array)323{324if (!val.isObject())325continue;326327if (val["name"].toString() != selected_tag_qstr)328continue;329330m_latest_sha = val["commit"].toObject()["sha"].toString();331if (m_latest_sha.isEmpty())332continue;333334if (updateNeeded())335{336queueGetLatestRelease();337return;338}339else340{341if (display_errors)342{343QMessageBox::information(this, tr("Automatic Updater"),344tr("No updates are currently available. Please try again later."));345}346347emit updateCheckCompleted();348return;349}350}351352if (display_errors)353reportError(fmt::format("{} release not found in JSON", selected_tag));354}355else356{357if (display_errors)358reportError("JSON is not an array");359}360}361else362{363if (display_errors)364reportError(fmt::format("Failed to download latest tag info: {}", error.GetDescription()));365}366367emit updateCheckCompleted();368#endif369}370371void AutoUpdaterWindow::getLatestReleaseComplete(s32 status_code, const Error& error, std::vector<u8> response)372{373#ifdef UPDATE_CHECKER_SUPPORTED374if (status_code == HTTPDownloader::HTTP_STATUS_OK)375{376QJsonParseError parse_error;377const QJsonDocument doc = QJsonDocument::fromJson(378QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);379if (doc.isObject())380{381const QJsonObject doc_object(doc.object());382383#ifdef AUTO_UPDATER_SUPPORTED384// search for the correct file385const QJsonArray assets(doc_object["assets"].toArray());386const QString asset_filename(UPDATE_ASSET_FILENAME);387bool asset_found = false;388for (const QJsonValue& asset : assets)389{390const QJsonObject asset_obj(asset.toObject());391if (asset_obj["name"] == asset_filename)392{393m_download_url = asset_obj["browser_download_url"].toString();394if (!m_download_url.isEmpty())395m_download_size = asset_obj["size"].toInt();396asset_found = true;397break;398}399}400401if (!asset_found)402{403reportError("Asset not found");404return;405}406#endif407408const QString current_date = QtHost::FormatNumber(409Host::NumberFormatType::ShortDateTime,410static_cast<s64>(411QDateTime::fromString(QString::fromUtf8(g_scm_date_str), Qt::DateFormat::ISODate).toSecsSinceEpoch()));412const QString release_date = QtHost::FormatNumber(413Host::NumberFormatType::ShortDateTime,414static_cast<s64>(415QDateTime::fromString(doc_object["published_at"].toString(), Qt::DateFormat::ISODate).toSecsSinceEpoch()));416417// strip sha off the version418std::string_view current_version = g_scm_tag_str;419if (std::string_view::size_type pos = current_version.find('-'); pos != std::string_view::npos)420{421if (std::string_view::size_type pos2 = current_version.find('-', pos + 1); pos2 != std::string_view::npos)422current_version = current_version.substr(0, pos2);423}424425m_ui.currentVersion->setText(426tr("Current Version: %1 (%2)")427.arg(QtUtils::StringViewToQString(TinyString::from_format("{}/{}", current_version, THIS_RELEASE_TAG)))428.arg(current_date));429m_ui.newVersion->setText(430tr("New Version: %1 (%2)").arg(QString::fromStdString(getCurrentUpdateTag())).arg(release_date));431m_ui.downloadSize->setText(432tr("Download Size: %1 MB").arg(static_cast<double>(m_download_size) / 1000000.0, 0, 'f', 2));433434#ifndef AUTO_UPDATER_SUPPORTED435// Just display the version and a download link.436m_ui.downloadAndInstall->setText(tr("Download..."));437#endif438439m_ui.downloadAndInstall->setEnabled(true);440m_ui.updateNotes->setText(tr("Loading..."));441queueGetChanges();442QtUtils::ShowOrRaiseWindow(this);443return;444}445else446{447reportError("JSON is not an object");448}449}450else451{452reportError(fmt::format("Failed to download latest release info: {}", error.GetDescription()));453}454455emit updateCheckCompleted();456#endif457}458459void AutoUpdaterWindow::queueGetChanges()460{461#ifdef UPDATE_CHECKER_SUPPORTED462if (!ensureHttpReady())463return;464465std::string url = fmt::format(fmt::runtime(CHANGES_URL), g_scm_hash_str, getCurrentUpdateTag());466m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterWindow::getChangesComplete, this, std::placeholders::_1,467std::placeholders::_2, std::placeholders::_4));468#endif469}470471void AutoUpdaterWindow::getChangesComplete(s32 status_code, const Error& error, std::vector<u8> response)472{473#ifdef UPDATE_CHECKER_SUPPORTED474if (status_code == HTTPDownloader::HTTP_STATUS_OK)475{476QJsonParseError parse_error;477const QJsonDocument doc = QJsonDocument::fromJson(478QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);479if (doc.isObject())480{481const QJsonObject doc_object(doc.object());482483QString changes_html = tr("<h2>Changes:</h2>");484changes_html += QStringLiteral("<ul>");485486const QJsonArray commits(doc_object["commits"].toArray());487bool update_will_break_save_states = false;488bool update_increases_settings_version = false;489490for (const QJsonValue& commit : commits)491{492const QJsonObject commit_obj(commit["commit"].toObject());493494QString message = commit_obj["message"].toString();495QString author = commit_obj["author"].toObject()["name"].toString();496const int first_line_terminator = message.indexOf('\n');497if (first_line_terminator >= 0)498message.remove(first_line_terminator, message.size() - first_line_terminator);499if (!message.isEmpty())500{501changes_html +=502QStringLiteral("<li>%1 <i>(%2)</i></li>").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());503}504505if (message.contains(QStringLiteral("[SAVEVERSION+]")))506update_will_break_save_states = true;507508if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))509update_increases_settings_version = true;510}511512changes_html += "</ul>";513514if (update_will_break_save_states)515{516changes_html.prepend(tr("<h2>Save State Warning</h2><p>Installing this update will make your save states "517"<b>incompatible</b>. Please ensure you have saved your games to memory card "518"before installing this update or you will lose progress.</p>"));519}520521if (update_increases_settings_version)522{523changes_html.prepend(524tr("<h2>Settings Warning</h2><p>Installing this update will reset your program configuration. Please note "525"that you will have to reconfigure your settings after this update.</p>"));526}527528m_ui.updateNotes->setText(changes_html);529}530else531{532reportError("Change list JSON is not an object");533}534}535else536{537reportError(fmt::format("Failed to download change list: {}", error.GetDescription()));538}539#endif540}541542void AutoUpdaterWindow::downloadUpdateClicked()543{544#ifdef AUTO_UPDATER_SUPPORTED545// Prevent multiple clicks of the button.546if (!m_ui.downloadAndInstall->isEnabled())547return;548m_ui.downloadAndInstall->setEnabled(false);549550std::optional<bool> download_result;551QtModalProgressCallback progress(this);552progress.SetTitle(tr("Automatic Updater").toUtf8().constData());553progress.SetStatusText(tr("Downloading %1...").arg(m_latest_sha).toUtf8().constData());554progress.GetDialog().setWindowIcon(windowIcon());555progress.SetCancellable(true);556progress.MakeVisible();557558m_http->CreateRequest(559m_download_url.toStdString(),560[this, &download_result](s32 status_code, const Error& error, const std::string&, std::vector<u8> response) {561if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)562return;563564if (status_code != HTTPDownloader::HTTP_STATUS_OK)565{566reportError(fmt::format("Download failed: {}", error.GetDescription()));567download_result = false;568return;569}570571if (response.empty())572{573reportError("Download failed: Update is empty");574download_result = false;575return;576}577578download_result = processUpdate(response);579},580&progress);581582// Since we're going to block, don't allow the timer to poll, otherwise the progress callback can cause the timer583// to run, and recursively poll again.584m_http_poll_timer->stop();585586// Block until completion.587QtUtils::ProcessEventsWithSleep(588QEventLoop::AllEvents,589[this]() {590m_http->PollRequests();591return m_http->HasAnyRequests();592},593HTTP_POLL_INTERVAL);594595if (download_result.value_or(false))596{597// updater started. since we're a modal on the main window, we have to queue this.598QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, false));599close();600}601else602{603// update failed, re-enable download button604m_ui.downloadAndInstall->setEnabled(true);605}606#elif defined(UPDATE_CHECKER_SUPPORTED)607QtUtils::OpenURL(this, fmt::format(fmt::runtime(DOWNLOAD_PAGE_URL), getCurrentUpdateTag()));608#endif609}610611bool AutoUpdaterWindow::updateNeeded() const612{613QString last_checked_sha = QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion"));614615INFO_LOG("Current SHA: {}", g_scm_hash_str);616INFO_LOG("Latest SHA: {}", m_latest_sha.toUtf8().constData());617INFO_LOG("Last Checked SHA: {}", last_checked_sha.toUtf8().constData());618if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha)619{620INFO_LOG("No update needed.");621return false;622}623624INFO_LOG("Update needed.");625return true;626}627628void AutoUpdaterWindow::skipThisUpdateClicked()629{630Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData());631Host::CommitBaseSettingChanges();632close();633}634635void AutoUpdaterWindow::remindMeLaterClicked()636{637close();638}639640void AutoUpdaterWindow::closeEvent(QCloseEvent* event)641{642emit updateCheckCompleted();643QWidget::closeEvent(event);644}645646#ifdef _WIN32647648static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";649static constexpr char UPDATER_ARCHIVE_NAME[] = "update.zip";650651bool AutoUpdaterWindow::doesUpdaterNeedElevation(const std::string& application_dir) const652{653// Try to create a dummy text file in the updater directory. If it fails, we probably won't have write permission.654const std::string dummy_path = Path::Combine(application_dir, "update.txt");655auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");656if (!fp)657return true;658659fp.reset();660FileSystem::DeleteFile(dummy_path.c_str());661return false;662}663664bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)665{666const std::string& application_dir = EmuFolders::AppRoot;667const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);668const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);669670Error error;671if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFile(update_zip_path.c_str(), &error)))672{673reportError(fmt::format("Removing existing update zip failed:\n{}", error.GetDescription()));674return false;675}676677if (!FileSystem::WriteAtomicRenamedFile(update_zip_path.c_str(), update_data, &error))678{679reportError(fmt::format("Writing update zip to '{}' failed:\n{}", update_zip_path, error.GetDescription()));680return false;681}682683Error updater_extract_error;684if (!extractUpdater(update_zip_path.c_str(), updater_path.c_str(), &updater_extract_error))685{686reportError(fmt::format("Extracting updater failed: {}", updater_extract_error.GetDescription()));687return false;688}689690return doUpdate(application_dir, update_zip_path, updater_path);691}692693bool AutoUpdaterWindow::extractUpdater(const std::string& zip_path, const std::string& destination_path, Error* error)694{695unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.c_str());696if (!zf)697{698reportError("Failed to open update zip");699return false;700}701702if (unzLocateFile(zf, UPDATER_EXECUTABLE, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)703{704Error::SetString(error, "Failed to locate updater.exe");705unzClose(zf);706return false;707}708709auto fp = FileSystem::OpenManagedCFile(destination_path.c_str(), "wb", error);710if (!fp)711{712Error::SetString(error, "Failed to open updater.exe for writing");713unzClose(zf);714return false;715}716717static constexpr size_t CHUNK_SIZE = 4096;718char chunk[CHUNK_SIZE];719for (;;)720{721int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);722if (size < 0)723{724Error::SetString(error, "Failed to decompress updater exe");725unzClose(zf);726fp.reset();727FileSystem::DeleteFile(destination_path.c_str());728return false;729}730else if (size == 0)731{732break;733}734735if (std::fwrite(chunk, size, 1, fp.get()) != 1)736{737Error::SetErrno(error, "Failed to write updater exe: fwrite() failed: ", errno);738unzClose(zf);739fp.reset();740FileSystem::DeleteFile(destination_path.c_str());741return false;742}743}744745unzClose(zf);746return true;747}748749bool AutoUpdaterWindow::doUpdate(const std::string& application_dir, const std::string& zip_path,750const std::string& updater_path)751{752const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();753if (program_path.empty())754{755reportError("Failed to get current application path");756return false;757}758759const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);760const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);761const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format(762"{} \"{}\" \"{}\" \"{}\"", QCoreApplication::applicationPid(), application_dir, zip_path, program_path));763764const bool needs_elevation = doesUpdaterNeedElevation(application_dir);765766SHELLEXECUTEINFOW sei = {};767sei.cbSize = sizeof(sei);768sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation769sei.lpFile = wupdater_path.c_str();770sei.lpParameters = arguments.c_str();771sei.lpDirectory = wapplication_dir.c_str();772sei.nShow = SW_SHOWNORMAL;773if (!ShellExecuteExW(&sei))774{775reportError(fmt::format("Failed to start {}: {}", needs_elevation ? "elevated updater" : "updater",776Error::CreateWin32(GetLastError()).GetDescription()));777return false;778}779780return true;781}782783void AutoUpdaterWindow::cleanupAfterUpdate()784{785// If we weren't portable, then updater executable gets left in the application directory.786if (EmuFolders::AppRoot == EmuFolders::DataRoot)787return;788789const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);790if (!FileSystem::FileExists(updater_path.c_str()))791return;792793Error error;794if (!FileSystem::DeleteFile(updater_path.c_str(), &error))795{796QMessageBox::critical(797nullptr, tr("Updater Error"),798tr("Failed to remove updater exe after update:\n%1").arg(QString::fromStdString(error.GetDescription())));799return;800}801}802803#elif defined(__APPLE__)804805bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)806{807std::optional<std::string> bundle_path = CocoaTools::GetNonTranslocatedBundlePath();808if (!bundle_path.has_value())809{810reportError("Couldn't obtain non-translocated bundle path.");811return false;812}813814QFileInfo info(QString::fromStdString(bundle_path.value()));815if (!info.isBundle())816{817reportError(fmt::format("Application {} isn't a bundle.", bundle_path.value()));818return false;819}820if (info.suffix() != QStringLiteral("app"))821{822reportError(823fmt::format("Unexpected application suffix {} on {}.", info.suffix().toStdString(), bundle_path.value()));824return false;825}826827// Use the updater from this version to unpack the new version.828const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app");829if (!FileSystem::DirectoryExists(updater_app.c_str()))830{831reportError(fmt::format("Failed to find updater at {}.", updater_app));832return false;833}834835// We use the user data directory to temporarily store the update zip.836const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip");837const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING");838Error error;839if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str(), &error))840{841reportError(fmt::format("Failed to remove old update zip:\n{}", error.GetDescription()));842return false;843}844845// Save update.846if (!FileSystem::WriteAtomicRenamedFile(zip_path.c_str(), update_data, &error))847{848reportError(fmt::format("Writing update zip to '{}' failed:\n{}", zip_path, error.GetDescription()));849return false;850}851852INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",853updater_app, zip_path, staging_directory, bundle_path.value());854855const std::string_view args[] = {856zip_path,857staging_directory,858bundle_path.value(),859};860861// Kick off updater!862CocoaTools::DelayedLaunch(updater_app, args);863return true;864}865866void AutoUpdaterWindow::cleanupAfterUpdate()867{868}869870#elif defined(__linux__)871872bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)873{874const char* appimage_path = std::getenv("APPIMAGE");875if (!appimage_path || !FileSystem::FileExists(appimage_path))876{877reportError("Missing APPIMAGE.");878return false;879}880881if (!FileSystem::FileExists(appimage_path))882{883reportError(fmt::format("Current AppImage does not exist: {}", appimage_path));884return false;885}886887const std::string new_appimage_path = fmt::format("{}.new", appimage_path);888const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);889INFO_LOG("APPIMAGE = {}", appimage_path);890INFO_LOG("Backup AppImage path = {}", backup_appimage_path);891INFO_LOG("New AppImage path = {}", new_appimage_path);892893// Remove old "new" appimage and existing backup appimage.894Error error;895if (FileSystem::FileExists(new_appimage_path.c_str()) && !FileSystem::DeleteFile(new_appimage_path.c_str(), &error))896{897reportError(898fmt::format("Failed to remove old destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));899return false;900}901if (FileSystem::FileExists(backup_appimage_path.c_str()) &&902!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))903{904reportError(905fmt::format("Failed to remove old backup AppImage: {}:\n{}", backup_appimage_path, error.GetDescription()));906return false;907}908909// Write "new" appimage.910{911// We want to copy the permissions from the old appimage to the new one.912static constexpr int permission_mask = S_IRWXU | S_IRWXG | S_IRWXO;913struct stat old_stat;914if (!FileSystem::StatFile(appimage_path, &old_stat, &error))915{916reportError(fmt::format("Failed to get old AppImage {} permissions:\n{}", appimage_path, error.GetDescription()));917return false;918}919920// We do this as a manual write here, rather than using WriteAtomicUpdatedFile(), because we want to write the file921// and set the permissions as one atomic operation.922FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(new_appimage_path.c_str(), "wb", &error);923bool success = static_cast<bool>(fp);924if (fp)925{926if (std::fwrite(update_data.data(), update_data.size(), 1, fp.get()) == 1 && std::fflush(fp.get()) == 0)927{928const int fd = fileno(fp.get());929if (fd >= 0)930{931if (fchmod(fd, old_stat.st_mode & permission_mask) != 0)932{933error.SetErrno("fchmod() failed: ", errno);934success = false;935}936}937else938{939error.SetErrno("fileno() failed: ", errno);940success = false;941}942}943else944{945error.SetErrno("fwrite() failed: ", errno);946success = false;947}948949fp.reset();950if (!success)951FileSystem::DeleteFile(new_appimage_path.c_str());952}953954if (!success)955{956reportError(957fmt::format("Failed to write new destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));958return false;959}960}961962// Rename "old" appimage.963if (!FileSystem::RenamePath(appimage_path, backup_appimage_path.c_str(), &error))964{965reportError(fmt::format("Failed to rename old AppImage to {}:\n{}", backup_appimage_path, error.GetDescription()));966FileSystem::DeleteFile(new_appimage_path.c_str());967return false;968}969970// Rename "new" appimage.971if (!FileSystem::RenamePath(new_appimage_path.c_str(), appimage_path, &error))972{973reportError(fmt::format("Failed to rename new AppImage to {}:\n{}", appimage_path, error.GetDescription()));974return false;975}976977// Execute new appimage.978QProcess* new_process = new QProcess();979new_process->setProgram(QString::fromUtf8(appimage_path));980new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});981if (!new_process->startDetached())982{983reportError("Failed to execute new AppImage.");984return false;985}986987// We exit once we return.988return true;989}990991void AutoUpdaterWindow::cleanupAfterUpdate()992{993// Remove old/backup AppImage.994const char* appimage_path = std::getenv("APPIMAGE");995if (!appimage_path)996return;997998const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);999if (!FileSystem::FileExists(backup_appimage_path.c_str()))1000return;10011002Error error;1003INFO_LOG("Removing backup AppImage: {}", backup_appimage_path);1004if (!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))1005ERROR_LOG("Failed to remove backup AppImage {}: {}", backup_appimage_path, error.GetDescription());1006}10071008#else10091010bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)1011{1012return false;1013}10141015void AutoUpdaterWindow::cleanupAfterUpdate()1016{1017}10181019#endif102010211022