Add BuildsRequest and config.json support to qt launcher

This commit is contained in:
Ryan Huffman 2019-10-02 14:22:50 -07:00
parent dbc136c438
commit f236b64b06
6 changed files with 267 additions and 130 deletions

View file

@ -136,6 +136,8 @@ set(src_files
src/LoginRequest.cpp
src/SignupRequest.h
src/SignupRequest.cpp
src/BuildsRequest.h
src/BuildsRequest.cpp
src/UserSettingsRequest.h
src/UserSettingsRequest.cpp
src/PathUtils.h
@ -231,6 +233,7 @@ if (APPLE)
endif()
if (LAUNCHER_SOURCE_TREE_RESOURCES)
target_compile_definitions(${PROJECT_NAME} PRIVATE RESOURCE_PREFIX_URL="${CMAKE_CURRENT_SOURCE_DIR}/resources/")
message("Use source tree resources path: file://${CMAKE_CURRENT_SOURCE_DIR}/resources/")

View file

@ -0,0 +1,115 @@
#include "BuildsRequest.h"
#include "Helper.h"
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QProcessEnvironment>
bool Builds::getBuild(QString tag, Build* outBuild) {
if (tag.isNull()) {
tag = defaultTag;
}
for (auto& build : builds) {
if (build.tag == tag) {
*outBuild = build;
return true;
}
}
return false;
}
void BuildsRequest::send(QNetworkAccessManager& nam) {
QString latestBuildRequestUrl { "https://thunder.highfidelity.com/builds/api/tags/latest/?format=json" };
QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment();
if (processEnvironment.contains("HQ_LAUNCHER_BUILDS_URL")) {
latestBuildRequestUrl = processEnvironment.value("HQ_LAUNCHER_BUILDS_URL");
}
QNetworkRequest request{ QUrl(latestBuildRequestUrl) };
auto reply = nam.get(request);
QObject::connect(reply, &QNetworkReply::finished, this, &BuildsRequest::receivedResponse);
}
void BuildsRequest::receivedResponse() {
_state = State::Finished;
auto reply = static_cast<QNetworkReply*>(sender());
if (reply->error()) {
qDebug() << "Error getting builds from thunder: " << reply->errorString();
_error = Error::Unknown;
emit finished();
return;
} else {
qDebug() << "Builds reply has been received";
auto data = reply->readAll();
QJsonParseError parseError;
auto doc = QJsonDocument::fromJson(data, &parseError);
if (parseError.error) {
qDebug() << "Error parsing response from thunder: " << data;
_error = Error::Unknown;
} else {
auto root = doc.object();
if (!root.contains("default_tag")) {
//setApplicationState(ApplicationState::UnexpectedError);
_error = Error::MissingDefaultTag;
emit finished();
return;
}
_latestBuilds.defaultTag = root["default_tag"].toString();
auto results = root["results"];
if (!results.isArray()) {
//setApplicationState(ApplicationState::UnexpectedError);
_error = Error::MalformedResponse;
emit finished();
return;
}
for (auto result : results.toArray()) {
auto entry = result.toObject();
Build build;
build.tag = entry["name"].toString();
build.latestVersion = entry["latest_version"].toInt();
build.buildNumber = entry["build_number"].toInt();
#ifdef Q_OS_WIN
build.installerZipURL = entry["installers"].toObject()["windows"].toObject()["zip_url"].toString();
#elif defined(Q_OS_MACOS)
build.installerZipURL = entry["installers"].toObject()["mac"].toObject()["zip_url"].toString();
#else
#error "Launcher is only supported on Windows and Mac OS"
#endif
_latestBuilds.builds.push_back(build);
}
auto launcherResults = root["launcher"].toObject();
Build launcherBuild;
launcherBuild.latestVersion = launcherResults["version"].toInt();
#ifdef Q_OS_WIN
launcherBuild.installerZipURL = launcherResults["windows"].toObject()["url"].toString();
#elif defined(Q_OS_MACOS)
launcherBuild.installerZipURL = launcherResults["mac"].toObject()["url"].toString();
#else
#error "Launcher is only supported on Windows and Mac OS"
#endif
_latestBuilds.launcherBuild = launcherBuild;
}
}
emit finished();
}

View file

@ -0,0 +1,54 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
struct Build {
QString tag{ QString::null };
int latestVersion{ 0 };
int buildNumber{ 0 };
QString installerZipURL{ QString::null };
};
struct Builds {
bool getBuild(QString tag, Build* outBuild);
QString defaultTag;
std::vector<Build> builds;
Build launcherBuild;
};
class BuildsRequest : public QObject {
Q_OBJECT
public:
enum class State {
Unsent,
Sending,
Finished
};
enum class Error {
None = 0,
Unknown,
MalformedResponse,
MissingDefaultTag,
};
Q_ENUM(Error)
void send(QNetworkAccessManager& nam);
Error getError() const { return _error; }
const Builds& getLatestBuilds() const { return _latestBuilds; }
signals:
void finished();
private slots:
void receivedResponse();
private:
State _state { State::Unsent };
Error _error { Error::None };
Builds _latestBuilds;
};

View file

@ -27,6 +27,10 @@
#include <qregularexpression.h>
//#define BREAK_ON_ERROR
const QString configLoggedInKey{ "loggedIn" };
const QString configLauncherPathKey{ "launcherPath" };
QString LauncherState::getContentCachePath() const {
return _launcherDirectory.filePath("cache");
@ -45,32 +49,30 @@ QString LauncherState::getClientExecutablePath() const {
#endif
}
QString LauncherState::getConfigFilePath() const {
QDir clientDirectory = getClientDirectory();
return clientDirectory.absoluteFilePath("config.json");
}
QString LauncherState::getLauncherFilePath() const {
#if defined(Q_OS_WIN)
return _launcherDirectory.absoluteFilePath("launcher.exe");
#elif defined(Q_OS_MACOS)
return _launcherDirectory.absoluteFilePath("launcher.app");
#endif
}
bool LauncherState::shouldDownloadContentCache() const {
return !_contentCacheURL.isNull() && !QFile::exists(getContentCachePath());
}
bool LatestBuilds::getBuild(QString tag, Build* outBuild) {
if (tag.isNull()) {
tag = defaultTag;
}
for (auto& build : builds) {
if (build.tag == tag) {
*outBuild = build;
return true;
}
}
return false;
}
static const std::array<QString, LauncherState::UIState::UI_STATE_NUM> QML_FILE_FOR_UI_STATE =
{ { "qml/SplashScreen.qml", "qml/HFBase/CreateAccountBase.qml", "qml/HFBase/LoginBase.qml", "DisplayName.qml",
"qml/Download.qml", "qml/DownloadFinished.qml", "qml/HFBase/Error.qml" } };
void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) {
if (_applicationState != state) {
#ifdef Q_OS_WIN
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
setApplicationState(ApplicationState::UnexpectedError);
@ -84,7 +86,7 @@ void LauncherState::ASSERT_STATE(const std::vector<LauncherState::ApplicationSta
}
}
#ifdef Q_OS_WIN
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
setApplicationState(ApplicationState::UnexpectedError);
@ -126,13 +128,13 @@ LauncherState::UIState LauncherState::getUIState() const {
case ApplicationState::LaunchingHighFidelity:
return DOWNLOAD_FINSISHED;
case ApplicationState::UnexpectedError:
#ifdef Q_OS_WIN
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
return ERROR_SCREEN;
default:
qDebug() << "FATAL: No UI for" << _applicationState;
#ifdef Q_OS_WIN
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
return ERROR_SCREEN;
@ -147,97 +149,34 @@ LauncherState::LastLoginError LauncherState::getLastLoginError() const {
return _lastLoginError;
}
void LauncherState::requestBuilds() {
ASSERT_STATE(ApplicationState::Init);
setApplicationState(ApplicationState::RequestingBuilds);
// TODO Show splash screen until this request is complete
QString latestBuildRequestUrl { "https://thunder.highfidelity.com/builds/api/tags/latest/?format=json" };
QProcessEnvironment processEnvironment =QProcessEnvironment::systemEnvironment();
if (processEnvironment.contains("HQ_LAUNCHER_BUILDS_URL")) {
latestBuildRequestUrl = processEnvironment.value("HQ_LAUNCHER_BUILDS_URL");
}
auto request = new QNetworkRequest(QUrl(latestBuildRequestUrl));
auto reply = _networkAccessManager.get(*request);
QObject::connect(reply, &QNetworkReply::finished, this, &LauncherState::receivedBuildsReply);
}
void LauncherState::restart() {
setApplicationState(ApplicationState::Init);
requestBuilds();
}
void LauncherState::receivedBuildsReply() {
auto reply = static_cast<QNetworkReply*>(sender());
void LauncherState::requestBuilds() {
ASSERT_STATE(ApplicationState::Init);
setApplicationState(ApplicationState::RequestingBuilds);
auto request = new BuildsRequest();
QObject::connect(request, &BuildsRequest::finished, this, [=] {
ASSERT_STATE(ApplicationState::RequestingBuilds);
if (reply->error()) {
qDebug() << "Error getting builds from thunder: " << reply->errorString();
} else {
qDebug() << "Builds reply has been received";
auto data = reply->readAll();
QJsonParseError parseError;
auto doc = QJsonDocument::fromJson(data, &parseError);
if (parseError.error) {
qDebug() << "Error parsing response from thunder: " << data;
} else {
auto root = doc.object();
if (!root.contains("default_tag")) {
setApplicationState(ApplicationState::UnexpectedError);
if (request->getError() != BuildsRequest::Error::None) {
setApplicationStateError("Could not retrieve latest builds");
return;
}
_latestBuilds.defaultTag = root["default_tag"].toString();
auto results = root["results"];
if (!results.isArray()) {
setApplicationState(ApplicationState::UnexpectedError);
return;
}
for (auto result : results.toArray()) {
auto entry = result.toObject();
Build build;
build.tag = entry["name"].toString();
build.latestVersion = entry["latest_version"].toInt();
build.buildNumber = entry["build_number"].toInt();
#ifdef Q_OS_WIN
build.installerZipURL = entry["installers"].toObject()["windows"].toObject()["zip_url"].toString();
#elif defined(Q_OS_MACOS)
build.installerZipURL = entry["installers"].toObject()["mac"].toObject()["zip_url"].toString();
#else
#error "Launcher is only supported on Windows and Mac OS"
#endif
_latestBuilds.builds.push_back(build);
}
auto launcherResults = root["launcher"].toObject();
Build launcherBuild;
launcherBuild.latestVersion = launcherResults["version"].toInt();
#ifdef Q_OS_WIN
launcherBuild.installerZipURL = launcherResults["windows"].toObject()["url"].toString();
#elif defined(Q_OS_MACOS)
launcherBuild.installerZipURL = launcherResults["mac"].toObject()["url"].toString();
#else
#error "Launcher is only supported on Windows and Mac OS"
#endif
_latestBuilds.launcherBuild = launcherBuild;
}
}
_latestBuilds = request->getLatestBuilds();
if (shouldDownloadLauncher()) {
//downloadLauncher();
}
getCurrentClientVersion();
}
});
request->send(_networkAccessManager);
}
bool LauncherState::shouldDownloadLauncher() {
return _latestBuilds.launcherBuild.latestVersion != atoi(LAUNCHER_BUILD_VERSION);
@ -271,8 +210,30 @@ void LauncherState::getCurrentClientVersion() {
}
qDebug() << "Current client version is: " << _currentClientVersion;
{
auto path = getConfigFilePath();
QFile configFile{ path };
if (configFile.open(QIODevice::ReadOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(configFile.readAll());
auto root = doc.object();
_config.launcherPath = getLauncherFilePath();
_config.loggedIn = false;
if (root.contains(configLoggedInKey)) {
_config.loggedIn = root["loggedIn"].toBool();
}
} else {
qDebug() << "Failed to open config.json";
}
}
if (_config.loggedIn) {
downloadClient();
} else {
setApplicationState(ApplicationState::WaitingForSignup);
}
}
void LauncherState::gotoSignup() {
@ -337,6 +298,7 @@ void LauncherState::signup(QString email, QString username, QString password, QS
}
_config.loggedIn = true;
_loginResponse = loginRequest->getToken();
_loginTokenResponse = loginRequest->getRawToken();
@ -372,6 +334,7 @@ void LauncherState::login(QString username, QString password, QString displayNam
return;
}
_config.loggedIn = true;
_loginResponse = request->getToken();
_loginTokenResponse = request->getRawToken();
@ -672,7 +635,17 @@ void LauncherState::launchClient() {
clientPath = installDirectory.absoluteFilePath("interface.app/Contents/MacOS/interface");
#endif
// TODO Get correct home path
auto path = getConfigFilePath();
QFile configFile{ path };
if (configFile.open(QIODevice::WriteOnly)) {
QJsonDocument doc = QJsonDocument::fromJson(configFile.readAll());
doc.setObject({
{ configLoggedInKey, _config.loggedIn },
{ configLauncherPathKey, _config.launcherPath },
});
configFile.write(doc.toJson());
}
QString defaultScriptsPath;
#if defined(Q_OS_WIN)
defaultScriptsPath = installDirectory.filePath("scripts/simplifiedUIBootstrapper.js");
@ -694,7 +667,7 @@ void LauncherState::setApplicationState(ApplicationState state) {
qDebug() << "Changing application state: " << _applicationState << " -> " << state;
if (state == ApplicationState::UnexpectedError) {
#ifdef Q_OS_WIN
#ifdef BREAK_ON_ERROR
__debugbreak();
#endif
}

View file

@ -10,20 +10,11 @@
#include "LoginRequest.h"
#include "SignupRequest.h"
#include "UserSettingsRequest.h"
#include "BuildsRequest.h"
struct Build {
QString tag;
int latestVersion;
int buildNumber;
QString installerZipURL;
};
struct LatestBuilds {
bool getBuild(QString tag, Build* outBuild);
QString defaultTag;
std::vector<Build> builds;
Build launcherBuild;
struct LauncherConfig {
QString launcherPath{ QString::null };
bool loggedIn{ false };
};
class LauncherState : public QObject {
@ -107,7 +98,6 @@ public:
// Request builds
void requestBuilds();
Q_INVOKABLE void receivedBuildsReply();
// Signup
Q_INVOKABLE void signup(QString email, QString username, QString password, QString displayName);
@ -156,13 +146,17 @@ private:
QString getContentCachePath() const;
QString getClientDirectory() const;
QString getClientExecutablePath() const;
QString getConfigFilePath() const;
QString getLauncherFilePath() const;
bool shouldDownloadLauncher();
QNetworkAccessManager _networkAccessManager;
LatestBuilds _latestBuilds;
Builds _latestBuilds;
QDir _launcherDirectory;
LauncherConfig _config;
// Application State
ApplicationState _applicationState { ApplicationState::Init };
LoginToken _loginResponse;

View file

@ -20,8 +20,6 @@ Q_IMPORT_PLUGIN(QtQuick2Plugin);
Q_IMPORT_PLUGIN(QtQuickControls2Plugin);
Q_IMPORT_PLUGIN(QtQuickTemplates2Plugin);
bool hasSuffix(const std::string& path, const std::string& suffix) {
if (path.substr(path.find_last_of(".") + 1) == suffix) {
return true;