diff --git a/launchers/qt/CMakeLists.txt b/launchers/qt/CMakeLists.txt index e2a8831bc0..3e23d2ee6a 100644 --- a/launchers/qt/CMakeLists.txt +++ b/launchers/qt/CMakeLists.txt @@ -140,6 +140,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 @@ -234,7 +236,6 @@ if (APPLE) ${OPENSSL_INCLUDE_DIR}) endif() - if (LAUNCHER_SOURCE_TREE_RESOURCES) target_compile_definitions(${PROJECT_NAME} PRIVATE RESOURCE_PREFIX_URL="${CMAKE_CURRENT_SOURCE_DIR}/resources/") target_compile_definitions(${PROJECT_NAME} PRIVATE HIFI_USE_LOCAL_FILE) diff --git a/launchers/qt/src/BuildsRequest.cpp b/launchers/qt/src/BuildsRequest.cpp new file mode 100644 index 0000000000..e693377dc7 --- /dev/null +++ b/launchers/qt/src/BuildsRequest.cpp @@ -0,0 +1,115 @@ +#include "BuildsRequest.h" + +#include "Helper.h" + +#include +#include +#include +#include +#include +#include + +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(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(); +} diff --git a/launchers/qt/src/BuildsRequest.h b/launchers/qt/src/BuildsRequest.h new file mode 100644 index 0000000000..865d375d2a --- /dev/null +++ b/launchers/qt/src/BuildsRequest.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +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 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; +}; diff --git a/launchers/qt/src/Helper.h b/launchers/qt/src/Helper.h index 8c685acc7c..a41b55b233 100644 --- a/launchers/qt/src/Helper.h +++ b/launchers/qt/src/Helper.h @@ -32,6 +32,9 @@ HRESULT createSymbolicLink(LPCSTR lpszPathObj, LPCSTR lpszPathLink, LPCSTR lpszD bool insertRegistryKey(const std::string& regPath, const std::string& name, const std::string& value); bool insertRegistryKey(const std::string& regPath, const std::string& name, DWORD value); bool deleteRegistryKey(const std::string& regPath); + +BOOL isProcessRunning(const char* processName, int& processID); +BOOL shutdownProcess(DWORD dwProcessId, UINT uExitCode); #endif QString getHTTPUserAgent(); diff --git a/launchers/qt/src/Helper_windows.cpp b/launchers/qt/src/Helper_windows.cpp index 84673e85b2..fda6a455f0 100644 --- a/launchers/qt/src/Helper_windows.cpp +++ b/launchers/qt/src/Helper_windows.cpp @@ -18,7 +18,8 @@ void launchClient(const QString& clientPath, const QString& homePath, const QStr + " --setBookmark \"hqhome=" + homePath + "\"" + " --defaultScriptsOverride \"file:///" + defaultScriptsPath + "\"" + " --displayName \"" + displayName + "\"" - + " --cache \"" + contentCachePath + "\""; + + " --cache \"" + contentCachePath + "\"" + + " --suppress-settings-reset --no-launcher --no-updater"; if (!loginResponseToken.isEmpty()) { params += " --tokens \"" + loginResponseToken.replace("\"", "\\\"") + "\""; @@ -147,3 +148,36 @@ bool deleteRegistryKey(const std::string& regPath) { } return false; } + + +BOOL isProcessRunning(const char* processName, int& processID) { + bool exists = false; + PROCESSENTRY32 entry; + entry.dwSize = sizeof(PROCESSENTRY32); + + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); + + if (Process32First(snapshot, &entry)) { + while (Process32Next(snapshot, &entry)) { + if (!_stricmp(entry.szExeFile, processName)) { + exists = true; + processID = entry.th32ProcessID; + break; + } + } + } + CloseHandle(snapshot); + return exists; +} + +BOOL shutdownProcess(DWORD dwProcessId, UINT uExitCode) { + DWORD dwDesiredAccess = PROCESS_TERMINATE; + BOOL bInheritHandle = FALSE; + HANDLE hProcess = OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId); + if (hProcess == NULL) { + return FALSE; + } + BOOL result = TerminateProcess(hProcess, uExitCode); + CloseHandle(hProcess); + return result; +} diff --git a/launchers/qt/src/LauncherState.cpp b/launchers/qt/src/LauncherState.cpp index 12ad27f4db..52f4c9f5ac 100644 --- a/launchers/qt/src/LauncherState.cpp +++ b/launchers/qt/src/LauncherState.cpp @@ -27,6 +27,11 @@ #include +//#define BREAK_ON_ERROR + +const QString configHomeLocationKey { "homeLocation" }; +const QString configLoggedInKey{ "loggedIn" }; +const QString configLauncherPathKey{ "launcherPath" }; QString LauncherState::getContentCachePath() const { return _launcherDirectory.filePath("cache"); @@ -45,23 +50,22 @@ QString LauncherState::getClientExecutablePath() const { #endif } -bool LauncherState::shouldDownloadContentCache() const { - return !_contentCacheURL.isEmpty() && !QFile::exists(getContentCachePath()); + +QString LauncherState::getConfigFilePath() const { + QDir clientDirectory = getClientDirectory(); + return clientDirectory.absoluteFilePath("config.json"); } -bool LatestBuilds::getBuild(QString tag, Build* outBuild) { - if (tag.isNull()) { - tag = defaultTag; - } +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 +} - for (auto& build : builds) { - if (build.tag == tag) { - *outBuild = build; - return true; - } - } - - return false; +bool LauncherState::shouldDownloadContentCache() const { + return !_contentCacheURL.isEmpty() && !QFile::exists(getContentCachePath()); } static const std::array QML_FILE_FOR_UI_STATE = @@ -70,7 +74,7 @@ static const std::array QML_FILE_ void LauncherState::ASSERT_STATE(LauncherState::ApplicationState state) { if (_applicationState != state) { -#ifdef Q_OS_WIN +#ifdef BREAK_ON_ERROR __debugbreak(); #endif setApplicationState(ApplicationState::UnexpectedError); @@ -84,8 +88,8 @@ void LauncherState::ASSERT_STATE(const std::vector(sender()); +void LauncherState::requestBuilds() { + ASSERT_STATE(ApplicationState::Init); + setApplicationState(ApplicationState::RequestingBuilds); - ASSERT_STATE(ApplicationState::RequestingBuilds); + auto request = new BuildsRequest(); - 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); - 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; + QObject::connect(request, &BuildsRequest::finished, this, [=] { + ASSERT_STATE(ApplicationState::RequestingBuilds); + if (request->getError() != BuildsRequest::Error::None) { + setApplicationStateError("Could not retrieve latest builds"); + return; } - } - if (shouldDownloadLauncher()) { - downloadLauncher(); - } - getCurrentClientVersion(); + _latestBuilds = request->getLatestBuilds(); + + if (shouldDownloadLauncher()) { + downloadLauncher(); + } + getCurrentClientVersion(); + }); + + request->send(_networkAccessManager); } - bool LauncherState::shouldDownloadLauncher() { return _latestBuilds.launcherBuild.latestVersion != atoi(LAUNCHER_BUILD_VERSION); } @@ -271,7 +212,32 @@ void LauncherState::getCurrentClientVersion() { } qDebug() << "Current client version is: " << _currentClientVersion; - setApplicationState(ApplicationState::WaitingForSignup); + { + 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(); + } + if (root.contains(configHomeLocationKey)) { + _config.homeLocation = root["homeLocation"].toString(); + } + } else { + qDebug() << "Failed to open config.json"; + } + } + + if (_config.loggedIn) { + downloadClient(); + } else { + setApplicationState(ApplicationState::WaitingForSignup); + } } @@ -337,6 +303,7 @@ void LauncherState::signup(QString email, QString username, QString password, QS } + _config.loggedIn = true; _loginResponse = loginRequest->getToken(); _loginTokenResponse = loginRequest->getRawToken(); @@ -372,6 +339,7 @@ void LauncherState::login(QString username, QString password, QString displayNam return; } + _config.loggedIn = true; _loginResponse = request->getToken(); _loginTokenResponse = request->getRawToken(); @@ -390,18 +358,17 @@ void LauncherState::requestSettings() { connect(request, &UserSettingsRequest::finished, this, [this, request]() { auto userSettings = request->getUserSettings(); if (userSettings.homeLocation.isEmpty()) { - qDebug() << "UserSettings is empty"; - _homeLocation = "hifi://hq"; + _config.homeLocation = "hifi://hq"; _contentCacheURL = ""; } else { - _homeLocation = userSettings.homeLocation; - auto host = QUrl(_homeLocation).host(); + _config.homeLocation = userSettings.homeLocation; + auto host = QUrl(_config.homeLocation).host(); _contentCacheURL = "http://orgs.highfidelity.com/host-content-cache/" + host + ".zip"; qDebug() << "Content cache url: " << _contentCacheURL; } - qDebug() << "Home location is: " << _homeLocation; + qDebug() << "Home location is: " << _config.homeLocation; qDebug() << "Content cache url is: " << _contentCacheURL; downloadClient(); @@ -685,7 +652,18 @@ 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({ + { configHomeLocationKey, _config.homeLocation }, + { configLoggedInKey, _config.loggedIn }, + { configLauncherPathKey, _config.launcherPath }, + }); + configFile.write(doc.toJson()); + } + QString defaultScriptsPath; #if defined(Q_OS_WIN) defaultScriptsPath = installDirectory.filePath("scripts/simplifiedUIBootstrapper.js"); @@ -695,7 +673,7 @@ void LauncherState::launchClient() { QString contentCachePath = _launcherDirectory.filePath("cache"); - ::launchClient(clientPath, _homeLocation, defaultScriptsPath, _displayName, contentCachePath, _loginTokenResponse); + ::launchClient(clientPath, _config.homeLocation, defaultScriptsPath, _displayName, contentCachePath, _loginTokenResponse); } void LauncherState::setApplicationStateError(QString errorMessage) { @@ -707,9 +685,9 @@ 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 +#endif } _applicationState = state; diff --git a/launchers/qt/src/LauncherState.h b/launchers/qt/src/LauncherState.h index 42bfb45d19..9b0d9b3961 100644 --- a/launchers/qt/src/LauncherState.h +++ b/launchers/qt/src/LauncherState.h @@ -10,20 +10,12 @@ #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 builds; - Build launcherBuild; +struct LauncherConfig { + QString launcherPath{ "" }; + bool loggedIn{ false }; + QString homeLocation{ "" }; }; class LauncherState : public QObject { @@ -110,7 +102,6 @@ public: // Request builds void requestBuilds(); - Q_INVOKABLE void receivedBuildsReply(); // Signup Q_INVOKABLE void signup(QString email, QString username, QString password, QString displayName); @@ -159,15 +150,19 @@ private: QString getContentCachePath() const; QString getClientDirectory() const; QString getClientExecutablePath() const; + QString getConfigFilePath() const; + QString getLauncherFilePath() const; float calculateDownloadProgress() const; bool shouldDownloadLauncher(); QNetworkAccessManager _networkAccessManager; - LatestBuilds _latestBuilds; + Builds _latestBuilds; QDir _launcherDirectory; + LauncherConfig _config; + // Application State ApplicationState _applicationState { ApplicationState::Init }; UIState _uiState { UIState::SPLASH_SCREEN }; @@ -180,7 +175,6 @@ private: QString _buildTag { QString::null }; QString _contentCacheURL; QString _loginTokenResponse; - QString _homeLocation; QFile _clientZipFile; QFile _launcherZipFile; QFile _contentZipFile; diff --git a/launchers/qt/src/main.cpp b/launchers/qt/src/main.cpp index c21e56cc24..0965a5742f 100644 --- a/launchers/qt/src/main.cpp +++ b/launchers/qt/src/main.cpp @@ -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; @@ -53,6 +51,12 @@ int main(int argc, char *argv[]) { launcherInstaller.uninstall(); return 0; } + + int interfacePID = -1; + if (isProcessRunning("interface.exe", interfacePID)) { + shutdownProcess(interfacePID, 0); + } + #endif QString name { "High Fidelity" }; QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);