From 2e6c80d1aafe26c7299cb0bccf3c7f265d845552 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 18 Aug 2017 23:05:49 -0600 Subject: [PATCH] Qt updater integration, based on QtAutoUpdater --- src/citra_qt/CMakeLists.txt | 5 +- src/citra_qt/configuration/config.cpp | 11 + .../configuration/configure_general.cpp | 8 + .../configuration/configure_general.ui | 72 +++-- src/citra_qt/main.cpp | 61 ++++ src/citra_qt/main.h | 12 +- src/citra_qt/main.ui | 16 + src/citra_qt/ui_settings.h | 4 + src/citra_qt/updater/updater.cpp | 304 ++++++++++++++++++ src/citra_qt/updater/updater.h | 120 +++++++ src/citra_qt/updater/updater_p.h | 66 ++++ 11 files changed, 651 insertions(+), 28 deletions(-) create mode 100644 src/citra_qt/updater/updater.cpp create mode 100644 src/citra_qt/updater/updater.h create mode 100644 src/citra_qt/updater/updater_p.h diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 8b6c09c2b..e4929a0b4 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -23,9 +23,10 @@ set(SRCS debugger/profiler.cpp debugger/registers.cpp debugger/wait_tree.cpp - aboutdialog.cpp + updater/updater.cpp util/spinbox.cpp util/util.cpp + aboutdialog.cpp bootmanager.cpp game_list.cpp hotkeys.cpp @@ -56,6 +57,8 @@ set(HEADERS debugger/profiler.h debugger/registers.h debugger/wait_tree.h + updater/updater.h + updater/updater_p.h util/spinbox.h util/util.h aboutdialog.h diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 601c8b0e3..97993e426 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -157,6 +157,12 @@ void Config::ReadValues() { qt_config->beginGroup("UI"); UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString(); + qt_config->beginGroup("Updater"); + UISettings::values.check_for_update_on_start = + qt_config->value("check_for_update_on_start", true).toBool(); + UISettings::values.update_on_close = qt_config->value("update_on_close", false).toBool(); + qt_config->endGroup(); + qt_config->beginGroup("UILayout"); UISettings::values.geometry = qt_config->value("geometry").toByteArray(); UISettings::values.state = qt_config->value("state").toByteArray(); @@ -307,6 +313,11 @@ void Config::SaveValues() { qt_config->beginGroup("UI"); qt_config->setValue("theme", UISettings::values.theme); + qt_config->beginGroup("Updater"); + qt_config->setValue("check_for_update_on_start", UISettings::values.check_for_update_on_start); + qt_config->setValue("update_on_close", UISettings::values.update_on_close); + qt_config->endGroup(); + qt_config->beginGroup("UILayout"); qt_config->setValue("geometry", UISettings::values.geometry); qt_config->setValue("state", UISettings::values.state); diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index 939379717..f8f6c305d 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -20,6 +20,7 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent) this->setConfiguration(); ui->toggle_cpu_jit->setEnabled(!Core::System::GetInstance().IsPoweredOn()); + ui->updateBox->setVisible(UISettings::values.updater_found); } ConfigureGeneral::~ConfigureGeneral() {} @@ -29,6 +30,9 @@ void ConfigureGeneral::setConfiguration() { ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing); ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit); + ui->toggle_update_check->setChecked(UISettings::values.check_for_update_on_start); + ui->toggle_auto_update->setChecked(UISettings::values.update_on_close); + // The first item is "auto-select" with actual value -1, so plus one here will do the trick ui->region_combobox->setCurrentIndex(Settings::values.region_value + 1); @@ -40,6 +44,10 @@ void ConfigureGeneral::applyConfiguration() { UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked(); UISettings::values.theme = ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString(); + + UISettings::values.check_for_update_on_start = ui->toggle_update_check->isChecked(); + UISettings::values.update_on_close = ui->toggle_auto_update->isChecked(); + Settings::values.region_value = ui->region_combobox->currentIndex() - 1; Settings::values.use_cpu_jit = ui->toggle_cpu_jit->isChecked(); Settings::Apply(); diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui index eedf2cbb0..00e5b49f5 100644 --- a/src/citra_qt/configuration/configure_general.ui +++ b/src/citra_qt/configuration/configure_general.ui @@ -25,16 +25,16 @@ - + - Search sub-directories for games + Confirm exit while emulation is running - + - Confirm exit while emulation is running + Search sub-directories for games @@ -44,24 +44,51 @@ - - - Performance - - - - - - - - Enable CPU JIT - - - - - + + + Updates + + + + + + + + Check for updates on start + + + + + + + Silently auto update after closing + + + - + + + + + + + + Performance + + + + + + + + Enable CPU JIT + + + + + + + @@ -149,8 +176,7 @@ - - + diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 00230d4bd..5976e092b 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -30,6 +30,7 @@ #include "citra_qt/hotkeys.h" #include "citra_qt/main.h" #include "citra_qt/ui_settings.h" +#include "citra_qt/updater/updater.h" #include "common/logging/backend.h" #include "common/logging/filter.h" #include "common/logging/log.h" @@ -100,6 +101,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { InitializeDebugWidgets(); InitializeRecentFileMenuActions(); InitializeHotkeys(); + ShowUpdaterWidgets(); SetDefaultUIGeometry(); RestoreUIState(); @@ -118,6 +120,10 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { // Show one-time "callout" messages to the user ShowCallouts(); + if (UISettings::values.check_for_update_on_start) { + CheckForUpdates(); + } + QStringList args = QApplication::arguments(); if (args.length() >= 2) { BootGame(args[1]); @@ -139,6 +145,10 @@ void GMainWindow::InitializeWidgets() { game_list = new GameList(this); ui.horizontalLayout->addWidget(game_list); + // Setup updater + updater = new Updater(this); + UISettings::values.updater_found = updater->HasUpdater(); + // Create status bar message_label = new QLabel(); // Configured separately for left alignment @@ -268,6 +278,13 @@ void GMainWindow::InitializeHotkeys() { }); } +void GMainWindow::ShowUpdaterWidgets() { + ui.action_Check_For_Updates->setVisible(UISettings::values.updater_found); + ui.action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found); + + connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound); +} + void GMainWindow::SetDefaultUIGeometry() { // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half const QRect screenRect = QApplication::desktop()->screenGeometry(this); @@ -346,6 +363,10 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_FAQ, &QAction::triggered, []() { QDesktopServices::openUrl(QUrl("https://citra-emu.org/wiki/faq/")); }); connect(ui.action_About, &QAction::triggered, this, &GMainWindow::OnMenuAboutCitra); + connect(ui.action_Check_For_Updates, &QAction::triggered, this, + &GMainWindow::OnCheckForUpdates); + connect(ui.action_Open_Maintenance_Tool, &QAction::triggered, this, + &GMainWindow::OnOpenUpdater); } void GMainWindow::OnDisplayTitleBars(bool show) { @@ -368,6 +389,46 @@ void GMainWindow::OnDisplayTitleBars(bool show) { } } +void GMainWindow::OnCheckForUpdates() { + CheckForUpdates(); +} + +void GMainWindow::CheckForUpdates() { + if (updater->CheckForUpdates()) { + LOG_INFO(Frontend, "Update check started"); + } else { + LOG_WARNING(Frontend, "Unable to start check for updates"); + } +} + +void GMainWindow::OnUpdateFound(bool found, bool error) { + if (error) { + LOG_WARNING(Frontend, "Update check failed"); + return; + } + + if (!found) { + LOG_INFO(Frontend, "No updates found"); + return; + } + + LOG_INFO(Frontend, "Update found!"); + auto result = QMessageBox::question( + this, tr("Update available!"), + tr("An update for Citra is available. Do you wish to install it now?

" + "This will terminate emulation, if it is running."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (result == QMessageBox::Yes) { + updater->LaunchUIOnExit(); + close(); + } +} + +void GMainWindow::OnOpenUpdater() { + updater->LaunchUI(); +} + bool GMainWindow::LoadROM(const QString& filename) { // Shutdown previous session if the emu thread is still active... if (emu_thread != nullptr) diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 18354bdf7..fe7ff6c4a 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -2,8 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#ifndef _CITRA_QT_MAIN_HXX_ -#define _CITRA_QT_MAIN_HXX_ +#pragma once #include #include @@ -24,6 +23,7 @@ class GRenderWindow; class MicroProfileDialog; class ProfilerWidget; class RegistersWidget; +class Updater; class WaitTreeWidget; class AboutDialog; @@ -82,6 +82,8 @@ private: void ShutdownGame(); void ShowCallouts(); + void ShowUpdaterWidgets(); + void CheckForUpdates(); /** * Stores the filename in the recently loaded files list. @@ -134,6 +136,9 @@ private slots: void OnCoreError(Core::System::ResultStatus, std::string); /// Called whenever a user selects Help->About Citra void OnMenuAboutCitra(); + void OnUpdateFound(bool found, bool error); + void OnCheckForUpdates(); + void OnOpenUpdater(); private: void UpdateStatusBar(); @@ -166,6 +171,7 @@ private: GraphicsVertexShaderWidget* graphicsVertexShaderWidget; GraphicsTracingWidget* graphicsTracingWidget; WaitTreeWidget* waitTreeWidget; + Updater* updater; QAction* actions_recent_files[max_recent_files_item]; @@ -174,5 +180,3 @@ protected: void dragEnterEvent(QDragEnterEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; }; - -#endif // _CITRA_QT_MAIN_HXX_ diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index 442904f01..417a8a6a6 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -96,6 +96,9 @@ &Help + + + @@ -211,6 +214,19 @@ Fullscreen + + + Modify Citra Install + + + Opens the maintenance tool to modify your Citra installation + + + + + Check for Updates + + diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h index ffbe14bc3..dba4c5d3c 100644 --- a/src/citra_qt/ui_settings.h +++ b/src/citra_qt/ui_settings.h @@ -39,6 +39,10 @@ struct Values { bool confirm_before_closing; bool first_start; + bool updater_found; + bool update_on_close; + bool check_for_update_on_start; + QString roms_path; QString symbols_path; QString gamedir; diff --git a/src/citra_qt/updater/updater.cpp b/src/citra_qt/updater/updater.cpp new file mode 100644 index 000000000..399300266 --- /dev/null +++ b/src/citra_qt/updater/updater.cpp @@ -0,0 +1,304 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +// +// Based on the original work by Felix Barx +// Copyright (c) 2015, Felix Barz +// All rights reserved. + +#include +#include +#include +#include +#include +#include "citra_qt/ui_settings.h" +#include "citra_qt/updater/updater.h" +#include "citra_qt/updater/updater_p.h" +#include "common/logging/log.h" + +#ifdef Q_OS_OSX +#define DEFAULT_TOOL_PATH QStringLiteral("../../../../maintenancetool") +#else +#define DEFAULT_TOOL_PATH QStringLiteral("../maintenancetool") +#endif + +Updater::Updater(QObject* parent) : Updater(DEFAULT_TOOL_PATH, parent) {} + +Updater::Updater(const QString& maintenance_tool_path, QObject* parent) + : QObject(parent), backend(std::make_unique(this)) { + backend->tool_path = UpdaterPrivate::ToSystemExe(maintenance_tool_path); +} + +Updater::~Updater() = default; + +bool Updater::ExitedNormally() const { + return backend->normal_exit; +} + +int Updater::ErrorCode() const { + return backend->last_error_code; +} + +QByteArray Updater::ErrorLog() const { + return backend->last_error_log; +} + +bool Updater::IsRunning() const { + return backend->running; +} + +QList Updater::LatestUpdateInfo() const { + return backend->update_info; +} + +bool Updater::HasUpdater() const { + return backend->HasUpdater(); +} + +bool Updater::CheckForUpdates() { + return backend->StartUpdateCheck(); +} + +void Updater::AbortUpdateCheck(int max_delay, bool async) { + backend->StopUpdateCheck(max_delay, async); +} + +void Updater::LaunchUI() { + backend->LaunchUI(); +} + +void Updater::SilentlyUpdate() { + backend->SilentlyUpdate(); +} + +void Updater::LaunchUIOnExit() { + backend->LaunchUIOnExit(); +} + +Updater::UpdateInfo::UpdateInfo() = default; + +Updater::UpdateInfo::UpdateInfo(const Updater::UpdateInfo&) = default; + +Updater::UpdateInfo::UpdateInfo(QString name, QString version, quint64 size) + : name(std::move(name)), version(std::move(version)), size(size) {} + +UpdaterPrivate::UpdaterPrivate(Updater* parent_ptr) : QObject(nullptr), parent(parent_ptr) { + connect(qApp, &QCoreApplication::aboutToQuit, this, &UpdaterPrivate::AboutToExit, + Qt::DirectConnection); + qRegisterMetaType("QProcess::ExitStatus"); +} + +UpdaterPrivate::~UpdaterPrivate() { + if (main_process && main_process->state() != QProcess::NotRunning) { + main_process->kill(); + main_process->waitForFinished(1000); + } +} + +QString UpdaterPrivate::ToSystemExe(QString base_path) { +#if defined(Q_OS_WIN32) + if (!base_path.endsWith(QStringLiteral(".exe"))) + return base_path + QStringLiteral(".exe"); + else + return base_path; +#elif defined(Q_OS_OSX) + if (base_path.endsWith(QStringLiteral(".app"))) + base_path.truncate(base_path.lastIndexOf(QStringLiteral("."))); + return base_path + QStringLiteral(".app/Contents/MacOS/") + QFileInfo(base_path).fileName(); +#elif defined(Q_OS_UNIX) + return base_path; +#endif +} + +bool UpdaterPrivate::HasUpdater() const { + QFileInfo tool_info(QCoreApplication::applicationDirPath(), tool_path); + return tool_info.exists(); +} + +bool UpdaterPrivate::StartUpdateCheck() { + if (running || !HasUpdater()) { + return false; + } + + update_info.clear(); + normal_exit = true; + last_error_code = EXIT_SUCCESS; + last_error_log.clear(); + + QFileInfo tool_info(QCoreApplication::applicationDirPath(), tool_path); + main_process = new QProcess(this); + main_process->setProgram(tool_info.absoluteFilePath()); + main_process->setArguments({QStringLiteral("--checkupdates"), QStringLiteral("-v")}); + + connect(main_process, + static_cast(&QProcess::finished), this, + &UpdaterPrivate::UpdaterReady, Qt::QueuedConnection); + connect(main_process, static_cast(&QProcess::error), + this, &UpdaterPrivate::UpdaterError, Qt::QueuedConnection); + + main_process->start(QIODevice::ReadOnly); + running = true; + + emit parent->UpdateInfoChanged(update_info); + emit parent->RunningChanged(true); + return true; +} + +void UpdaterPrivate::StopUpdateCheck(int delay, bool async) { + if (main_process == nullptr || main_process->state() == QProcess::NotRunning) { + return; + } + + if (delay > 0) { + main_process->terminate(); + if (async) { + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); + + connect(timer, &QTimer::timeout, [=]() { + StopUpdateCheck(0, false); + timer->deleteLater(); + }); + + timer->start(delay); + } else { + if (!main_process->waitForFinished(delay)) { + main_process->kill(); + main_process->waitForFinished(100); + } + } + } else { + main_process->kill(); + main_process->waitForFinished(100); + } +} + +XMLParseResult UpdaterPrivate::ParseResult(const QByteArray& output, + QList& out) { + const auto out_string = QString::fromUtf8(output); + const auto xml_begin = out_string.indexOf(QStringLiteral("")); + if (xml_begin < 0) + return XMLParseResult::NoUpdate; + const auto xml_end = out_string.indexOf(QStringLiteral(""), xml_begin); + if (xml_end < 0) + return XMLParseResult::NoUpdate; + + QList updates; + QXmlStreamReader reader(out_string.mid(xml_begin, (xml_end + 10) - xml_begin)); + + reader.readNextStartElement(); + // should always work because it was search for + if (reader.name() != QStringLiteral("updates")) { + return XMLParseResult::InvalidXML; + } + + while (reader.readNextStartElement()) { + if (reader.name() != QStringLiteral("update")) + return XMLParseResult::InvalidXML; + + auto ok = false; + Updater::UpdateInfo info( + reader.attributes().value(QStringLiteral("name")).toString(), + reader.attributes().value(QStringLiteral("version")).toString(), + reader.attributes().value(QStringLiteral("size")).toULongLong(&ok)); + + if (info.name.isEmpty() || info.version.isNull() || !ok) + return XMLParseResult::InvalidXML; + if (reader.readNextStartElement()) + return XMLParseResult::InvalidXML; + + updates.append(info); + } + + if (reader.hasError()) { + LOG_ERROR(Frontend, "Cannot read xml for update: %s", + reader.errorString().toStdString().c_str()); + return XMLParseResult::InvalidXML; + } + + out = updates; + return XMLParseResult::Success; +} + +void UpdaterPrivate::UpdaterReady(int exit_code, QProcess::ExitStatus exit_status) { + if (main_process == nullptr) { + return; + } + + if (exit_status != QProcess::NormalExit) { + UpdaterError(QProcess::Crashed); + return; + } + + normal_exit = true; + last_error_code = exit_code; + last_error_log = main_process->readAllStandardError(); + const auto update_out = main_process->readAllStandardOutput(); + main_process->deleteLater(); + main_process = nullptr; + + running = false; + emit parent->RunningChanged(false); + + QList update_info; + auto err = ParseResult(update_out, update_info); + bool has_error = false; + + if (err == XMLParseResult::Success) { + if (!update_info.isEmpty()) + emit parent->UpdateInfoChanged(update_info); + } else if (err == XMLParseResult::InvalidXML) { + has_error = true; + } + + emit parent->CheckUpdatesDone(!update_info.isEmpty(), has_error); +} + +void UpdaterPrivate::UpdaterError(QProcess::ProcessError error) { + if (main_process) { + normal_exit = false; + last_error_code = error; + last_error_log = main_process->errorString().toUtf8(); + main_process->deleteLater(); + main_process = nullptr; + + running = false; + emit parent->RunningChanged(false); + emit parent->CheckUpdatesDone(false, true); + } +} + +void UpdaterPrivate::LaunchWithArguments(const QStringList& args) { + if (!HasUpdater()) { + return; + } + + QFileInfo tool_info(QCoreApplication::applicationDirPath(), tool_path); + + if (!QProcess::startDetached(tool_info.absoluteFilePath(), args, tool_info.absolutePath())) { + LOG_WARNING(Frontend, "Unable to start program %s", + tool_info.absoluteFilePath().toStdString().c_str()); + } +} + +void UpdaterPrivate::LaunchUI() { + LOG_INFO(Frontend, "Launching update UI..."); + LaunchWithArguments(run_arguments); +} + +void UpdaterPrivate::SilentlyUpdate() { + LOG_INFO(Frontend, "Launching silent update..."); + LaunchWithArguments(silent_arguments); +} + +void UpdaterPrivate::AboutToExit() { + if (launch_ui_on_exit) { + LaunchUI(); + } else if (UISettings::values.update_on_close) { + SilentlyUpdate(); + } +} + +void UpdaterPrivate::LaunchUIOnExit() { + launch_ui_on_exit = true; +} diff --git a/src/citra_qt/updater/updater.h b/src/citra_qt/updater/updater.h new file mode 100644 index 000000000..a7220dd91 --- /dev/null +++ b/src/citra_qt/updater/updater.h @@ -0,0 +1,120 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +// +// Based on the original work by Felix Barx +// Copyright (c) 2015, Felix Barz +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of QtAutoUpdater nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include +#include +#include +#include +#include +#include + +class UpdaterPrivate; + +/// The main updater. Can check for updates and run the maintenancetool as updater +class Updater : public QObject { + Q_OBJECT; + + /// Specifies whether the updater is currently checking for updates or not + Q_PROPERTY(bool running READ IsRunning NOTIFY RunningChanged); + /// Holds extended information about the last update check + Q_PROPERTY(QList update_info READ LatestUpdateInfo NOTIFY UpdateInfoChanged); + +public: + /// Provides information about updates for components + struct UpdateInfo { + /// The name of the component that has an update + QString name; + /// The new version for that compontent + QString version; + /// The update download size (in Bytes) + quint64 size = 0; + + /// Default Constructor + UpdateInfo(); + /// Copy Constructor + UpdateInfo(const UpdateInfo& other); + /// Constructor that takes name, version and size + UpdateInfo(QString name, QString version, quint64 size); + }; + + /// Default constructor + explicit Updater(QObject* parent = nullptr); + /// Constructor with an explicitly set path + explicit Updater(const QString& maintenance_tool_path, QObject* parent = nullptr); + /// Destroys the updater and kills the update check (if running) + ~Updater(); + + /// Returns `true`, if the updater exited normally + bool ExitedNormally() const; + /// Returns the mainetancetools error code of the last update + int ErrorCode() const; + /// returns the error output (stderr) of the last update + QByteArray ErrorLog() const; + + /// readAcFn{Updater::running} + bool IsRunning() const; + /// readAcFn{Updater::updateInfo} + QList LatestUpdateInfo() const; + + /// Launches the updater UI formally + void LaunchUI(); + + /// Silently updates the application in the background + void SilentlyUpdate(); + + /// Checks to see if a updater application is available + bool HasUpdater() const; + + /// Instead of silently updating, explictly open the UI on shutdown + void LaunchUIOnExit(); + +public slots: + /// Starts checking for updates + bool CheckForUpdates(); + /// Aborts checking for updates + void AbortUpdateCheck(int max_delay = 5000, bool async = false); + +signals: + /// Will be emitted as soon as the updater finished checking for updates + void CheckUpdatesDone(bool has_updates, bool has_error); + + /// notifyAcFn{Updater::running} + void RunningChanged(bool running); + /// notifyAcFn{Updater::updateInfo} + void UpdateInfoChanged(QList update_info); + +private: + std::unique_ptr backend; +}; diff --git a/src/citra_qt/updater/updater_p.h b/src/citra_qt/updater/updater_p.h new file mode 100644 index 000000000..210400535 --- /dev/null +++ b/src/citra_qt/updater/updater_p.h @@ -0,0 +1,66 @@ +// Copyright 2017 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +// +// Based on the original work by Felix Barx +// Copyright (c) 2015, Felix Barz +// All rights reserved. + +#pragma once + +#include +#include "citra_qt/updater/updater.h" + +enum class XMLParseResult { + Success, + NoUpdate, + InvalidXML, +}; + +class UpdaterPrivate : public QObject { + Q_OBJECT; + +public: + explicit UpdaterPrivate(Updater* parent_ptr); + ~UpdaterPrivate(); + + static QString ToSystemExe(QString base_path); + + bool HasUpdater() const; + + bool StartUpdateCheck(); + + void LaunchWithArguments(const QStringList& args); + void LaunchUI(); + void SilentlyUpdate(); + + void LaunchUIOnExit(); + +public slots: + void StopUpdateCheck(int delay, bool async); + void UpdaterReady(int exit_code, QProcess::ExitStatus exit_status); + void UpdaterError(QProcess::ProcessError error); + + void AboutToExit(); + +private: + XMLParseResult ParseResult(const QByteArray& output, QList& out); + + Updater* parent; + + QString tool_path{}; + QList update_info{}; + bool normal_exit = true; + int last_error_code = 0; + QByteArray last_error_log = EXIT_SUCCESS; + + bool running = false; + QProcess* main_process = nullptr; + + bool launch_ui_on_exit = false; + + QStringList run_arguments{"--updater"}; + QStringList silent_arguments{"--silentUpdate"}; + + friend class Updater; +};