All: Add updater stub and hook it into the Qt frontend

This commit is contained in:
Vicki Pfau 2021-08-05 02:24:13 -07:00
parent 4a7feb66f9
commit 2d5f6dd675
15 changed files with 520 additions and 9 deletions

View File

@ -908,6 +908,19 @@ if(BUILD_OPENEMU)
install(TARGETS ${BINARY_NAME}-openemu LIBRARY DESTINATION ${OE_LIBDIR} COMPONENT ${BINARY_NAME}.oecoreplugin NAMELINK_SKIP)
endif()
if(BUILD_QT AND WIN32)
set(BUILD_UPDATER ON)
endif()
if(BUILD_UPDATER)
add_executable(updater-stub WIN32 ${CMAKE_CURRENT_SOURCE_DIR}/src/feature/updater-main.c)
target_link_libraries(updater-stub ${OS_LIB} ${PLATFORM_LIBRARY} ${BINARY_NAME})
if(NOT MSVC)
set_target_properties(updater-stub PROPERTIES LINK_FLAGS_RELEASE -s)
set_target_properties(updater-stub PROPERTIES LINK_FLAGS_RELWITHDEBINFO -s)
endif()
endif()
if(BUILD_SDL)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/platform/sdl ${CMAKE_CURRENT_BINARY_DIR}/sdl)
endif()

View File

