Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/duckstation-qt/autoupdaterwindow.cpp
4802 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "autoupdaterwindow.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 "fmt/format.h"
23
24
#include <QtCore/QCoreApplication>
25
#include <QtCore/QFileInfo>
26
#include <QtCore/QJsonArray>
27
#include <QtCore/QJsonDocument>
28
#include <QtCore/QJsonObject>
29
#include <QtCore/QJsonValue>
30
#include <QtCore/QProcess>
31
#include <QtCore/QString>
32
#include <QtCore/QTimer>
33
#include <QtWidgets/QCheckBox>
34
#include <QtWidgets/QDialog>
35
#include <QtWidgets/QMessageBox>
36
#include <QtWidgets/QProgressDialog>
37
#include <QtWidgets/QPushButton>
38
39
#include "moc_autoupdaterwindow.cpp"
40
41
// Interval at which HTTP requests are polled.
42
static constexpr u32 HTTP_POLL_INTERVAL = 10;
43
44
#if defined(_WIN32)
45
#include "common/windows_headers.h"
46
#include <shellapi.h>
47
#elif defined(__APPLE__)
48
#include "common/cocoa_tools.h"
49
#else
50
#include <sys/stat.h>
51
#endif
52
53
// Logic to detect whether we can use the auto updater.
54
// Requires that the channel be defined by the buildbot.
55
#if __has_include("scmversion/tag.h")
56
#include "scmversion/tag.h"
57
#define UPDATE_CHECKER_SUPPORTED
58
#ifdef SCM_RELEASE_ASSET
59
#define AUTO_UPDATER_SUPPORTED
60
#endif
61
#endif
62
63
#ifdef UPDATE_CHECKER_SUPPORTED
64
65
static const char* LATEST_TAG_URL = "https://api.github.com/repos/stenzek/duckstation/tags";
66
static const char* LATEST_RELEASE_URL = "https://api.github.com/repos/stenzek/duckstation/releases/tags/{}";
67
static const char* CHANGES_URL = "https://api.github.com/repos/stenzek/duckstation/compare/{}...{}";
68
static const char* DOWNLOAD_PAGE_URL = "https://github.com/stenzek/duckstation/releases/tag/{}";
69
static const char* UPDATE_TAGS[] = SCM_RELEASE_TAGS;
70
static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG;
71
72
#ifdef AUTO_UPDATER_SUPPORTED
73
static const char* UPDATE_ASSET_FILENAME = SCM_RELEASE_ASSET;
74
#endif
75
76
#endif
77
78
LOG_CHANNEL(Host);
79
80
AutoUpdaterWindow::AutoUpdaterWindow(QWidget* parent /* = nullptr */) : QWidget(parent)
81
{
82
m_ui.setupUi(this);
83
setWindowIcon(QtHost::GetAppIcon());
84
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
85
86
connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterWindow::downloadUpdateClicked);
87
connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterWindow::skipThisUpdateClicked);
88
connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterWindow::remindMeLaterClicked);
89
90
Error error;
91
m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent(), &error);
92
if (!m_http)
93
ERROR_LOG("Failed to create HTTP downloader, auto updater will not be available:\n{}", error.GetDescription());
94
}
95
96
AutoUpdaterWindow::~AutoUpdaterWindow() = default;
97
98
bool AutoUpdaterWindow::isSupported()
99
{
100
#ifdef UPDATE_CHECKER_SUPPORTED
101
return true;
102
#else
103
return false;
104
#endif
105
}
106
107
bool AutoUpdaterWindow::canInstallUpdate()
108
{
109
#ifndef AUTO_UPDATER_SUPPORTED
110
return false;
111
#elif defined(__linux__)
112
// Linux Flatpak is a wrapper of the AppImage, which will have the AUTO_UPDATER_SUPPORTED flag set.
113
// Redirect to the download page instead if not running under an AppImage.
114
return (std::getenv("APPIMAGE") != nullptr);
115
#else
116
return true;
117
#endif
118
}
119
120
bool AutoUpdaterWindow::isOfficialBuild()
121
{
122
#if !__has_include("scmversion/tag.h")
123
return false;
124
#else
125
return true;
126
#endif
127
}
128
129
void AutoUpdaterWindow::warnAboutUnofficialBuild()
130
{
131
//
132
// To those distributing their own builds or packages of DuckStation, and seeing this message:
133
//
134
// DuckStation is licensed under the CC-BY-NC-ND-4.0 license.
135
//
136
// This means that you do NOT have permission to re-distribute your own modified builds of DuckStation.
137
// Modifying DuckStation for personal use is fine, but you cannot distribute builds with your changes.
138
// As per the CC-BY-NC-ND conditions, you can re-distribute the official builds from https://www.duckstation.org/ and
139
// https://github.com/stenzek/duckstation, so long as they are left intact, without modification. I welcome and
140
// appreciate any pull requests made to the official repository at https://github.com/stenzek/duckstation.
141
//
142
// I made the decision to switch to a no-derivatives license because of numerous "forks" that were created purely for
143
// generating money for the person who knocked it off, and always died, leaving the community with multiple builds to
144
// choose from, most of which were out of date and broken, and endless confusion. Other forks copy/pasted upstream
145
// changes without attribution, violating copyright.
146
//
147
// Thanks, and I hope you understand.
148
//
149
150
#if !__has_include("scmversion/tag.h")
151
constexpr const char* CONFIG_SECTION = "UI";
152
constexpr const char* CONFIG_KEY = "UnofficialBuildWarningConfirmed";
153
if (
154
#ifndef _WIN32
155
!StringUtil::StartsWithNoCase(EmuFolders::AppRoot, "/usr") &&
156
#endif
157
Host::GetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, false))
158
{
159
return;
160
}
161
162
constexpr int DELAY_SECONDS = 5;
163
164
const QString message =
165
QStringLiteral("<h1>You are not using an official release!</h1><h3>DuckStation is licensed under the terms of "
166
"CC-BY-NC-ND-4.0, which does not allow modified builds to be distributed.</h3>"
167
"<p>If you are a developer and using a local build, you can check the box below and continue.</p>"
168
"<p>Otherwise, you should delete this build and download an official release from "
169
"<a href=\"https://www.duckstation.org/\">duckstation.org</a>.</p><p>Do you want to exit and "
170
"open this page now?</p>");
171
172
QMessageBox mbox;
173
mbox.setIcon(QMessageBox::Warning);
174
mbox.setWindowTitle(QStringLiteral("Unofficial Build Warning"));
175
mbox.setWindowIcon(QtHost::GetAppIcon());
176
mbox.setWindowFlag(Qt::CustomizeWindowHint, true);
177
mbox.setWindowFlag(Qt::WindowCloseButtonHint, false);
178
mbox.setTextFormat(Qt::RichText);
179
mbox.setText(message);
180
181
mbox.addButton(QMessageBox::Yes);
182
QPushButton* no = mbox.addButton(QMessageBox::No);
183
const QString orig_no_text = no->text();
184
no->setEnabled(false);
185
186
QCheckBox* cb = new QCheckBox(&mbox);
187
cb->setText(tr("Do not show again"));
188
mbox.setCheckBox(cb);
189
190
int remaining_time = DELAY_SECONDS;
191
no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));
192
193
QTimer* timer = new QTimer(&mbox);
194
connect(timer, &QTimer::timeout, &mbox, [no, timer, &remaining_time, &orig_no_text]() {
195
remaining_time--;
196
if (remaining_time == 0)
197
{
198
no->setText(orig_no_text);
199
no->setEnabled(true);
200
timer->stop();
201
}
202
else
203
{
204
no->setText(QStringLiteral("%1 [%2]").arg(orig_no_text).arg(remaining_time));
205
}
206
});
207
timer->start(1000);
208
209
if (mbox.exec() == QMessageBox::Yes)
210
{
211
QtUtils::OpenURL(nullptr, "https://duckstation.org/");
212
QMetaObject::invokeMethod(qApp, &QApplication::quit, Qt::QueuedConnection);
213
return;
214
}
215
216
if (cb->isChecked())
217
Host::SetBaseBoolSettingValue(CONFIG_SECTION, CONFIG_KEY, true);
218
#endif
219
}
220
221
QStringList AutoUpdaterWindow::getTagList()
222
{
223
#ifdef UPDATE_CHECKER_SUPPORTED
224
return QStringList(std::begin(UPDATE_TAGS), std::end(UPDATE_TAGS));
225
#else
226
return QStringList();
227
#endif
228
}
229
230
std::string AutoUpdaterWindow::getDefaultTag()
231
{
232
#ifdef UPDATE_CHECKER_SUPPORTED
233
return THIS_RELEASE_TAG;
234
#else
235
return {};
236
#endif
237
}
238
239
std::string AutoUpdaterWindow::getCurrentUpdateTag() const
240
{
241
#ifdef UPDATE_CHECKER_SUPPORTED
242
return Host::GetBaseStringSettingValue("AutoUpdater", "UpdateTag", THIS_RELEASE_TAG);
243
#else
244
return {};
245
#endif
246
}
247
248
void AutoUpdaterWindow::reportError(const std::string_view msg)
249
{
250
QMessageBox::critical(this, tr("Updater Error"), QtUtils::StringViewToQString(msg));
251
}
252
253
bool AutoUpdaterWindow::ensureHttpReady()
254
{
255
if (!m_http)
256
return false;
257
258
if (!m_http_poll_timer)
259
{
260
m_http_poll_timer = new QTimer(this);
261
m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterWindow::httpPollTimerPoll);
262
}
263
264
if (!m_http_poll_timer->isActive())
265
{
266
m_http_poll_timer->setSingleShot(false);
267
m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL);
268
m_http_poll_timer->start();
269
}
270
271
return true;
272
}
273
274
void AutoUpdaterWindow::httpPollTimerPoll()
275
{
276
Assert(m_http);
277
m_http->PollRequests();
278
279
if (!m_http->HasAnyRequests())
280
{
281
VERBOSE_LOG("All HTTP requests done.");
282
m_http_poll_timer->stop();
283
}
284
}
285
286
void AutoUpdaterWindow::queueUpdateCheck(bool display_errors)
287
{
288
#ifdef UPDATE_CHECKER_SUPPORTED
289
if (!ensureHttpReady())
290
{
291
emit updateCheckCompleted();
292
return;
293
}
294
295
m_http->CreateRequest(LATEST_TAG_URL,
296
[this, display_errors](s32 status_code, const Error& error, const std::string& content_type,
297
std::vector<u8> response) {
298
getLatestTagComplete(status_code, error, std::move(response), display_errors);
299
});
300
#else
301
emit updateCheckCompleted();
302
#endif
303
}
304
305
void AutoUpdaterWindow::queueGetLatestRelease()
306
{
307
#ifdef UPDATE_CHECKER_SUPPORTED
308
if (!ensureHttpReady())
309
{
310
emit updateCheckCompleted();
311
return;
312
}
313
314
std::string url = fmt::format(fmt::runtime(LATEST_RELEASE_URL), getCurrentUpdateTag());
315
m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterWindow::getLatestReleaseComplete, this,
316
std::placeholders::_1, std::placeholders::_2, std::placeholders::_4));
317
#endif
318
}
319
320
void AutoUpdaterWindow::getLatestTagComplete(s32 status_code, const Error& error, std::vector<u8> response,
321
bool display_errors)
322
{
323
#ifdef UPDATE_CHECKER_SUPPORTED
324
const std::string selected_tag(getCurrentUpdateTag());
325
const QString selected_tag_qstr = QString::fromStdString(selected_tag);
326
327
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
328
{
329
QJsonParseError parse_error;
330
const QJsonDocument doc = QJsonDocument::fromJson(
331
QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);
332
if (doc.isArray())
333
{
334
const QJsonArray doc_array(doc.array());
335
for (const QJsonValue& val : doc_array)
336
{
337
if (!val.isObject())
338
continue;
339
340
if (val["name"].toString() != selected_tag_qstr)
341
continue;
342
343
m_latest_sha = val["commit"].toObject()["sha"].toString();
344
if (m_latest_sha.isEmpty())
345
continue;
346
347
if (updateNeeded())
348
{
349
queueGetLatestRelease();
350
return;
351
}
352
else
353
{
354
if (display_errors)
355
{
356
QMessageBox::information(this, tr("Automatic Updater"),
357
tr("No updates are currently available. Please try again later."));
358
}
359
360
emit updateCheckCompleted();
361
return;
362
}
363
}
364
365
if (display_errors)
366
reportError(fmt::format("{} release not found in JSON", selected_tag));
367
}
368
else
369
{
370
if (display_errors)
371
reportError("JSON is not an array");
372
}
373
}
374
else
375
{
376
if (display_errors)
377
reportError(fmt::format("Failed to download latest tag info: {}", error.GetDescription()));
378
}
379
380
emit updateCheckCompleted();
381
#endif
382
}
383
384
void AutoUpdaterWindow::getLatestReleaseComplete(s32 status_code, const Error& error, std::vector<u8> response)
385
{
386
#ifdef UPDATE_CHECKER_SUPPORTED
387
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
388
{
389
QJsonParseError parse_error;
390
const QJsonDocument doc = QJsonDocument::fromJson(
391
QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);
392
if (doc.isObject())
393
{
394
const QJsonObject doc_object(doc.object());
395
396
#ifdef AUTO_UPDATER_SUPPORTED
397
// search for the correct file
398
const QJsonArray assets(doc_object["assets"].toArray());
399
const QString asset_filename(UPDATE_ASSET_FILENAME);
400
bool asset_found = false;
401
for (const QJsonValue& asset : assets)
402
{
403
const QJsonObject asset_obj(asset.toObject());
404
if (asset_obj["name"] == asset_filename)
405
{
406
m_download_url = asset_obj["browser_download_url"].toString();
407
if (!m_download_url.isEmpty())
408
m_download_size = asset_obj["size"].toInt();
409
asset_found = true;
410
break;
411
}
412
}
413
414
if (!asset_found)
415
{
416
reportError("Asset not found");
417
return;
418
}
419
#endif
420
421
const QString current_date = QtHost::FormatNumber(
422
Host::NumberFormatType::ShortDateTime,
423
static_cast<s64>(
424
QDateTime::fromString(QString::fromUtf8(g_scm_date_str), Qt::DateFormat::ISODate).toSecsSinceEpoch()));
425
const QString release_date = QtHost::FormatNumber(
426
Host::NumberFormatType::ShortDateTime,
427
static_cast<s64>(
428
QDateTime::fromString(doc_object["published_at"].toString(), Qt::DateFormat::ISODate).toSecsSinceEpoch()));
429
430
m_ui.currentVersion->setText(
431
tr("Current Version: %1 (%2)")
432
.arg(QtUtils::StringViewToQString(TinyString::from_format("{}/{}", g_scm_version_str, THIS_RELEASE_TAG)))
433
.arg(current_date));
434
m_ui.newVersion->setText(
435
tr("New Version: %1 (%2)").arg(QString::fromStdString(getCurrentUpdateTag())).arg(release_date));
436
m_ui.downloadSize->setText(
437
tr("Download Size: %1 MB").arg(static_cast<double>(m_download_size) / 1000000.0, 0, 'f', 2));
438
439
if (!canInstallUpdate())
440
{
441
// Just display the version and a download link.
442
m_ui.downloadAndInstall->setText(tr("Download..."));
443
}
444
445
m_ui.downloadAndInstall->setEnabled(true);
446
m_ui.updateNotes->setText(tr("Loading..."));
447
queueGetChanges();
448
QtUtils::ShowOrRaiseWindow(this);
449
return;
450
}
451
else
452
{
453
reportError("JSON is not an object");
454
}
455
}
456
else
457
{
458
reportError(fmt::format("Failed to download latest release info: {}", error.GetDescription()));
459
}
460
461
emit updateCheckCompleted();
462
#endif
463
}
464
465
void AutoUpdaterWindow::queueGetChanges()
466
{
467
#ifdef UPDATE_CHECKER_SUPPORTED
468
if (!ensureHttpReady())
469
return;
470
471
std::string url = fmt::format(fmt::runtime(CHANGES_URL), g_scm_hash_str, getCurrentUpdateTag());
472
m_http->CreateRequest(std::move(url), std::bind(&AutoUpdaterWindow::getChangesComplete, this, std::placeholders::_1,
473
std::placeholders::_2, std::placeholders::_4));
474
#endif
475
}
476
477
void AutoUpdaterWindow::getChangesComplete(s32 status_code, const Error& error, std::vector<u8> response)
478
{
479
#ifdef UPDATE_CHECKER_SUPPORTED
480
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
481
{
482
QJsonParseError parse_error;
483
const QJsonDocument doc = QJsonDocument::fromJson(
484
QByteArray(reinterpret_cast<const char*>(response.data()), response.size()), &parse_error);
485
if (doc.isObject())
486
{
487
const QJsonObject doc_object(doc.object());
488
489
QString changes_html = tr("<h2>Changes:</h2>");
490
changes_html += QStringLiteral("<ul>");
491
492
const QJsonArray commits(doc_object["commits"].toArray());
493
bool update_will_break_save_states = false;
494
bool update_increases_settings_version = false;
495
496
for (const QJsonValue& commit : commits)
497
{
498
const QJsonObject commit_obj(commit["commit"].toObject());
499
500
QString message = commit_obj["message"].toString();
501
QString author = commit_obj["author"].toObject()["name"].toString();
502
const int first_line_terminator = message.indexOf('\n');
503
if (first_line_terminator >= 0)
504
message.remove(first_line_terminator, message.size() - first_line_terminator);
505
if (!message.isEmpty())
506
{
507
changes_html +=
508
QStringLiteral("<li>%1 <i>(%2)</i></li>").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());
509
}
510
511
if (message.contains(QStringLiteral("[SAVEVERSION+]")))
512
update_will_break_save_states = true;
513
514
if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))
515
update_increases_settings_version = true;
516
}
517
518
changes_html += "</ul>";
519
520
if (update_will_break_save_states)
521
{
522
changes_html.prepend(tr("<h2>Save State Warning</h2><p>Installing this update will make your save states "
523
"<b>incompatible</b>. Please ensure you have saved your games to memory card "
524
"before installing this update or you will lose progress.</p>"));
525
}
526
527
if (update_increases_settings_version)
528
{
529
changes_html.prepend(
530
tr("<h2>Settings Warning</h2><p>Installing this update will reset your program configuration. Please note "
531
"that you will have to reconfigure your settings after this update.</p>"));
532
}
533
534
m_ui.updateNotes->setText(changes_html);
535
}
536
else
537
{
538
reportError("Change list JSON is not an object");
539
}
540
}
541
else
542
{
543
reportError(fmt::format("Failed to download change list: {}", error.GetDescription()));
544
}
545
#endif
546
}
547
548
void AutoUpdaterWindow::downloadUpdateClicked()
549
{
550
#ifdef UPDATE_CHECKER_SUPPORTED
551
if (!canInstallUpdate())
552
{
553
QtUtils::OpenURL(this, fmt::format(fmt::runtime(DOWNLOAD_PAGE_URL), getCurrentUpdateTag()));
554
return;
555
}
556
#endif
557
#ifdef AUTO_UPDATER_SUPPORTED
558
// Prevent multiple clicks of the button.
559
if (!m_ui.downloadAndInstall->isEnabled())
560
return;
561
m_ui.downloadAndInstall->setEnabled(false);
562
563
std::optional<bool> download_result;
564
QtModalProgressCallback progress(this);
565
progress.SetTitle(tr("Automatic Updater").toUtf8().constData());
566
progress.SetStatusText(tr("Downloading %1...").arg(m_latest_sha).toUtf8().constData());
567
progress.GetDialog().setWindowIcon(windowIcon());
568
progress.SetCancellable(true);
569
progress.MakeVisible();
570
571
m_http->CreateRequest(
572
m_download_url.toStdString(),
573
[this, &download_result](s32 status_code, const Error& error, const std::string&, std::vector<u8> response) {
574
if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)
575
return;
576
577
if (status_code != HTTPDownloader::HTTP_STATUS_OK)
578
{
579
reportError(fmt::format("Download failed: {}", error.GetDescription()));
580
download_result = false;
581
return;
582
}
583
584
if (response.empty())
585
{
586
reportError("Download failed: Update is empty");
587
download_result = false;
588
return;
589
}
590
591
download_result = processUpdate(response);
592
},
593
&progress);
594
595
// Since we're going to block, don't allow the timer to poll, otherwise the progress callback can cause the timer
596
// to run, and recursively poll again.
597
m_http_poll_timer->stop();
598
599
// Block until completion.
600
QtUtils::ProcessEventsWithSleep(
601
QEventLoop::AllEvents,
602
[this]() {
603
m_http->PollRequests();
604
return m_http->HasAnyRequests();
605
},
606
HTTP_POLL_INTERVAL);
607
608
if (download_result.value_or(false))
609
{
610
// updater started. since we're a modal on the main window, we have to queue this.
611
QMetaObject::invokeMethod(g_main_window, &MainWindow::requestExit, Qt::QueuedConnection, false);
612
close();
613
}
614
else
615
{
616
// update failed, re-enable download button
617
m_ui.downloadAndInstall->setEnabled(true);
618
}
619
#endif
620
}
621
622
bool AutoUpdaterWindow::updateNeeded() const
623
{
624
QString last_checked_sha = QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion"));
625
626
INFO_LOG("Current SHA: {}", g_scm_hash_str);
627
INFO_LOG("Latest SHA: {}", m_latest_sha.toUtf8().constData());
628
INFO_LOG("Last Checked SHA: {}", last_checked_sha.toUtf8().constData());
629
if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha)
630
{
631
INFO_LOG("No update needed.");
632
return false;
633
}
634
635
INFO_LOG("Update needed.");
636
return true;
637
}
638
639
void AutoUpdaterWindow::skipThisUpdateClicked()
640
{
641
Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData());
642
Host::CommitBaseSettingChanges();
643
close();
644
}
645
646
void AutoUpdaterWindow::remindMeLaterClicked()
647
{
648
close();
649
}
650
651
void AutoUpdaterWindow::closeEvent(QCloseEvent* event)
652
{
653
emit updateCheckCompleted();
654
QWidget::closeEvent(event);
655
}
656
657
#ifdef _WIN32
658
659
static constexpr char UPDATER_EXECUTABLE[] = "updater.exe";
660
static constexpr char UPDATER_ARCHIVE_NAME[] = "update.zip";
661
662
bool AutoUpdaterWindow::doesUpdaterNeedElevation(const std::string& application_dir) const
663
{
664
// Try to create a dummy text file in the updater directory. If it fails, we probably won't have write permission.
665
const std::string dummy_path = Path::Combine(application_dir, "update.txt");
666
auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");
667
if (!fp)
668
return true;
669
670
fp.reset();
671
FileSystem::DeleteFile(dummy_path.c_str());
672
return false;
673
}
674
675
bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)
676
{
677
const std::string& application_dir = EmuFolders::AppRoot;
678
const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);
679
const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
680
681
Error error;
682
if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFile(update_zip_path.c_str(), &error)))
683
{
684
reportError(fmt::format("Removing existing update zip failed:\n{}", error.GetDescription()));
685
return false;
686
}
687
688
if (!FileSystem::WriteAtomicRenamedFile(update_zip_path.c_str(), update_data, &error))
689
{
690
reportError(fmt::format("Writing update zip to '{}' failed:\n{}", update_zip_path, error.GetDescription()));
691
return false;
692
}
693
694
Error updater_extract_error;
695
if (!extractUpdater(update_zip_path.c_str(), updater_path.c_str(), &updater_extract_error))
696
{
697
reportError(fmt::format("Extracting updater failed: {}", updater_extract_error.GetDescription()));
698
return false;
699
}
700
701
return doUpdate(application_dir, update_zip_path, updater_path);
702
}
703
704
bool AutoUpdaterWindow::extractUpdater(const std::string& zip_path, const std::string& destination_path, Error* error)
705
{
706
unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.c_str());
707
if (!zf)
708
{
709
reportError("Failed to open update zip");
710
return false;
711
}
712
713
if (unzLocateFile(zf, UPDATER_EXECUTABLE, 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK)
714
{
715
Error::SetString(error, "Failed to locate updater.exe");
716
unzClose(zf);
717
return false;
718
}
719
720
auto fp = FileSystem::OpenManagedCFile(destination_path.c_str(), "wb", error);
721
if (!fp)
722
{
723
Error::SetString(error, "Failed to open updater.exe for writing");
724
unzClose(zf);
725
return false;
726
}
727
728
static constexpr size_t CHUNK_SIZE = 4096;
729
char chunk[CHUNK_SIZE];
730
for (;;)
731
{
732
int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE);
733
if (size < 0)
734
{
735
Error::SetString(error, "Failed to decompress updater exe");
736
unzClose(zf);
737
fp.reset();
738
FileSystem::DeleteFile(destination_path.c_str());
739
return false;
740
}
741
else if (size == 0)
742
{
743
break;
744
}
745
746
if (std::fwrite(chunk, size, 1, fp.get()) != 1)
747
{
748
Error::SetErrno(error, "Failed to write updater exe: fwrite() failed: ", errno);
749
unzClose(zf);
750
fp.reset();
751
FileSystem::DeleteFile(destination_path.c_str());
752
return false;
753
}
754
}
755
756
unzClose(zf);
757
return true;
758
}
759
760
bool AutoUpdaterWindow::doUpdate(const std::string& application_dir, const std::string& zip_path,
761
const std::string& updater_path)
762
{
763
const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();
764
if (program_path.empty())
765
{
766
reportError("Failed to get current application path");
767
return false;
768
}
769
770
const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);
771
const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);
772
const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format(
773
"{} \"{}\" \"{}\" \"{}\"", QCoreApplication::applicationPid(), application_dir, zip_path, program_path));
774
775
const bool needs_elevation = doesUpdaterNeedElevation(application_dir);
776
777
SHELLEXECUTEINFOW sei = {};
778
sei.cbSize = sizeof(sei);
779
sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation
780
sei.lpFile = wupdater_path.c_str();
781
sei.lpParameters = arguments.c_str();
782
sei.lpDirectory = wapplication_dir.c_str();
783
sei.nShow = SW_SHOWNORMAL;
784
if (!ShellExecuteExW(&sei))
785
{
786
reportError(fmt::format("Failed to start {}: {}", needs_elevation ? "elevated updater" : "updater",
787
Error::CreateWin32(GetLastError()).GetDescription()));
788
return false;
789
}
790
791
return true;
792
}
793
794
void AutoUpdaterWindow::cleanupAfterUpdate()
795
{
796
// If we weren't portable, then updater executable gets left in the application directory.
797
if (EmuFolders::AppRoot == EmuFolders::DataRoot)
798
return;
799
800
const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
801
if (!FileSystem::FileExists(updater_path.c_str()))
802
return;
803
804
Error error;
805
if (!FileSystem::DeleteFile(updater_path.c_str(), &error))
806
{
807
QMessageBox::critical(
808
nullptr, tr("Updater Error"),
809
tr("Failed to remove updater exe after update:\n%1").arg(QString::fromStdString(error.GetDescription())));
810
return;
811
}
812
}
813
814
#elif defined(__APPLE__)
815
816
bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)
817
{
818
std::optional<std::string> bundle_path = CocoaTools::GetNonTranslocatedBundlePath();
819
if (!bundle_path.has_value())
820
{
821
reportError("Couldn't obtain non-translocated bundle path.");
822
return false;
823
}
824
825
QFileInfo info(QString::fromStdString(bundle_path.value()));
826
if (!info.isBundle())
827
{
828
reportError(fmt::format("Application {} isn't a bundle.", bundle_path.value()));
829
return false;
830
}
831
if (info.suffix() != QStringLiteral("app"))
832
{
833
reportError(
834
fmt::format("Unexpected application suffix {} on {}.", info.suffix().toStdString(), bundle_path.value()));
835
return false;
836
}
837
838
// Use the updater from this version to unpack the new version.
839
const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app");
840
if (!FileSystem::DirectoryExists(updater_app.c_str()))
841
{
842
reportError(fmt::format("Failed to find updater at {}.", updater_app));
843
return false;
844
}
845
846
// We use the user data directory to temporarily store the update zip.
847
const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip");
848
const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING");
849
Error error;
850
if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str(), &error))
851
{
852
reportError(fmt::format("Failed to remove old update zip:\n{}", error.GetDescription()));
853
return false;
854
}
855
856
// Save update.
857
if (!FileSystem::WriteAtomicRenamedFile(zip_path.c_str(), update_data, &error))
858
{
859
reportError(fmt::format("Writing update zip to '{}' failed:\n{}", zip_path, error.GetDescription()));
860
return false;
861
}
862
863
INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",
864
updater_app, zip_path, staging_directory, bundle_path.value());
865
866
const std::string_view args[] = {
867
zip_path,
868
staging_directory,
869
bundle_path.value(),
870
};
871
872
// Kick off updater!
873
CocoaTools::DelayedLaunch(updater_app, args);
874
return true;
875
}
876
877
void AutoUpdaterWindow::cleanupAfterUpdate()
878
{
879
}
880
881
#elif defined(__linux__)
882
883
bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)
884
{
885
const char* appimage_path = std::getenv("APPIMAGE");
886
if (!appimage_path || !FileSystem::FileExists(appimage_path))
887
{
888
reportError("Missing APPIMAGE.");
889
return false;
890
}
891
892
if (!FileSystem::FileExists(appimage_path))
893
{
894
reportError(fmt::format("Current AppImage does not exist: {}", appimage_path));
895
return false;
896
}
897
898
const std::string new_appimage_path = fmt::format("{}.new", appimage_path);
899
const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);
900
INFO_LOG("APPIMAGE = {}", appimage_path);
901
INFO_LOG("Backup AppImage path = {}", backup_appimage_path);
902
INFO_LOG("New AppImage path = {}", new_appimage_path);
903
904
// Remove old "new" appimage and existing backup appimage.
905
Error error;
906
if (FileSystem::FileExists(new_appimage_path.c_str()) && !FileSystem::DeleteFile(new_appimage_path.c_str(), &error))
907
{
908
reportError(
909
fmt::format("Failed to remove old destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));
910
return false;
911
}
912
if (FileSystem::FileExists(backup_appimage_path.c_str()) &&
913
!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))
914
{
915
reportError(
916
fmt::format("Failed to remove old backup AppImage: {}:\n{}", backup_appimage_path, error.GetDescription()));
917
return false;
918
}
919
920
// Write "new" appimage.
921
{
922
// We want to copy the permissions from the old appimage to the new one.
923
static constexpr int permission_mask = S_IRWXU | S_IRWXG | S_IRWXO;
924
struct stat old_stat;
925
if (!FileSystem::StatFile(appimage_path, &old_stat, &error))
926
{
927
reportError(fmt::format("Failed to get old AppImage {} permissions:\n{}", appimage_path, error.GetDescription()));
928
return false;
929
}
930
931
// We do this as a manual write here, rather than using WriteAtomicUpdatedFile(), because we want to write the file
932
// and set the permissions as one atomic operation.
933
FileSystem::ManagedCFilePtr fp = FileSystem::OpenManagedCFile(new_appimage_path.c_str(), "wb", &error);
934
bool success = static_cast<bool>(fp);
935
if (fp)
936
{
937
if (std::fwrite(update_data.data(), update_data.size(), 1, fp.get()) == 1 && std::fflush(fp.get()) == 0)
938
{
939
const int fd = fileno(fp.get());
940
if (fd >= 0)
941
{
942
if (fchmod(fd, old_stat.st_mode & permission_mask) != 0)
943
{
944
error.SetErrno("fchmod() failed: ", errno);
945
success = false;
946
}
947
}
948
else
949
{
950
error.SetErrno("fileno() failed: ", errno);
951
success = false;
952
}
953
}
954
else
955
{
956
error.SetErrno("fwrite() failed: ", errno);
957
success = false;
958
}
959
960
fp.reset();
961
if (!success)
962
FileSystem::DeleteFile(new_appimage_path.c_str());
963
}
964
965
if (!success)
966
{
967
reportError(
968
fmt::format("Failed to write new destination AppImage: {}:\n{}", new_appimage_path, error.GetDescription()));
969
return false;
970
}
971
}
972
973
// Rename "old" appimage.
974
if (!FileSystem::RenamePath(appimage_path, backup_appimage_path.c_str(), &error))
975
{
976
reportError(fmt::format("Failed to rename old AppImage to {}:\n{}", backup_appimage_path, error.GetDescription()));
977
FileSystem::DeleteFile(new_appimage_path.c_str());
978
return false;
979
}
980
981
// Rename "new" appimage.
982
if (!FileSystem::RenamePath(new_appimage_path.c_str(), appimage_path, &error))
983
{
984
reportError(fmt::format("Failed to rename new AppImage to {}:\n{}", appimage_path, error.GetDescription()));
985
return false;
986
}
987
988
// Execute new appimage.
989
QProcess* new_process = new QProcess();
990
new_process->setProgram(QString::fromUtf8(appimage_path));
991
new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});
992
if (!new_process->startDetached())
993
{
994
reportError("Failed to execute new AppImage.");
995
return false;
996
}
997
998
// We exit once we return.
999
return true;
1000
}
1001
1002
void AutoUpdaterWindow::cleanupAfterUpdate()
1003
{
1004
// Remove old/backup AppImage.
1005
const char* appimage_path = std::getenv("APPIMAGE");
1006
if (!appimage_path)
1007
return;
1008
1009
const std::string backup_appimage_path = fmt::format("{}.backup", appimage_path);
1010
if (!FileSystem::FileExists(backup_appimage_path.c_str()))
1011
return;
1012
1013
Error error;
1014
INFO_LOG("Removing backup AppImage: {}", backup_appimage_path);
1015
if (!FileSystem::DeleteFile(backup_appimage_path.c_str(), &error))
1016
ERROR_LOG("Failed to remove backup AppImage {}: {}", backup_appimage_path, error.GetDescription());
1017
}
1018
1019
#else
1020
1021
bool AutoUpdaterWindow::processUpdate(const std::vector<u8>& update_data)
1022
{
1023
return false;
1024
}
1025
1026
void AutoUpdaterWindow::cleanupAfterUpdate()
1027
{
1028
}
1029
1030
#endif
1031
1032