From 93c443abc87dc2e25a8b42da95579aab05575413 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 13 Sep 2019 13:57:26 -0700 Subject: [PATCH] Add qt launcher states, download, and install --- launchers/qt/resources/qml/Download.qml | 20 +- .../resources/qml/HFControls/HFTextField.qml | 1 + launchers/qt/resources/qml/root.qml | 11 + launchers/qt/src/Launcher.cpp | 2 +- launchers/qt/src/LauncherState.cpp | 368 ++++++++++++++++-- launchers/qt/src/LauncherState.h | 87 +++-- launchers/qt/src/main.cpp | 2 +- 7 files changed, 428 insertions(+), 63 deletions(-) diff --git a/launchers/qt/resources/qml/Download.qml b/launchers/qt/resources/qml/Download.qml index 8020889e8a..c323d3b505 100644 --- a/launchers/qt/resources/qml/Download.qml +++ b/launchers/qt/resources/qml/Download.qml @@ -62,6 +62,8 @@ Item { width: 394 height: 8 + value: LauncherState.downloadProgress; + anchors { top: secondText.bottom topMargin: 30 @@ -88,14 +90,14 @@ Item { } - PropertyAnimation { - target: progressBar; - loops: Animation.Infinite - property: "value" - from: 0; - to: 1; - duration: 5000 - running: true - } + //PropertyAnimation { + //target: progressBar; + //loops: Animation.Infinite + //property: "value" + //from: 0; + //to: 1; + //duration: 5000 + //running: true + //} } } diff --git a/launchers/qt/resources/qml/HFControls/HFTextField.qml b/launchers/qt/resources/qml/HFControls/HFTextField.qml index 77a6601fdf..547cd94843 100644 --- a/launchers/qt/resources/qml/HFControls/HFTextField.qml +++ b/launchers/qt/resources/qml/HFControls/HFTextField.qml @@ -10,6 +10,7 @@ TextField { horizontalAlignment: TextInput.AlignLeft placeholderText: "PlaceHolder" property string seperatorColor: "#FFFFFF" + selectByMouse: true background: Item { anchors.fill: parent Rectangle { diff --git a/launchers/qt/resources/qml/root.qml b/launchers/qt/resources/qml/root.qml index 0ceda5b189..e80466a6eb 100644 --- a/launchers/qt/resources/qml/root.qml +++ b/launchers/qt/resources/qml/root.qml @@ -4,6 +4,7 @@ import QtQuick 2.3 import QtQuick.Controls 2.1 import HQLauncher 1.0 import "HFControls" + Image { id: root width: 515 @@ -21,4 +22,14 @@ Image { loader.source = url; }); } + + Text { + font.pixelSize: 12 + + anchors.right: root.right + anchors.bottom: root.bottom + + color: "#FFFFFF" + text: LauncherState.uiState.toString() + " - " + LauncherState.applicationState + } } diff --git a/launchers/qt/src/Launcher.cpp b/launchers/qt/src/Launcher.cpp index 20405d5a39..23467624f8 100644 --- a/launchers/qt/src/Launcher.cpp +++ b/launchers/qt/src/Launcher.cpp @@ -11,7 +11,7 @@ Launcher::Launcher(int& argc, char**argv) : QGuiApplication(argc, argv) { QString resourceBinaryLocation = QGuiApplication::applicationDirPath() + "/resources.rcc"; QResource::registerResource(resourceBinaryLocation); _launcherState = std::make_shared(); - _launcherState->setUIState(LauncherState::SPLASH_SCREEN); + //_launcherState->setUIState(LauncherState::SPLASH_SCREEN); _launcherWindow = std::make_unique(); _launcherWindow->rootContext()->setContextProperty("LauncherState", _launcherState.get()); _launcherWindow->setFlags(Qt::FramelessWindowHint); diff --git a/launchers/qt/src/LauncherState.cpp b/launchers/qt/src/LauncherState.cpp index 9c5f66f9cf..d1635c277c 100644 --- a/launchers/qt/src/LauncherState.cpp +++ b/launchers/qt/src/LauncherState.cpp @@ -1,5 +1,9 @@ #include "LauncherState.h" +#include "Unzipper.h" + +#include + #include #include @@ -13,18 +17,56 @@ #include #include +#include + +#include + + +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 QML_FILE_FOR_UI_STATE = { { "qrc:/qml/SplashScreen.qml", "qrc:/qml/Login.qml", "qrc:/qml/DisplayName.qml", "qrc:/qml/Download.qml", "qrc:/qml/DownloadFinshed.qml", "qrc:/qml/Error.qml" } }; -void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) const { - if (_appState != state) { +void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) { + if (_applicationState != state) { +#ifdef Q_OS_WIN __debugbreak(); - exit(0); +#endif + setApplicationState(ApplicationState::UnexpectedError); } } +void LauncherState::ASSERT_STATE(std::vector states) { + for (auto state : states) { + if (_applicationState == state) { + return; + } + } +#ifdef Q_OS_WIN + __debugbreak(); +#endif + setApplicationState(ApplicationState::UnexpectedError); +} + LauncherState::LauncherState() { + _launcherDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + // TODO Fix launcher directory + qDebug() << "Launcher directory: " << _launcherDirectory.absolutePath(); + _launcherDirectory.mkpath(_launcherDirectory.absolutePath()); requestBuilds(); } @@ -36,13 +78,29 @@ void LauncherState::declareQML() { qmlRegisterType("HQLauncher", 1, 0, "LauncherStateEnums"); } -void LauncherState::setUIState(UIState state) { - _uiState = state; - emit updateSourceUrl(getCurrentUISource()); -} - LauncherState::UIState LauncherState::getUIState() const { - return _uiState; + switch (_applicationState) { + case ApplicationState::Init: + case ApplicationState::RequestingBuilds: + return SPLASH_SCREEN; + case ApplicationState::WaitingForLogin: + case ApplicationState::RequestingLogin: + return LOGIN_SCREEN; + case ApplicationState::DownloadingClient: + case ApplicationState::InstallingClient: + case ApplicationState::DownloadingContentCache: + case ApplicationState::InstallingContentCache: + return DOWNLOAD_SCREEN; + case ApplicationState::LaunchingHighFidelity: + return DOWNLOAD_FINSISHED; + case ApplicationState::UnexpectedError: + __debugbreak(); + return ERROR_SCREEN; + default: + qDebug() << "FATAL: No UI for" << _applicationState; + __debugbreak(); + return ERROR_SCREEN; + } } void LauncherState::setLastLoginError(LastLoginError lastLoginError) { @@ -54,8 +112,8 @@ LauncherState::LastLoginError LauncherState::getLastLoginError() const { } void LauncherState::requestBuilds() { - ASSERT_STATE(ApplicationState::INIT); - _appState = ApplicationState::REQUESTING_BUILDS; + ASSERT_STATE(ApplicationState::Init); + setApplicationState(ApplicationState::RequestingBuilds); // TODO Show splash screen until this request is complete auto request = new QNetworkRequest(QUrl("https://thunder.highfidelity.com/builds/api/tags/latest/?format=json")); @@ -67,7 +125,7 @@ void LauncherState::requestBuilds() { void LauncherState::receivedBuildsReply() { auto reply = static_cast(sender()); - ASSERT_STATE(ApplicationState::REQUESTING_BUILDS); + ASSERT_STATE(ApplicationState::RequestingBuilds); if (reply->error()) { qDebug() << "Error getting builds from thunder: " << reply->errorString(); @@ -81,8 +139,7 @@ void LauncherState::receivedBuildsReply() { } else { auto root = doc.object(); if (!root.contains("default_tag")) { - _appState = ApplicationState::REQUESTING_BUILDS_FAILED; - setUIState(LauncherState::ERROR_SCREEN); + setApplicationState(ApplicationState::UnexpectedError); return; } @@ -90,8 +147,7 @@ void LauncherState::receivedBuildsReply() { auto results = root["results"]; if (!results.isArray()) { - _appState = ApplicationState::REQUESTING_BUILDS_FAILED; - setUIState(LauncherState::ERROR_SCREEN); + setApplicationState(ApplicationState::UnexpectedError); return; } @@ -112,14 +168,13 @@ void LauncherState::receivedBuildsReply() { } } } - _appState = ApplicationState::WAITING_FOR_LOGIN; - setUIState(LauncherState::LOGIN_SCREEN); + setApplicationState(ApplicationState::WaitingForLogin); } void LauncherState::login(QString username, QString password) { - ASSERT_STATE(ApplicationState::WAITING_FOR_LOGIN); + ASSERT_STATE(ApplicationState::WaitingForLogin); - _appState = ApplicationState::REQUESTING_LOGIN; + setApplicationState(ApplicationState::RequestingLogin); qDebug() << "Got login: " << username << password; @@ -133,32 +188,287 @@ void LauncherState::login(QString username, QString password) { query.addQueryItem("scope", "owner"); auto reply = _networkAccessManager.post(*request, query.toString().toUtf8()); - QObject::connect(reply, &QNetworkReply::finished, this, &LauncherState::receivedLoginReply); } Q_INVOKABLE void LauncherState::receivedLoginReply() { + ASSERT_STATE(ApplicationState::RequestingLogin); + + // TODO Check for errors auto reply = static_cast(sender()); - ASSERT_STATE(ApplicationState::REQUESTING_LOGIN); + if (reply->error()) { + setApplicationState(ApplicationState::UnexpectedError); + return; + } - qDebug() << "Got response for login: " << reply->readAll(); + auto data = reply->readAll(); + QJsonParseError parseError; + auto doc = QJsonDocument::fromJson(data, &parseError); + auto root = doc.object(); + if (!root.contains("access_token") + || !root.contains("token_type") + || !root.contains("expires_in") + || !root.contains("refresh_token") + || !root.contains("scope") + || !root.contains("created_at")) { - download(); + setApplicationState(ApplicationState::UnexpectedError); + return; + } + + _loginResponse.accessToken = root["access_token"].toString(); + _loginResponse.refreshToken = root["refresh_token"].toString(); + _loginResponse.tokenType = root["token_type"].toString(); + + qDebug() << "Got response for login: " << data; + + downloadClient(); } -void LauncherState::download() { - _appState = ApplicationState::DOWNLOADING_CONTENT; - setUIState(LauncherState::DOWNLOAD_SCREEN); +QString LauncherState::getContentCachePath() const { + return _launcherDirectory.filePath("cache"); } -void LauncherState::contentDownloadComplete() { +bool LauncherState::shouldDownloadContentCache() const { + return !_contentCacheURL.isNull() && !QFile::exists(getContentCachePath()); +} + +void LauncherState::downloadClient() { + ASSERT_STATE(ApplicationState::RequestingLogin); + + Build build; + if (!_latestBuilds.getBuild(_buildTag, &build)) { + qDebug() << "Cannot determine latest build"; + setApplicationState(ApplicationState::UnexpectedError); + return; + } + + _downloadProgress = 0; + setApplicationState(ApplicationState::DownloadingClient); + + // Start client download + { + qDebug() << "Latest build: " << build.tag << build.buildNumber << build.latestVersion << build.installerZipURL; + auto request = new QNetworkRequest(QUrl(build.installerZipURL)); + auto reply = _networkAccessManager.get(*request); + + _clientZipFile.setFileName(_launcherDirectory.absoluteFilePath("client.zip")); + + qDebug() << "Opening " << _clientZipFile.fileName(); + if (!_clientZipFile.open(QIODevice::WriteOnly)) { + setApplicationState(ApplicationState::UnexpectedError); + return; + } + + connect(reply, &QNetworkReply::finished, this, &LauncherState::clientDownloadComplete); + connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { + char buf[4096]; + while (reply->bytesAvailable() > 0) { + qint64 size; + size = reply->read(buf, (qint64)sizeof(buf)); + if (size == 0) { + break; + } + _clientZipFile.write(buf, size); + } + }); + connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total) { + _downloadProgress = (float)received / (float)total; + emit downloadProgressChanged(); + }); + } } void LauncherState::clientDownloadComplete() { + ASSERT_STATE(ApplicationState::DownloadingClient); + + _clientZipFile.close(); + + installClient(); +} + +void LauncherState::installClient() { + ASSERT_STATE(ApplicationState::DownloadingClient); + setApplicationState(ApplicationState::InstallingClient); + + auto installDir = _launcherDirectory.absoluteFilePath("interface_install"); + _launcherDirectory.mkpath("interface_install"); + + _downloadProgress = 0; + + qDebug() << "Unzipping " << _clientZipFile.fileName() << " to " << installDir; + + auto unzipper = new Unzipper(_clientZipFile.fileName(), QDir(installDir)); + unzipper->setAutoDelete(true); + connect(unzipper, &Unzipper::progress, this, [this](float progress) { + qDebug() << "Unzipper progress: " << progress; + _downloadProgress = progress; + emit downloadProgressChanged(); + }); + connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) { + if (error) { + qDebug() << "Unzipper finished with error: " << errorMessage; + setApplicationState(ApplicationState::UnexpectedError); + } else { + qDebug() << "Unzipper finished without error"; + downloadContentCache(); + } + }); + QThreadPool::globalInstance()->start(unzipper); + + //launchClient(); +} + +void LauncherState::downloadContentCache() { + ASSERT_STATE(ApplicationState::InstallingClient); + + // Start content set cache download + if (shouldDownloadContentCache()) { + setApplicationState(ApplicationState::DownloadingContentCache); + + _downloadProgress = 0; + + auto request = new QNetworkRequest(QUrl(_contentCacheURL)); + auto reply = _networkAccessManager.get(*request); + + _contentZipFile.setFileName(_launcherDirectory.absoluteFilePath("content_cache.zip")); + + qDebug() << "Opening " << _contentZipFile.fileName(); + if (!_contentZipFile.open(QIODevice::WriteOnly)) { + setApplicationState(ApplicationState::UnexpectedError); + return; + } + + connect(reply, &QNetworkReply::finished, this, &LauncherState::contentCacheDownloadComplete); + connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { + char buf[4096]; + while (reply->bytesAvailable() > 0) { + qint64 size; + size = reply->read(buf, (qint64)sizeof(buf)); + if (size == 0) { + break; + } + _contentZipFile.write(buf, size); + } + }); + connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64 total) { + _downloadProgress = (float)received / (float)total; + emit downloadProgressChanged(); + }); + } else { + launchClient(); + } +} + +void LauncherState::contentCacheDownloadComplete() { + ASSERT_STATE(ApplicationState::DownloadingContentCache); + + _contentZipFile.close(); + + installContentCache(); +} + + +void LauncherState::installContentCache() { + ASSERT_STATE(ApplicationState::DownloadingContentCache); + setApplicationState(ApplicationState::InstallingContentCache); + + auto installDir = getContentCachePath(); + + qDebug() << "Unzipping " << _contentZipFile.fileName() << " to " << installDir; + + _downloadProgress = 0; + + auto unzipper = new Unzipper(_contentZipFile.fileName(), QDir(installDir)); + unzipper->setAutoDelete(true); + connect(unzipper, &Unzipper::progress, this, [this](float progress) { + qDebug() << "Unzipper progress (content cache): " << progress; + _downloadProgress = progress; + emit downloadProgressChanged(); + }); + connect(unzipper, &Unzipper::finished, this, [this](bool error, QString errorMessage) { + if (error) { + qDebug() << "Unzipper finished with error: " << errorMessage; + setApplicationState(ApplicationState::UnexpectedError); + } else { + qDebug() << "Unzipper finished without error"; + launchClient(); + } + }); + QThreadPool::globalInstance()->start(unzipper); + } void LauncherState::launchClient() { - _appState = ApplicationState::LAUNCHING_HIGH_FIDELITY; + ASSERT_STATE({ ApplicationState::InstallingClient, ApplicationState::InstallingContentCache }); + + setApplicationState(ApplicationState::LaunchingHighFidelity); + + QDir installDirectory = _launcherDirectory.filePath("interface_install"); + auto clientPath = installDirectory.absoluteFilePath("interface.exe"); + + QString homePath = "hifi://hq"; + QString defaultScriptsPath = installDirectory.filePath("scripts/simplifiedUIBootstrapper"); + QString displayName = "fixMe"; + QString contentCachePath = _launcherDirectory.filePath("cache"); + + // TODO Fix parameters + QString params = "--url " + homePath + + " --setBookmark hqhome=\"" + homePath + "\"" + + " --defaultScriptsOverride " + QDir::toNativeSeparators(defaultScriptsPath) + + " --displayName " + displayName + + " --cache " + contentCachePath; + +#if defined(Q_OS_WIN) + STARTUPINFO si; + PROCESS_INFORMATION pi; + + // set the size of the structures + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + // start the program up + BOOL success = CreateProcess( + clientPath.toUtf8().data(), + params.toUtf8().data(), + nullptr, // Process handle not inheritable + nullptr, // Thread handle not inheritable + FALSE, // Set handle inheritance to FALSE + CREATE_NEW_CONSOLE, // Opens file in a separate console + nullptr, // Use parent's environment block + nullptr, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi // Pointer to PROCESS_INFORMATION structure + ); + // Close process and thread handles. + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + exit(0); +#elif defined(Q_OS_MACOS) + // TODO Implement launching of client +#else +#error UNSUPPORTED PLATFORM +#endif +} + +void LauncherState::setApplicationState(ApplicationState state) { + qDebug() << "Changing application state: " << _applicationState << " -> " << state; + + if (state == ApplicationState::UnexpectedError) { + __debugbreak(); + } + + _applicationState = state; + + emit uiStateChanged(); + emit updateSourceUrl(getCurrentUISource()); + + emit applicationStateChanged(); +} + +LauncherState::ApplicationState LauncherState::getApplicationState() const { + return _applicationState; } diff --git a/launchers/qt/src/LauncherState.h b/launchers/qt/src/LauncherState.h index 62a58c9778..156daaa165 100644 --- a/launchers/qt/src/LauncherState.h +++ b/launchers/qt/src/LauncherState.h @@ -1,6 +1,10 @@ +#pragma once + +#include #include #include #include +#include struct Build { QString tag; @@ -10,12 +14,24 @@ struct Build { }; struct LatestBuilds { + bool getBuild(QString tag, Build* outBuild); + QString defaultTag; std::vector builds; }; +struct LoginResponse { + QString accessToken; + QString tokenType; + QString refreshToken; +}; + class LauncherState : public QObject { Q_OBJECT + Q_PROPERTY(UIState uiState READ getUIState NOTIFY uiStateChanged); + Q_PROPERTY(ApplicationState applicationState READ getApplicationState NOTIFY applicationStateChanged); + + Q_PROPERTY(float downloadProgress READ getDownloadProgress NOTIFY downloadProgressChanged); public: LauncherState(); @@ -30,29 +46,27 @@ public: ERROR_SCREEN, UI_STATE_NUM }; - Q_ENUMS(UIState); + Q_ENUM(UIState); enum class ApplicationState { - INIT, + Init, - REQUESTING_BUILDS, - REQUESTING_BUILDS_FAILED, + UnexpectedError, - WAITING_FOR_LOGIN, - REQUESTING_LOGIN, + RequestingBuilds, - WAITING_FOR_SIGNUP, - REQUESTING_SIGNUP, + WaitingForLogin, + RequestingLogin, - DOWNLOADING_CONTENT, - DOWNLOADING_HIGH_FIDELITY, + DownloadingClient, + DownloadingContentCache, - EXTRACTING_DATA, + InstallingClient, + InstallingContentCache, - LAUNCHING_HIGH_FIDELITY + LaunchingHighFidelity }; - Q_ENUMS(ApplicationState); - + Q_ENUM(ApplicationState); enum LastLoginError { NONE = 0, @@ -60,19 +74,23 @@ public: CREDENTIALS, LAST_ERROR_NUM }; - Q_ENUMS(LastLoginError); + Q_ENUM(LastLoginError); + Q_INVOKABLE QString getCurrentUISource() const; - void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) const; + void ASSERT_STATE(LauncherState::ApplicationState state); + void ASSERT_STATE(std::vector states); static void declareQML(); - void setUIState(UIState state); UIState getUIState() const; void setLastLoginError(LastLoginError lastLoginError); LastLoginError getLastLoginError() const; + void setApplicationState(ApplicationState state); + ApplicationState getApplicationState() const; + // Request builds void requestBuilds(); Q_INVOKABLE void receivedBuildsReply(); @@ -81,22 +99,45 @@ public: Q_INVOKABLE void login(QString username, QString password); Q_INVOKABLE void receivedLoginReply(); - // Download - void download(); - Q_INVOKABLE void contentDownloadComplete(); - Q_INVOKABLE void clientDownloadComplete(); + // Client + void downloadClient(); + void installClient(); + + // Content Cache + void downloadContentCache(); + void installContentCache(); // Launching void launchClient(); + Q_INVOKABLE float getDownloadProgress() const { return _downloadProgress; } + signals: void updateSourceUrl(QString sourceUrl); + void uiStateChanged(); + void applicationStateChanged(); + void downloadProgressChanged(); + +private slots: + void clientDownloadComplete(); + void contentCacheDownloadComplete(); private: + bool shouldDownloadContentCache() const; + QString getContentCachePath() const; + QNetworkAccessManager _networkAccessManager; LatestBuilds _latestBuilds; + QDir _launcherDirectory; - ApplicationState _appState { ApplicationState::INIT }; - UIState _uiState { SPLASH_SCREEN }; + // Application State + ApplicationState _applicationState { ApplicationState::Init }; + LoginResponse _loginResponse; LastLoginError _lastLoginError { NONE }; + QString _buildTag { QString::null }; + QString _contentCacheURL{ "https://orgs.highfidelity.com/content-cache/content_cache_small-only_data8.zip" }; // QString::null }; // If null, there is no content cache to download + QFile _clientZipFile; + QFile _contentZipFile; + + float _downloadProgress { 0 }; }; diff --git a/launchers/qt/src/main.cpp b/launchers/qt/src/main.cpp index 9ed6f42390..93a0c0e7dc 100644 --- a/launchers/qt/src/main.cpp +++ b/launchers/qt/src/main.cpp @@ -10,7 +10,7 @@ Q_IMPORT_PLUGIN(QtQuickControls2Plugin); Q_IMPORT_PLUGIN(QtQuickTemplates2Plugin); int main(int argc, char *argv[]) { - QString name { "HQLauncher" }; + QString name { "High Fidelity" }; QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setOrganizationName(name);