@ -28,6 +28,8 @@ void ConfigurationSetUIntValue(struct Configuration*, const char* section, const
void ConfigurationSetFloatValue(struct Configuration*, const char* section, const char* key, float value);
bool ConfigurationHasSection(const struct Configuration*, const char* section);
void ConfigurationDeleteSection(struct Configuration*, const char* section);
const char* ConfigurationGetValue(const struct Configuration*, const char* section, const char* key);
void ConfigurationClearValue(struct Configuration*, const char* section, const char* key);

View File

@ -38,6 +38,14 @@ const char* mUpdaterGetBucket(const struct mUpdaterContext*);
void mUpdateRecord(struct mCoreConfig*, const char* prefix, const struct mUpdate*);
bool mUpdateLoad(const struct mCoreConfig*, const char* prefix, struct mUpdate*);
void mUpdateRegister(struct mCoreConfig*, const char* arg0, const char* updatePath);
void mUpdateDeregister(struct mCoreConfig*);
const char* mUpdateGetRoot(const struct mCoreConfig*);
const char* mUpdateGetCommand(const struct mCoreConfig*);
const char* mUpdateGetArchiveExtension(const struct mCoreConfig*);
bool mUpdateGetArchivePath(const struct mCoreConfig*, char* out, size_t outLength);
CXX_GUARD_END
#endif

143
src/feature/updater-main.c Normal file
View File

@ -0,0 +1,143 @@
/* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/core/config.h>
#include <mgba/feature/updater.h>
#include <mgba-util/vfs.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#include <io.h>
#include <process.h>
#define mkdir(X, Y) _mkdir(X)
#elif defined(_POSIX_C_SOURCE)
#include <unistd.h>
#endif
bool extractArchive(struct VDir* archive, const char* root) {
char path[PATH_MAX] = {};
struct VDirEntry* vde;
uint8_t block[8192];
ssize_t size;
while ((vde = archive->listNext(archive))) {
struct VFile* vfIn;
struct VFile* vfOut;
const char* fname = strchr(vde->name(vde), '/');
if (!fname) {
continue;
}
snprintf(path, sizeof(path), "%s/%s", root, &fname[1]);
switch (vde->type(vde)) {
case VFS_DIRECTORY:
printf("mkdir %s\n", fname);
if (mkdir(path, 0755) < 0 && errno != EEXIST) {
return false;
}
break;
case VFS_FILE:
printf("extract %s\n", fname);
vfIn = archive->openFile(archive, vde->name(vde), O_RDONLY);
errno = 0;
vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
if (!vfOut && errno == EACCES) {
sleep(1);
vfOut = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
}
if (!vfOut) {
vfIn->close(vfIn);
return false;
}
while ((size = vfIn->read(vfIn, block, sizeof(block))) > 0) {
vfOut->write(vfOut, block, size);
}
vfOut->close(vfOut);
vfIn->close(vfIn);
if (size < 0) {
return false;
}
break;
case VFS_UNKNOWN:
return false;
}
}
return true;
}
int main(int argc, char* argv[]) {
UNUSED(argc);
UNUSED(argv);
struct mCoreConfig config;
char updateArchive[PATH_MAX] = {};
const char* root;
int ok = 1;
mCoreConfigInit(&config, "updater");
if (!mCoreConfigLoad(&config)) {
puts("Failed to load config");
} else if (!mUpdateGetArchivePath(&config, updateArchive, sizeof(updateArchive)) || !(root = mUpdateGetRoot(&config))) {
puts("No pending update found");
} else if (access(root, W_OK)) {
puts("Cannot write to update path");
} else {
bool isPortable = mCoreConfigIsPortable();
struct VDir* archive = VDirOpenArchive(updateArchive);
if (!archive) {
puts("Cannot open update archive");
} else {
puts("Extracting update");
if (extractArchive(archive, root)) {
puts("Complete");
ok = 0;
mUpdateDeregister(&config);
} else {
puts("An error occurred");
}
archive->close(archive);
unlink(updateArchive);
}
if (!isPortable) {
char portableIni[PATH_MAX] = {};
snprintf(portableIni, sizeof(portableIni), "%s/portable.ini", root);
unlink(portableIni);
}
}
const char* bin = mUpdateGetCommand(&config);
mCoreConfigDeinit(&config);
if (ok == 0) {
const char* argv[] = { bin, NULL };
#ifdef _WIN32
_execv(bin, argv);
#elif defined(_POSIX_C_SOURCE)
execv(bin, argv);
#endif
}
return 1;
}
#ifdef _WIN32
#include <mgba-util/string.h>
#include <mgba-util/vector.h>
int wmain(int argc, wchar_t* argv[]) {
struct StringList argv8;
StringListInit(&argv8, argc);
for (int i = 0; i < argc; ++i) {
*StringListAppend(&argv8) = utf16to8((uint16_t*) argv[i], wcslen(argv[i]) * 2);
}
int ret = main(argc, StringListGetPointer(&argv8, 0));
size_t i;
for (i = 0; i < StringListSize(&argv8); ++i) {
free(*StringListGetPointer(&argv8, i));
}
return ret;
}
#endif

View File

@ -5,10 +5,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/feature/updater.h>
#include <mgba-util/string.h>
#include <mgba-util/table.h>
#include <mgba-util/vector.h>
#include <mgba-util/vfs.h>
#define UPDATE_SECTION "update"
struct mUpdateMatch {
const char* channel;
struct mUpdate* out;
@ -168,3 +171,54 @@ bool mUpdateLoad(const struct mCoreConfig* config, const char* prefix, struct mU
update->sha256 = mCoreConfigGetValue(config, key);
return true;
}
void mUpdateRegister(struct mCoreConfig* config, const char* arg0, const char* updatePath) {
struct Configuration* cfg = &config->configTable;
char filename[PATH_MAX];
strlcpy(filename, arg0, sizeof(filename));
char* last;
#ifdef _WIN32
last = strrchr(filename, '\\');
#else
last = strrchr(filename, '/');
#endif
if (last) {
last[0] = '\0';
}
ConfigurationSetValue(cfg, UPDATE_SECTION, "bin", arg0);
ConfigurationSetValue(cfg, UPDATE_SECTION, "root", filename);
separatePath(updatePath, NULL, NULL, filename);
ConfigurationSetValue(cfg, UPDATE_SECTION, "extension", filename);
mCoreConfigSave(config);
}
void mUpdateDeregister(struct mCoreConfig* config) {
ConfigurationDeleteSection(&config->configTable, UPDATE_SECTION);
mCoreConfigSave(config);
}
const char* mUpdateGetRoot(const struct mCoreConfig* config) {
return ConfigurationGetValue(&config->configTable, UPDATE_SECTION, "root");
}
const char* mUpdateGetCommand(const struct mCoreConfig* config) {
return ConfigurationGetValue(&config->configTable, UPDATE_SECTION, "bin");
}
const char* mUpdateGetArchiveExtension(const struct mCoreConfig* config) {
return ConfigurationGetValue(&config->configTable, UPDATE_SECTION, "extension");
}
bool mUpdateGetArchivePath(const struct mCoreConfig* config, char* out, size_t outLength) {
const char* extension = ConfigurationGetValue(&config->configTable, UPDATE_SECTION, "extension");
if (!extension) {
return false;
}
mCoreConfigDirectory(out, outLength);
size_t start = strlen(out);
outLength -= start;
snprintf(&out[start], outLength, PATH_SEP "update.%s", extension);
return true;
}

View File

@ -0,0 +1,71 @@
/* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ApplicationUpdatePrompt.h"
#include <QCryptographicHash>
#include <QPushButton>
#include "ApplicationUpdater.h"
#include "GBAApp.h"
#include "utils.h"
#include <mgba/core/version.h>
using namespace QGBA;
ApplicationUpdatePrompt::ApplicationUpdatePrompt(QWidget* parent)
: QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint)
{
m_ui.setupUi(this);
ApplicationUpdater* updater = GBAApp::app()->updater();
ApplicationUpdater::UpdateInfo info = updater->updateInfo();
m_ui.text->setText(tr("An update to %1 is available.\nDo you want to download and install it now? You will need to restart the emulator when the download is complete.")
.arg(QLatin1String(projectName)));
m_ui.details->setText(tr("Current version: %1\nNew version: %2\nDownload size: %3")
.arg(QLatin1String(projectVersion))
.arg(info)
.arg(niceSizeFormat(info.size)));
m_ui.progressBar->setVisible(false);
connect(updater, &AbstractUpdater::updateProgress, this, [this](float progress) {
m_ui.progressBar->setValue(progress * 100);
});
m_okDownload = connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &ApplicationUpdatePrompt::startUpdate);
connect(updater, &AbstractUpdater::updateDone, this, &ApplicationUpdatePrompt::promptRestart);
}
void ApplicationUpdatePrompt::startUpdate() {
ApplicationUpdater* updater = GBAApp::app()->updater();
updater->downloadUpdate();
m_ui.buttonBox->disconnect(m_okDownload);
m_ui.progressBar->show();
m_ui.text->setText(tr("Downloading update..."));
m_ui.details->hide();
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
}
void ApplicationUpdatePrompt::promptRestart() {
ApplicationUpdater* updater = GBAApp::app()->updater();
QString filename = updater->destination();
QByteArray expectedHash = updater->updateInfo().sha256;
QCryptographicHash sha256(QCryptographicHash::Sha256);
QFile update(filename);
update.open(QIODevice::ReadOnly);
if (!sha256.addData(&update) || sha256.result() != expectedHash) {
update.close();
update.remove();
m_ui.text->setText(tr("Downloading failed. Please update manually.")
.arg(QLatin1String(projectName)));
} else {
m_ui.text->setText(tr("Downloading done. Press OK to restart %1 and install the update.")
.arg(QLatin1String(projectName)));
}
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
}

View File

@ -0,0 +1,29 @@
/* Copyright (c) 2013-2021 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#pragma once
#include <QDialog>
#include "ui_ApplicationUpdatePrompt.h"
namespace QGBA {
class ApplicationUpdatePrompt : public QDialog {
Q_OBJECT
public:
ApplicationUpdatePrompt(QWidget* parent = nullptr);
private slots:
void startUpdate();
void promptRestart();
private:
Ui::ApplicationUpdatePrompt m_ui;
QMetaObject::Connection m_okDownload;
};
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ApplicationUpdatePrompt</class>
<widget class="QDialog" name="ApplicationUpdatePrompt">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>186</width>
<height>127</height>
</rect>
</property>
<property name="windowTitle">
<string>An update is available</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<widget class="QLabel" name="text">
<property name="text">
<string>{text}</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="details">
<property name="text">
<string>{details}</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ApplicationUpdatePrompt</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -5,8 +5,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ApplicationUpdater.h"
#include <QDir>
#include <QFileInfo>
#include "ApplicationUpdatePrompt.h"
#include "ConfigController.h"
#include "GBAApp.h"
@ -37,9 +39,26 @@ ApplicationUpdater::ApplicationUpdater(ConfigController* config, QObject* parent
}
}
connect(this, &AbstractUpdater::updateAvailable, this, [this, config](bool) {
connect(this, &AbstractUpdater::updateAvailable, this, [this, config](bool available) {
m_lastCheck = QDateTime::currentDateTimeUtc();
config->setQtOption("lastUpdateCheck", m_lastCheck);
if (available && currentVersion() < updateInfo()) {
#ifdef Q_OS_WIN
// Only works on Windows at the moment
ApplicationUpdatePrompt* prompt = new ApplicationUpdatePrompt;
connect(prompt, &QDialog::accepted, GBAApp::app(), &GBAApp::restartForUpdate);
prompt->setAttribute(Qt::WA_DeleteOnClose);
prompt->show();
#endif
}
});
connect(this, &AbstractUpdater::updateDone, this, [this, config]() {
QByteArray arg0 = GBAApp::app()->arguments().at(0).toUtf8();
QByteArray path = updateInfo().url.path().toUtf8();
mUpdateRegister(config->config(), arg0.constData(), path.constData());
config->write();
});
}
@ -79,6 +98,7 @@ QString ApplicationUpdater::readableChannel(const QString& channel) {
ApplicationUpdater::UpdateInfo ApplicationUpdater::currentVersion() {
UpdateInfo info;
info.version = QLatin1String(projectVersion);
info.rev = gitRevision;
info.commit = QLatin1String(gitCommit);
return info;
@ -118,14 +138,20 @@ QUrl ApplicationUpdater::parseManifest(const QByteArray& manifest) {
}
QString ApplicationUpdater::destination() const {
return {};
QFileInfo path(updateInfo().url.path());
QDir dir(ConfigController::configDir());
return dir.filePath(QLatin1String("update.") + path.completeSuffix());
}
const char* ApplicationUpdater::platform() {
#ifdef Q_OS_WIN
QFileInfo exe(GBAApp::app()->arguments().at(0));
QFileInfo uninstallInfo(exe.dir().filePath("unins000.dat"));
#ifdef Q_OS_WIN64
return "win64";
return uninstallInfo.exists() ? "win64-installer" : "win64";
#elif defined(Q_OS_WIN32)
return "win32";
return uninstallInfo.exists() ? "win32-installer" : "win32";
#endif
#elif defined(Q_OS_MACOS)
return "osx";
#else
@ -160,8 +186,8 @@ bool ApplicationUpdater::UpdateInfo::operator<(const ApplicationUpdater::UpdateI
QStringList components = version.split(QChar('.'));
QStringList otherComponents = other.version.split(QChar('.'));
for (int i = 0; i < std::max<int>(components.count(), otherComponents.count()); ++i) {
int component = 0;
int otherComponent = 0;
int component = -1;
int otherComponent = -1;
if (i < components.count()) {
bool ok = true;
component = components[i].toInt(&ok);
@ -189,6 +215,9 @@ ApplicationUpdater::UpdateInfo::operator QString() const {
if (!version.isNull()) {
return version;
}
if (rev <= 0) {
return tr("(None)");
}
int len = strlen(gitCommitShort);
const char* pos = strchr(gitCommitShort, '-');
if (pos) {

View File

@ -49,10 +49,11 @@ public:
QDateTime lastCheck() const { return m_lastCheck; }
virtual QString destination() const override;
protected:
virtual QUrl manifestLocation() const override;
virtual QUrl parseManifest(const QByteArray&) override;
virtual QString destination() const override;
private:
static const char* platform();

View File

@ -61,6 +61,7 @@ set(SOURCE_FILES
Action.cpp
ActionMapper.cpp
ApplicationUpdater.cpp
ApplicationUpdatePrompt.cpp
AssetInfo.cpp
AssetTile.cpp
AssetView.cpp
@ -123,6 +124,7 @@ set(SOURCE_FILES
set(UI_FILES
AboutScreen.ui
ApplicationUpdatePrompt.ui
ArchiveInspector.ui
AssetTile.ui
BattleChipView.ui
@ -228,6 +230,11 @@ if(USE_DISCORD_RPC)
endif()
qt5_add_resources(RESOURCES resources.qrc)
if(BUILD_UPDATER)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/updater.qrc.in ${CMAKE_CURRENT_BINARY_DIR}/updater.qrc)
qt5_add_resources(UPDATER_RESOURCES ${CMAKE_CURRENT_BINARY_DIR}/updater.qrc)
list(APPEND RESOURCES ${UPDATER_RESOURCES})
endif()
if(APPLE)
set(MACOSX_BUNDLE_ICON_FILE mgba.icns)
set(MACOSX_BUNDLE_BUNDLE_VERSION ${LIB_VERSION_STRING})

View File

@ -19,6 +19,7 @@
#include <QFontDatabase>
#include <QIcon>
#include <mgba/feature/updater.h>
#include <mgba-util/socket.h>
#include <mgba-util/vfs.h>
@ -80,6 +81,8 @@ GBAApp::GBAApp(int& argc, char* argv[], ConfigController* config)
m_configController->updateOption("useDiscordPresence");
#endif
cleanupAfterUpdate();
connect(this, &GBAApp::aboutToQuit, this, &GBAApp::cleanup);
if (m_configController->getOption("updateAutoCheck", 0).toInt()) {
QMetaObject::invokeMethod(&m_updater, "checkUpdate", Qt::QueuedConnection);
@ -268,7 +271,6 @@ bool GBAApp::removeWorkerJob(qint64 jobId) {
return success;
}
bool GBAApp::waitOnJob(qint64 jobId, QObject* context, std::function<void ()> callback) {
if (!m_workerJobs.contains(jobId)) {
return false;
@ -286,6 +288,52 @@ bool GBAApp::waitOnJob(qint64 jobId, QObject* context, std::function<void ()> ca
return true;
}
void GBAApp::cleanupAfterUpdate() {
// Remove leftover updater if there's one present
QDir configDir(ConfigController::configDir());
QString extractedPath = configDir.filePath(QLatin1String("updater"));
#ifdef Q_OS_WIN
extractedPath += ".exe";
#endif
QFile updater(extractedPath);
if (updater.exists()) {
updater.remove();
}
#ifdef Q_OS_WIN
// Remove the installer exe if we downloaded that too
extractedPath = configDir.filePath(QLatin1String("update.exe"));
QFile update(extractedPath);
if (update.exists()) {
update.remove();
}
#endif
}
void GBAApp::restartForUpdate() {
QFileInfo updaterPath(m_updater.updateInfo().url.path());
QDir configDir(ConfigController::configDir());
if (updaterPath.completeSuffix() == "exe") {
m_invokeOnExit = configDir.filePath(QLatin1String("update.exe"));
} else {
QFile updater(":/updater");
QString extractedPath = configDir.filePath(QLatin1String("updater"));
#ifdef Q_OS_WIN
extractedPath += ".exe";
#endif
updater.copy(extractedPath);
#ifndef Q_OS_WIN
QFile(extractedPath).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner);
#endif
m_invokeOnExit = extractedPath;
}
for (auto& window : m_windows) {
window->deleteLater();
}
QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection);
}
void GBAApp::finishJob(qint64 jobId) {
m_workerJobs.remove(jobId);
emit jobFinished(jobId);

View File

@ -77,6 +77,10 @@ public:
bool waitOnJob(qint64 jobId, QObject* context, std::function<void ()> callback);
ApplicationUpdater* updater() { return &m_updater; }
QString invokeOnExit() { return m_invokeOnExit; }
public slots:
void restartForUpdate();
signals:
void jobFinished(qint64 jobId);
@ -104,6 +108,8 @@ private:
Window* newWindowInternal();
void cleanupAfterUpdate();
void pauseAll(QList<Window*>* paused);
void continueAll(const QList<Window*>& paused);
@ -112,6 +118,7 @@ private:
MultiplayerController m_multiplayer;
CoreManager m_manager;
ApplicationUpdater m_updater;
QString m_invokeOnExit;
QMap<qint64, WorkerJob*> m_workerJobs;
QMultiMap<qint64, QMetaObject::Connection> m_workerJobCallbacks;

View File

@ -39,6 +39,12 @@ Q_IMPORT_PLUGIN(AVFServicePlugin);
#endif
#endif
#ifdef Q_OS_WIN
#include <process.h>
#else
#include <unistd.h>
#endif
using namespace QGBA;
int main(int argc, char* argv[]) {
@ -121,7 +127,21 @@ int main(int argc, char* argv[]) {
w->show();
return application.exec();
int ret = application.exec();
if (ret != 0) {
return ret;
}
QString invoke = application.invokeOnExit();
if (!invoke.isNull()) {
QByteArray proc = invoke.toUtf8();
#ifdef Q_OS_WIN
_execl(proc.constData(), proc.constData(), NULL);
#else
execl(proc.constData(), proc.constData(), NULL);
#endif
}
return ret;
}
#ifdef _WIN32

View File

@ -137,6 +137,10 @@ bool ConfigurationHasSection(const struct Configuration* configuration, const ch
return HashTableLookup(&configuration->sections, section);
}
void ConfigurationDeleteSection(struct Configuration* configuration, const char* section) {
HashTableRemove(&configuration->sections, section);
}
const char* ConfigurationGetValue(const struct Configuration* configuration, const char* section, const char* key) {
const struct Table* currentSection = &configuration->root;
if (section) {