duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

autoupdaterdialog.cpp (29280B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "autoupdaterdialog.h"
      5 #include "mainwindow.h"
      6 #include "qthost.h"
      7 #include "qtprogresscallback.h"
      8 #include "qtutils.h"
      9 #include "scmversion/scmversion.h"
     10 #include "unzip.h"
     11 
     12 #include "util/http_downloader.h"
     13 
     14 #include "common/assert.h"
     15 #include "common/error.h"
     16 #include "common/file_system.h"
     17 #include "common/log.h"
     18 #include "common/minizip_helpers.h"
     19 #include "common/path.h"
     20 #include "common/string_util.h"
     21 
     22 #include <QtCore/QCoreApplication>
     23 #include <QtCore/QFile>
     24 #include <QtCore/QJsonArray>
     25 #include <QtCore/QJsonDocument>
     26 #include <QtCore/QJsonObject>
     27 #include <QtCore/QJsonValue>
     28 #include <QtCore/QProcess>
     29 #include <QtCore/QString>
     30 #include <QtCore/QTimer>
     31 #include <QtWidgets/QCheckBox>
     32 #include <QtWidgets/QDialog>
     33 #include <QtWidgets/QMessageBox>
     34 #include <QtWidgets/QProgressDialog>
     35 #include <QtWidgets/QPushButton>
     36 
     37 // Interval at which HTTP requests are polled.
     38 static constexpr u32 HTTP_POLL_INTERVAL = 10;
     39 
     40 #if defined(_WIN32)
     41 #include "common/windows_headers.h"
     42 #include <shellapi.h>
     43 #elif defined(__APPLE__)
     44 #include "common/cocoa_tools.h"
     45 #endif
     46 
     47 // Logic to detect whether we can use the auto updater.
     48 // Requires that the channel be defined by the buildbot.
     49 #if __has_include("scmversion/tag.h")
     50 #include "scmversion/tag.h"
     51 #ifdef SCM_RELEASE_TAGS
     52 #define AUTO_UPDATER_SUPPORTED
     53 #endif
     54 #endif
     55 
     56 #ifdef AUTO_UPDATER_SUPPORTED
     57 
     58 static const char* LATEST_TAG_URL = "https://api.github.com/repos/stenzek/duckstation/tags";
     59 static const char* LATEST_RELEASE_URL = "https://api.github.com/repos/stenzek/duckstation/releases/tags/{}";
     60 static const char* CHANGES_URL = "https://api.github.com/repos/stenzek/duckstation/compare/{}...{}";
     61 static const char* UPDATE_ASSET_FILENAME = SCM_RELEASE_ASSET;
     62 static const char* UPDATE_TAGS[] = SCM_RELEASE_TAGS;
     63 static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG;
     64 
     65 #endif
     66 
     67 Log_SetChannel(AutoUpdaterDialog);
     68 
     69 AutoUpdaterDialog::AutoUpdaterDialog(QWidget* parent /* = nullptr */) : QDialog(parent)
     70 {
     71   m_ui.setupUi(this);
     72 
     73   setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
     74 
     75   connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterDialog::downloadUpdateClicked);
     76   connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterDialog::skipThisUpdateClicked);
     77   connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterDialog::remindMeLaterClicked);
     78 
     79   m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent());
     80   if (!m_http)
     81     ERROR_LOG("Failed to create HTTP downloader, auto updater will not be available.");
     82 }
     83 
     84 AutoUpdaterDialog::~AutoUpdaterDialog() = default;
     85 
     86 bool AutoUpdaterDialog::isSupported()
     87 {
     88 #ifdef AUTO_UPDATER_SUPPORTED
     89 #ifdef __linux__
     90   // For Linux, we need to check whether we're running from the appimage.
     91   if (!std::getenv("APPIMAGE"))
     92   {
     93     INFO_LOG("We're a CI release, but not running from an AppImage. Disabling automatic updater.");
     94     return false;
     95   }
     96 
     97   return true;
     98 #else
     99   // Windows/Mac - always supported.
    100   return true;
    101 #endif
    102 #else
    103   return false;
    104 #endif
    105 }
    106 
    107 bool AutoUpdaterDialog::isOfficialBuild()
    108 {
    109 #if !__has_include("scmversion/tag.h")
    110   return false;
    111 #else
    112   return true;
    113 #endif
    114 }
    115 
    116 bool AutoUpdaterDialog::warnAboutUnofficialBuild()
    117 {
    118   //
    119   // To those distributing their own builds or packages of DuckStation, and seeing this message:
    120   //
    121   // This message is here for a reason. Under the terms of the license, you are within your rights to distribute your
    122   // own builds of my application. However, it is a headache for me, as users run into broken functionality, or end up
    123   // on untested/preview commits that have not been adequately tested, and I cannot resolve their issues. I provide
    124   // builds for a range of platforms that covers almost all use cases, and can guarantee quality of these builds.
    125   //
    126   // If you must distribute builds/packages, per the GPL, modified builds should be clearly marked as such.
    127   // This message is thus one way of meeting the requirement. See Section 5 of the GPLv3:
    128   // https://www.gnu.org/licenses/gpl-3.0.en.html#section5
    129   //
    130   // This includes building the binary with any method that does not match the official release, including dependencies,
    131   // as it is not uncommon for differing dependency versions to create issues I cannot reproduce.
    132   //
    133   // You should also provide user support for your package, and not direct them to upstream, as any users that ask for
    134   // community help will be instructed to download a supported release instead.
    135   //
    136 #if !__has_include("scmversion/tag.h") && !defined(_DEBUG)
    137   constexpr const char* CONFIG_SECTION = "UI";
    138   constexpr const char* CONFIG_KEY = "UnofficialBuildWarningConfirmed";
    139   if (Host::GetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, false))
    140     return true;
    141 
    142   constexpr int DELAY_SECONDS = 5;
    143 
    144   const QString message = QStringLiteral(
    145     "<h1>You are not using an official release!</h1><h3>If you continue to use this build, expect to run into "
    146     "issues.</h3><p><strong>No assistance will be provided by the developers or community</strong>, as we cannot fix "
    147     "broken functionality in builds we do not control.</p><p>We <strong>strongly recommend</strong> downloading an "
    148     "official release from <a href=\"https://www.duckstation.org/\">duckstation.org</a>.</p><p>Do you want to exit and "
    149     "open this page now?</p>");
    150 
    151   QMessageBox mbox;
    152   mbox.setIcon(QMessageBox::Warning);
    153   mbox.setWindowTitle(QStringLiteral("Unofficial Build Warning"));
    154   mbox.setWindowIcon(QtHost::GetAppIcon());
    155   mbox.setTextFormat(Qt::RichText);
    156   mbox.setText(message);
    157 
    158   mbox.addButton(QMessageBox::Yes);
    159   QPushButton* no = mbox.addButton(QMessageBox::No);
    160   const QString orig_no_text = no->text();
    161   no->setEnabled(false);
    162 
    163   QCheckBox* cb = new QCheckBox(&mbox);
    164   cb->setText(tr("Do not show again"));
    165   mbox.setCheckBox(cb);
    166 
    167   int remaining_time = DELAY_SECONDS;
    168   no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));
    169 
    170   QTimer* timer = new QTimer(&mbox);
    171   connect(timer, &QTimer::timeout, &mbox, [no, timer, &remaining_time, &orig_no_text]() {
    172     remaining_time--;
    173     if (remaining_time == 0)
    174     {
    175       no->setText(orig_no_text);
    176       no->setEnabled(true);
    177       timer->stop();
    178     }
    179     else
    180     {
    181       no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));
    182     }
    183   });
    184   timer->start(1000);
    185 
    186   if (mbox.exec() == QMessageBox::Yes)
    187   {
    188     QtUtils::OpenURL(nullptr, "https://duckstation.org/");
    189     return false;
    190   }
    191 
    192   if (cb->isChecked())
    193     Host::SetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, true);
    194 
    195   return true;
    196 #else
    197   return true;
    198 #endif
    199 }
    200 
    201 QStringList AutoUpdaterDialog::getTagList()
    202 {
    203 #ifdef AUTO_UPDATER_SUPPORTED
    204   return QStringList(std::begin(UPDATE_TAGS), std::end(UPDATE_TAGS));
    205 #else
    206   return QStringList();
    207 #endif
    208 }
    209 
    210 std::string AutoUpdaterDialog::getDefaultTag()
    211 {
    212 #ifdef AUTO_UPDATER_SUPPORTED
    213   return THIS_RELEASE_TAG;
    214 #else
    215   return {};
    216 #endif
    217 }
    218 
    219 std::string AutoUpdaterDialog::getCurrentUpdateTag() const
    220 {
    221 #ifdef AUTO_UPDATER_SUPPORTED
    222   return Host::GetBaseStringSettingValue("AutoUpdater", "UpdateTag", THIS_RELEASE_TAG);
    223 #else
    224   return {};
    225 #endif
    226 }
    227 
    228 void AutoUpdaterDialog::reportError(const std::string_view msg)
    229 {
    230   QMessageBox::critical(this, tr("Updater Error"), QtUtils::StringViewToQString(msg));
    231 }
    232 
    233 bool AutoUpdaterDialog::ensureHttpReady()
    234 {
    235   if (!m_http)
    236     return false;
    237 
    238   if (!m_http_poll_timer)
    239   {
    240     m_http_poll_timer = new QTimer(this);
    241     m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterDialog::httpPollTimerPoll);
    242   }
    243 
    244   if (!m_http_poll_timer->isActive())
    245   {
    246     m_http_poll_timer->setSingleShot(false);
    247     m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL);
    248     m_http_poll_timer->start();
    249   }
    250 
    251   return true;
    252 }
    253 
    254 void AutoUpdaterDialog::httpPollTimerPoll()
    255 {
    256   Assert(m_http);
    257   m_http->PollRequests();
    258 
    259   if (!m_http->HasAnyRequests())
    260   {
    261     VERBOSE_LOG("All HTTP requests done.");
    262     m_http_poll_timer->stop();
    263   }
    264 }
    265 
    266 void AutoUpdaterDialog::queueUpdateCheck(bool display_message)
    267 {
    268   m_display_messages = display_message;
    269 
    270 #ifdef AUTO_UPDATER_SUPPORTED
    271   if (!ensureHttpReady())
    272   {
    273     emit updateCheckCompleted();
    274     return;
    275   }
    276 
    277   m_http->CreateRequest(LATEST_TAG_URL, std::bind(&AutoUpdaterDialog::getLatestTagComplete, this, std::placeholders::_1,
    278                                                   std::placeholders::_3));
    279 #else
    280   emit updateCheckCompleted();
    281 #endif
    282 }
    283 
    284 void AutoUpdaterDialog::queueGetLatestRelease()
    285 {
    286 #ifdef AUTO_UPDATER_SUPPORTED
    287   if (!ensureHttpReady())
    288   {
    289     emit updateCheckCompleted();
    290     return;
    291   }
    292 
    293   std::string url = fmt::format(fmt::runtime(LATEST_RELEASE_URL), getCurrentUpdateTag());
    294   m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterDialog::getLatestReleaseComplete, this,
    295                                                   std::placeholders::_1, std::placeholders::_3));
    296 #endif
    297 }
    298 
    299 void AutoUpdaterDialog::getLatestTagComplete(s32 status_code, std::vector<u8> response)
    300 {
    301 #ifdef AUTO_UPDATER_SUPPORTED
    302   const std::string selected_tag(getCurrentUpdateTag());
    303   const QString selected_tag_qstr = QString::fromStdString(selected_tag);
    304 
    305   if (status_code == HTTPDownloader::HTTP_STATUS_OK)
    306   {
    307     QJsonParseError parse_error;
    308     const QJsonDocument doc = QJsonDocument::fromJson(
    309       QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);
    310     if (doc.isArray())
    311     {
    312       const QJsonArray doc_array(doc.array());
    313       for (const QJsonValue& val : doc_array)
    314       {
    315         if (!val.isObject())
    316           continue;
    317 
    318         if (val["name"].toString() != selected_tag_qstr)
    319           continue;
    320 
    321         m_latest_sha = val["commit"].toObject()["sha"].toString();
    322         if (m_latest_sha.isEmpty())
    323           continue;
    324 
    325         if (updateNeeded())
    326         {
    327           queueGetLatestRelease();
    328           return;
    329         }
    330         else
    331         {
    332           if (m_display_messages)
    333             QMessageBox::information(this, tr("Automatic Updater"),
    334                                      tr("No updates are currently available. Please try again later."));
    335           emit updateCheckCompleted();
    336           return;
    337         }
    338       }
    339 
    340       if (m_display_messages)
    341         reportError(fmt::format("{} release not found in JSON", selected_tag));
    342     }
    343     else
    344     {
    345       if (m_display_messages)
    346         reportError("JSON is not an array");
    347     }
    348   }
    349   else
    350   {
    351     if (m_display_messages)
    352       reportError(fmt::format("Failed to download latest tag info: HTTP {}", status_code));
    353   }
    354 
    355   emit updateCheckCompleted();
    356 #endif
    357 }
    358 
    359 void AutoUpdaterDialog::getLatestReleaseComplete(s32 status_code, std::vector<u8> response)
    360 {
    361 #ifdef AUTO_UPDATER_SUPPORTED
    362   if (status_code == HTTPDownloader::HTTP_STATUS_OK)
    363   {
    364     QJsonParseError parse_error;
    365     const QJsonDocument doc = QJsonDocument::fromJson(
    366       QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);
    367     if (doc.isObject())
    368     {
    369       const QJsonObject doc_object(doc.object());
    370 
    371       // search for the correct file
    372       const QJsonArray assets(doc_object["assets"].toArray());
    373       const QString asset_filename(UPDATE_ASSET_FILENAME);
    374       for (const QJsonValue& asset : assets)
    375       {
    376         const QJsonObject asset_obj(asset.toObject());
    377         if (asset_obj["name"] == asset_filename)
    378         {
    379           m_download_url = asset_obj["browser_download_url"].toString();
    380           if (!m_download_url.isEmpty())
    381           {
    382             m_download_size = asset_obj["size"].toInt();
    383             m_ui.currentVersion->setText(tr("Current Version: %1 (%2)").arg(g_scm_hash_str).arg(g_scm_date_str));
    384             m_ui.newVersion->setText(
    385               tr("New Version: %1 (%2)").arg(m_latest_sha).arg(doc_object["published_at"].toString()));
    386             m_ui.updateNotes->setText(tr("Loading..."));
    387             m_ui.downloadAndInstall->setEnabled(true);
    388             queueGetChanges();
    389 
    390             // We have to defer this, because it comes back through the timer/HTTP callback...
    391             QMetaObject::invokeMethod(this, "exec", Qt::QueuedConnection);
    392 
    393             emit updateCheckCompleted();
    394             return;
    395           }
    396 
    397           break;
    398         }
    399       }
    400 
    401       reportError("Asset/asset download not found");
    402     }
    403     else
    404     {
    405       reportError("JSON is not an object");
    406     }
    407   }
    408   else
    409   {
    410     reportError(fmt::format("Failed to download latest release info: HTTP {}", status_code));
    411   }
    412 
    413   emit updateCheckCompleted();
    414 #endif
    415 }
    416 
    417 void AutoUpdaterDialog::queueGetChanges()
    418 {
    419 #ifdef AUTO_UPDATER_SUPPORTED
    420   if (!ensureHttpReady())
    421     return;
    422 
    423   std::string url = fmt::format(fmt::runtime(CHANGES_URL), g_scm_hash_str, getCurrentUpdateTag());
    424   m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterDialog::getChangesComplete, this, std::placeholders::_1,
    425                                                   std::placeholders::_3));
    426 #endif
    427 }
    428 
    429 void AutoUpdaterDialog::getChangesComplete(s32 status_code, std::vector<u8> response)
    430 {
    431 #ifdef AUTO_UPDATER_SUPPORTED
    432   if (status_code == HTTPDownloader::HTTP_STATUS_OK)
    433   {
    434     QJsonParseError parse_error;
    435     const QJsonDocument doc = QJsonDocument::fromJson(
    436       QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);
    437     if (doc.isObject())
    438     {
    439       const QJsonObject doc_object(doc.object());
    440 
    441       QString changes_html = tr("<h2>Changes:</h2>");
    442       changes_html += QStringLiteral("<ul>");
    443 
    444       const QJsonArray commits(doc_object["commits"].toArray());
    445       bool update_will_break_save_states = false;
    446       bool update_increases_settings_version = false;
    447 
    448       for (const QJsonValue& commit : commits)
    449       {
    450         const QJsonObject commit_obj(commit["commit"].toObject());
    451 
    452         QString message = commit_obj["message"].toString();
    453         QString author = commit_obj["author"].toObject()["name"].toString();
    454         const int first_line_terminator = message.indexOf('\n');
    455         if (first_line_terminator >= 0)
    456           message.remove(first_line_terminator, message.size() - first_line_terminator);
    457         if (!message.isEmpty())
    458         {
    459           changes_html +=
    460             QStringLiteral("<li>%1 <i>(%2)</i></li>").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());
    461         }
    462 
    463         if (message.contains(QStringLiteral("[SAVEVERSION+]")))
    464           update_will_break_save_states = true;
    465 
    466         if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))
    467           update_increases_settings_version = true;
    468       }
    469 
    470       changes_html += "</ul>";
    471 
    472       if (update_will_break_save_states)
    473       {
    474         changes_html.prepend(tr("<h2>Save State Warning</h2><p>Installing this update will make your save states "
    475                                 "<b>incompatible</b>. Please ensure you have saved your games to memory card "
    476                                 "before installing this update or you will lose progress.</p>"));
    477       }
    478 
    479       if (update_increases_settings_version)
    480       {
    481         changes_html.prepend(
    482           tr("<h2>Settings Warning</h2><p>Installing this update will reset your program configuration. Please note "
    483              "that you will have to reconfigure your settings after this update.</p>"));
    484       }
    485 
    486       changes_html += tr("<h4>Installing this update will download %1 MB through your internet connection.</h4>")
    487                         .arg(static_cast<double>(m_download_size) / 1000000.0, 0, 'f', 2);
    488 
    489       m_ui.updateNotes->setText(changes_html);
    490     }
    491     else
    492     {
    493       reportError("Change list JSON is not an object");
    494     }
    495   }
    496   else
    497   {
    498     reportError(fmt::format("Failed to download change list: HTTP {}", status_code));
    499   }
    500 #endif
    501 }
    502 
    503 void AutoUpdaterDialog::downloadUpdateClicked()
    504 {
    505   m_display_messages = true;
    506 
    507   std::optional<bool> download_result;
    508   QtModalProgressCallback progress(this);
    509   progress.SetTitle(tr("Automatic Updater").toUtf8().constData());
    510   progress.SetStatusText(tr("Downloading %1...").arg(m_latest_sha).toUtf8().constData());
    511   progress.GetDialog().setWindowIcon(windowIcon());
    512   progress.SetCancellable(true);
    513 
    514   m_http->CreateRequest(
    515     m_download_url.toStdString(),
    516     [this, &download_result](s32 status_code, const std::string&, std::vector<u8> response) {
    517       if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)
    518         return;
    519 
    520       if (status_code != HTTPDownloader::HTTP_STATUS_OK)
    521       {
    522         reportError(fmt::format("Download failed: HTTP status code {}", status_code));
    523         download_result = false;
    524         return;
    525       }
    526 
    527       if (response.empty())
    528       {
    529         reportError("Download failed: Update is empty");
    530         download_result = false;
    531         return;
    532       }
    533 
    534       download_result = processUpdate(response);
    535     },
    536     &progress);
    537 
    538   // Since we're going to block, don't allow the timer to poll, otherwise the progress callback can cause the timer to
    539   // run, and recursively poll again.
    540   m_http_poll_timer->stop();
    541 
    542   // Block until completion.
    543   while (m_http->HasAnyRequests())
    544   {
    545     QApplication::processEvents(QEventLoop::AllEvents, HTTP_POLL_INTERVAL);
    546     m_http->PollRequests();
    547   }
    548 
    549   if (download_result.value_or(false))
    550   {
    551     // updater started. since we're a modal on the main window, we have to queue this.
    552     QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, true));
    553     done(0);
    554   }
    555 }
    556 
    557 bool AutoUpdaterDialog::updateNeeded() const
    558 {
    559   QString last_checked_sha = QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion"));
    560 
    561   INFO_LOG("Current SHA: {}", g_scm_hash_str);
    562   INFO_LOG("Latest SHA: {}", m_latest_sha.toUtf8().constData());
    563   INFO_LOG("Last Checked SHA: {}", last_checked_sha.toUtf8().constData());
    564   if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha)
    565   {
    566     INFO_LOG("No update needed.");
    567     return false;
    568   }
    569 
    570   INFO_LOG("Update needed.");
    571   return true;
    572 }
    573 
    574 void AutoUpdaterDialog::skipThisUpdateClicked()
    575 {
    576   Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData());
    577   Host::CommitBaseSettingChanges();
    578   done(0);
    579 }
    580 
    581 void AutoUpdaterDialog::remindMeLaterClicked()
    582 {
    583   done(0);
    584 }
    585 
    586 #ifdef _WIN32
    587 
    588 static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";
    589 static constexpr char UPDATER_ARCHIVE_NAME[] = "update.zip";
    590 
    591 bool AutoUpdaterDialog::doesUpdaterNeedElevation(const std::string& application_dir) const
    592 {
    593   // Try to create a dummy text file in the PCSX2 updater directory. If it fails, we probably won't have write
    594   // permission.
    595   const std::string dummy_path = Path::Combine(application_dir, "update.txt");
    596   auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");
    597   if (!fp)
    598     return true;
    599 
    600   fp.reset();
    601   FileSystem::DeleteFile(dummy_path.c_str());
    602   return false;
    603 }
    604 
    605 bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
    606 {
    607   const std::string& application_dir = EmuFolders::AppRoot;
    608   const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);
    609   const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
    610 
    611   if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFile(update_zip_path.c_str())))
    612   {
    613     reportError("Removing existing update zip failed");
    614     return false;
    615   }
    616 
    617   if (!FileSystem::WriteBinaryFile(update_zip_path.c_str(), update_data.data(), update_data.size()))
    618   {
    619     reportError(fmt::format("Writing update zip to '{}' failed", update_zip_path));
    620     return false;
    621   }
    622 
    623   Error updater_extract_error;
    624   if (!extractUpdater(update_zip_path.c_str(), updater_path.c_str(), &updater_extract_error))
    625   {
    626     reportError(fmt::format("Extracting updater failed: {}", updater_extract_error.GetDescription()));
    627     return false;
    628   }
    629 
    630   return doUpdate(application_dir, update_zip_path, updater_path);
    631 }
    632 
    633 bool AutoUpdaterDialog::extractUpdater(const std::string& zip_path, const std::string& destination_path, Error* error)
    634 {
    635   unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.c_str());
    636   if (!zf)
    637   {
    638     reportError("Failed to open update zip");
    639     return false;
    640   }
    641 
    642   if (unzLocateFile(zf, UPDATER_EXECUTABLE, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)
    643   {
    644     Error::SetString(error, "Failed to locate updater.exe");
    645     unzClose(zf);
    646     return false;
    647   }
    648 
    649   auto fp = FileSystem::OpenManagedCFile(destination_path.c_str(), "wb", error);
    650   if (!fp)
    651   {
    652     Error::SetString(error, "Failed to open updater.exe for writing");
    653     unzClose(zf);
    654     return false;
    655   }
    656 
    657   static constexpr size_t CHUNK_SIZE = 4096;
    658   char chunk[CHUNK_SIZE];
    659   for (;;)
    660   {
    661     int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);
    662     if (size < 0)
    663     {
    664       Error::SetString(error, "Failed to decompress updater exe");
    665       unzClose(zf);
    666       fp.reset();
    667       FileSystem::DeleteFile(destination_path.c_str());
    668       return false;
    669     }
    670     else if (size == 0)
    671     {
    672       break;
    673     }
    674 
    675     if (std::fwrite(chunk, size, 1, fp.get()) != 1)
    676     {
    677       Error::SetString(error, "Failed to write updater exe");
    678       unzClose(zf);
    679       fp.reset();
    680       FileSystem::DeleteFile(destination_path.c_str());
    681       return false;
    682     }
    683   }
    684 
    685   unzClose(zf);
    686   return true;
    687 }
    688 
    689 bool AutoUpdaterDialog::doUpdate(const std::string& application_dir, const std::string& zip_path,
    690                                  const std::string& updater_path)
    691 {
    692   const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();
    693   if (program_path.empty())
    694   {
    695     reportError("Failed to get current application path");
    696     return false;
    697   }
    698 
    699   const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);
    700   const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);
    701   const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format(
    702     "{} \"{}\" \"{}\" \"{}\"", QCoreApplication::applicationPid(), application_dir, zip_path, program_path));
    703 
    704   const bool needs_elevation = doesUpdaterNeedElevation(application_dir);
    705 
    706   SHELLEXECUTEINFOW sei = {};
    707   sei.cbSize = sizeof(sei);
    708   sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation
    709   sei.lpFile = wupdater_path.c_str();
    710   sei.lpParameters = arguments.c_str();
    711   sei.lpDirectory = wapplication_dir.c_str();
    712   sei.nShow = SW_SHOWNORMAL;
    713   if (!ShellExecuteExW(&sei))
    714   {
    715     reportError(fmt::format("Failed to start {}: {}", needs_elevation ? "elevated updater" : "updater",
    716                             Error::CreateWin32(GetLastError()).GetDescription()));
    717     return false;
    718   }
    719 
    720   return true;
    721 }
    722 
    723 void AutoUpdaterDialog::cleanupAfterUpdate()
    724 {
    725   // If we weren't portable, then updater executable gets left in the application directory.
    726   if (EmuFolders::AppRoot == EmuFolders::DataRoot)
    727     return;
    728 
    729   const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
    730   if (!FileSystem::FileExists(updater_path.c_str()))
    731     return;
    732 
    733   if (!FileSystem::DeleteFile(updater_path.c_str()))
    734   {
    735     QMessageBox::critical(nullptr, tr("Updater Error"), tr("Failed to remove updater exe after update."));
    736     return;
    737   }
    738 }
    739 
    740 #elif defined(__APPLE__)
    741 
    742 bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
    743 {
    744   std::optional<std::string> bundle_path = CocoaTools::GetNonTranslocatedBundlePath();
    745   if (!bundle_path.has_value())
    746   {
    747     reportError("Couldn't obtain non-translocated bundle path.");
    748     return false;
    749   }
    750 
    751   QFileInfo info(QString::fromStdString(bundle_path.value()));
    752   if (!info.isBundle())
    753   {
    754     reportError(fmt::format("Application {} isn't a bundle.", bundle_path.value()));
    755     return false;
    756   }
    757   if (info.suffix() != QStringLiteral("app"))
    758   {
    759     reportError(
    760       fmt::format("Unexpected application suffix {} on {}.", info.suffix().toStdString(), bundle_path.value()));
    761     return false;
    762   }
    763 
    764   // Use the updater from this version to unpack the new version.
    765   const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app");
    766   if (!FileSystem::DirectoryExists(updater_app.c_str()))
    767   {
    768     reportError(fmt::format("Failed to find updater at {}.", updater_app));
    769     return false;
    770   }
    771 
    772   // We use the user data directory to temporarily store the update zip.
    773   const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip");
    774   const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING");
    775   if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str()))
    776   {
    777     reportError("Failed to remove old update zip.");
    778     return false;
    779   }
    780 
    781   // Save update.
    782   {
    783     QFile zip_file(QString::fromStdString(zip_path));
    784     if (!zip_file.open(QIODevice::WriteOnly) ||
    785         zip_file.write(reinterpret_cast<const char*>(update_data.data()), static_cast<qint64>(update_data.size())) !=
    786           static_cast<qint64>(update_data.size()))
    787     {
    788       reportError(fmt::format("Writing update zip to '{}' failed", zip_path));
    789       return false;
    790     }
    791     zip_file.close();
    792   }
    793 
    794   INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",
    795            updater_app, zip_path, staging_directory, bundle_path.value());
    796 
    797   const std::string_view args[] = {
    798     zip_path,
    799     staging_directory,
    800     bundle_path.value(),
    801   };
    802 
    803   // Kick off updater!
    804   CocoaTools::DelayedLaunch(updater_app, args);
    805   return true;
    806 }
    807 
    808 void AutoUpdaterDialog::cleanupAfterUpdate()
    809 {
    810 }
    811 
    812 #elif defined(__linux__)
    813 
    814 bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
    815 {
    816   const char* appimage_path = std::getenv("APPIMAGE");
    817   if (!appimage_path || !FileSystem::FileExists(appimage_path))
    818   {
    819     reportError("Missing APPIMAGE.");
    820     return false;
    821   }
    822 
    823   const QString qappimage_path(QString::fromUtf8(appimage_path));
    824   if (!QFile::exists(qappimage_path))
    825   {
    826     reportError(fmt::format("Current AppImage does not exist: {}", appimage_path));
    827     return false;
    828   }
    829 
    830   const QString new_appimage_path(qappimage_path + QStringLiteral(".new"));
    831   const QString backup_appimage_path(qappimage_path + QStringLiteral(".backup"));
    832   INFO_LOG("APPIMAGE = {}", appimage_path);
    833   INFO_LOG("Backup AppImage path = {}", backup_appimage_path.toStdString());
    834   INFO_LOG("New AppImage path = {}", new_appimage_path.toStdString());
    835 
    836   // Remove old "new" appimage and existing backup appimage.
    837   if (QFile::exists(new_appimage_path) && !QFile::remove(new_appimage_path))
    838   {
    839     reportError(fmt::format("Failed to remove old destination AppImage: {}", new_appimage_path.toStdString()));
    840     return false;
    841   }
    842   if (QFile::exists(backup_appimage_path) && !QFile::remove(backup_appimage_path))
    843   {
    844     reportError(fmt::format("Failed to remove old backup AppImage: {}", new_appimage_path.toStdString()));
    845     return false;
    846   }
    847 
    848   // Write "new" appimage.
    849   {
    850     // We want to copy the permissions from the old appimage to the new one.
    851     QFile old_file(qappimage_path);
    852     const QFileDevice::Permissions old_permissions = old_file.permissions();
    853     QFile new_file(new_appimage_path);
    854     if (!new_file.open(QIODevice::WriteOnly) ||
    855         new_file.write(reinterpret_cast<const char*>(update_data.data()), static_cast<qint64>(update_data.size())) !=
    856           static_cast<qint64>(update_data.size()) ||
    857         !new_file.setPermissions(old_permissions))
    858     {
    859       QFile::remove(new_appimage_path);
    860       reportError(fmt::format("Failed to write new destination AppImage: {}", new_appimage_path.toStdString()));
    861       return false;
    862     }
    863   }
    864 
    865   // Rename "old" appimage.
    866   if (!QFile::rename(qappimage_path, backup_appimage_path))
    867   {
    868     reportError(fmt::format("Failed to rename old AppImage to {}", backup_appimage_path.toStdString()));
    869     QFile::remove(new_appimage_path);
    870     return false;
    871   }
    872 
    873   // Rename "new" appimage.
    874   if (!QFile::rename(new_appimage_path, qappimage_path))
    875   {
    876     reportError(fmt::format("Failed to rename new AppImage to {}", qappimage_path.toStdString()));
    877     return false;
    878   }
    879 
    880   // Execute new appimage.
    881   QProcess* new_process = new QProcess();
    882   new_process->setProgram(qappimage_path);
    883   new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});
    884   if (!new_process->startDetached())
    885   {
    886     reportError("Failed to execute new AppImage.");
    887     return false;
    888   }
    889 
    890   // We exit once we return.
    891   return true;
    892 }
    893 
    894 void AutoUpdaterDialog::cleanupAfterUpdate()
    895 {
    896   // Remove old/backup AppImage.
    897   const char* appimage_path = std::getenv("APPIMAGE");
    898   if (!appimage_path)
    899     return;
    900 
    901   const QString qappimage_path(QString::fromUtf8(appimage_path));
    902   const QString backup_appimage_path(qappimage_path + QStringLiteral(".backup"));
    903   if (!QFile::exists(backup_appimage_path))
    904     return;
    905 
    906   INFO_LOG(QStringLiteral("Removing backup AppImage %1").arg(backup_appimage_path).toStdString().c_str());
    907   if (!QFile::remove(backup_appimage_path))
    908   {
    909     ERROR_LOG(QStringLiteral("Failed to remove backup AppImage %1").arg(backup_appimage_path).toStdString().c_str());
    910   }
    911 }
    912 
    913 #else
    914 
    915 bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
    916 {
    917   return false;
    918 }
    919 
    920 void AutoUpdaterDialog::cleanupAfterUpdate()
    921 {
    922 }
    923 
    924 #endif