2017-08-19 07:05:49 +02:00
|
|
|
// 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) {
|
2017-10-31 05:41:18 +01:00
|
|
|
QTimer* timer = new QTimer(this);
|
|
|
|
timer->setSingleShot(true);
|
2017-08-19 07:05:49 +02:00
|
|
|
|
2017-10-31 05:41:18 +01:00
|
|
|
connect(timer, &QTimer::timeout, [=]() {
|
|
|
|
StopUpdateCheck(0, false);
|
|
|
|
timer->deleteLater();
|
|
|
|
});
|
2017-08-19 07:05:49 +02:00
|
|
|
|
2017-10-31 05:41:18 +01:00
|
|
|
timer->start(delay);
|
2017-08-19 07:05:49 +02:00
|
|
|
} 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;
|
|
|
|
}
|