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