// 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/uisettings.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::errorOccurred), 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: {}", reader.errorString().toStdString()); 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 {}", tool_info.absoluteFilePath().toStdString()); } } 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; }