mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-30 21:30:04 +00:00 
			
		
		
		
	Qt updater integration, based on QtAutoUpdater
This commit is contained in:
		
							parent
							
								
									ee5aecee3f
								
							
						
					
					
						commit
						2e6c80d1aa
					
				
					 11 changed files with 651 additions and 28 deletions
				
			
		
							
								
								
									
										304
									
								
								src/citra_qt/updater/updater.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								src/citra_qt/updater/updater.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <QCoreApplication> | ||||
| #include <QDir> | ||||
| #include <QFileInfo> | ||||
| #include <QTimer> | ||||
| #include <QXmlStreamReader> | ||||
| #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<UpdaterPrivate>(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::UpdateInfo> 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>("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<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, | ||||
|             &UpdaterPrivate::UpdaterReady, Qt::QueuedConnection); | ||||
|     connect(main_process, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&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<Updater::UpdateInfo>& out) { | ||||
|     const auto out_string = QString::fromUtf8(output); | ||||
|     const auto xml_begin = out_string.indexOf(QStringLiteral("<updates>")); | ||||
|     if (xml_begin < 0) | ||||
|         return XMLParseResult::NoUpdate; | ||||
|     const auto xml_end = out_string.indexOf(QStringLiteral("</updates>"), xml_begin); | ||||
|     if (xml_end < 0) | ||||
|         return XMLParseResult::NoUpdate; | ||||
| 
 | ||||
|     QList<Updater::UpdateInfo> 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<Updater::UpdateInfo> 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; | ||||
| } | ||||
							
								
								
									
										120
									
								
								src/citra_qt/updater/updater.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/citra_qt/updater/updater.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <memory> | ||||
| #include <QDateTime> | ||||
| #include <QList> | ||||
| #include <QScopedPointer> | ||||
| #include <QString> | ||||
| #include <QStringList> | ||||
| 
 | ||||
| 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<UpdateInfo> 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<UpdateInfo> 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<Updater::UpdateInfo> update_info); | ||||
| 
 | ||||
| private: | ||||
|     std::unique_ptr<UpdaterPrivate> backend; | ||||
| }; | ||||
							
								
								
									
										66
									
								
								src/citra_qt/updater/updater_p.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/citra_qt/updater/updater_p.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <QtCore/QProcess> | ||||
| #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<Updater::UpdateInfo>& out); | ||||
| 
 | ||||
|     Updater* parent; | ||||
| 
 | ||||
|     QString tool_path{}; | ||||
|     QList<Updater::UpdateInfo> 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; | ||||
| }; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue