diff --git a/CMakeLists.txt b/CMakeLists.txt index 52fe8387b..5f85583a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/include/mgba-util/configuration.h b/include/mgba-util/configuration.h index 3409db0eb..e19ad13ef 100644 --- a/include/mgba-util/configuration.h +++ b/include/mgba-util/configuration.h @@ -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); diff --git a/include/mgba/feature/updater.h b/include/mgba/feature/updater.h index ad14b9661..323ff749c 100644 --- a/include/mgba/feature/updater.h +++ b/include/mgba/feature/updater.h @@ -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 diff --git a/src/feature/updater-main.c b/src/feature/updater-main.c new file mode 100644 index 000000000..6273ec192 --- /dev/null +++ b/src/feature/updater-main.c @@ -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 +#include +#include + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include + +#define mkdir(X, Y) _mkdir(X) +#elif defined(_POSIX_C_SOURCE) +#include +#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 +#include + +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 diff --git a/src/feature/updater.c b/src/feature/updater.c index 771ec4448..e67777473 100644 --- a/src/feature/updater.c +++ b/src/feature/updater.c @@ -5,10 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +#include #include #include #include +#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; +} diff --git a/src/platform/qt/ApplicationUpdatePrompt.cpp b/src/platform/qt/ApplicationUpdatePrompt.cpp new file mode 100644 index 000000000..36e9e88d4 --- /dev/null +++ b/src/platform/qt/ApplicationUpdatePrompt.cpp @@ -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 +#include + +#include "ApplicationUpdater.h" +#include "GBAApp.h" +#include "utils.h" + +#include + +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); +} diff --git a/src/platform/qt/ApplicationUpdatePrompt.h b/src/platform/qt/ApplicationUpdatePrompt.h new file mode 100644 index 000000000..2318e7985 --- /dev/null +++ b/src/platform/qt/ApplicationUpdatePrompt.h @@ -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 + +#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; +}; + +} diff --git a/src/platform/qt/ApplicationUpdatePrompt.ui b/src/platform/qt/ApplicationUpdatePrompt.ui new file mode 100644 index 000000000..31b3e6135 --- /dev/null +++ b/src/platform/qt/ApplicationUpdatePrompt.ui @@ -0,0 +1,75 @@ + + + ApplicationUpdatePrompt + + + + 0 + 0 + 186 + 127 + + + + An update is available + + + + QLayout::SetFixedSize + + + + + {text} + + + true + + + + + + + {details} + + + + + + + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + rejected() + ApplicationUpdatePrompt + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/platform/qt/ApplicationUpdater.cpp b/src/platform/qt/ApplicationUpdater.cpp index 54516540c..ad1003660 100644 --- a/src/platform/qt/ApplicationUpdater.cpp +++ b/src/platform/qt/ApplicationUpdater.cpp @@ -5,8 +5,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ApplicationUpdater.h" +#include #include +#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(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) { diff --git a/src/platform/qt/ApplicationUpdater.h b/src/platform/qt/ApplicationUpdater.h index 71f276255..54fbad495 100644 --- a/src/platform/qt/ApplicationUpdater.h +++ b/src/platform/qt/ApplicationUpdater.h @@ -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(); diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt index 61c440dae..032d01d07 100644 --- a/src/platform/qt/CMakeLists.txt +++ b/src/platform/qt/CMakeLists.txt @@ -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}) diff --git a/src/platform/qt/GBAApp.cpp b/src/platform/qt/GBAApp.cpp index 77ce546dc..7d4252841 100644 --- a/src/platform/qt/GBAApp.cpp +++ b/src/platform/qt/GBAApp.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -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 callback) { if (!m_workerJobs.contains(jobId)) { return false; @@ -286,6 +288,52 @@ bool GBAApp::waitOnJob(qint64 jobId, QObject* context, std::function 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); diff --git a/src/platform/qt/GBAApp.h b/src/platform/qt/GBAApp.h index f84208a40..a563a4c42 100644 --- a/src/platform/qt/GBAApp.h +++ b/src/platform/qt/GBAApp.h @@ -77,6 +77,10 @@ public: bool waitOnJob(qint64 jobId, QObject* context, std::function 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* paused); void continueAll(const QList& paused); @@ -112,6 +118,7 @@ private: MultiplayerController m_multiplayer; CoreManager m_manager; ApplicationUpdater m_updater; + QString m_invokeOnExit; QMap m_workerJobs; QMultiMap m_workerJobCallbacks; diff --git a/src/platform/qt/main.cpp b/src/platform/qt/main.cpp index 5dc302b1f..21570c0b8 100644 --- a/src/platform/qt/main.cpp +++ b/src/platform/qt/main.cpp @@ -39,6 +39,12 @@ Q_IMPORT_PLUGIN(AVFServicePlugin); #endif #endif +#ifdef Q_OS_WIN +#include +#else +#include +#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 diff --git a/src/util/configuration.c b/src/util/configuration.c index e0dada00c..4c7805580 100644 --- a/src/util/configuration.c +++ b/src/util/configuration.c @@ -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) {