diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 5c42f9145d7..dff725c9e69 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -108,6 +108,7 @@ set(viewer_SOURCE_FILES alstreaminfo.cpp altoolalign.cpp alunzip.cpp + alupdatemanager.cpp alviewermenu.cpp bdanimator.cpp bdfloaterposer.cpp @@ -854,6 +855,7 @@ set(viewer_HEADER_FILES alstreaminfo.h altoolalign.h alunzip.h + alupdatemanager.h alviewermenu.h bdanimator.h bdfloaterposer.h diff --git a/indra/newview/alupdatemanager.cpp b/indra/newview/alupdatemanager.cpp new file mode 100644 index 00000000000..7b8b27e491e --- /dev/null +++ b/indra/newview/alupdatemanager.cpp @@ -0,0 +1,256 @@ +/** + * @file alupdatemanager.h + * @brief Manager for updating! + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2024, Kyler "Felix" Eastridge. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * $/LicenseInfo$ + */ + +#include "alupdatemanager.h" +#include "llversioninfo.h" +#include "llviewercontrol.h" +#include "llcorehttputil.h" +#include "llsdserialize.h" +#include "llsdutil.h" +#include "llnotificationmanager.h" +#include "llnotifications.h" +#include "llnotificationsutil.h" +#include "llappviewer.h" // For gDisconnected +#include "llweb.h" +#include "llstartup.h" + +/* +Current LLSD data template: + + + + + channels + + Alchemy Test + + + build + 123456 + + + version + 1.2.3.45678 + + + download + Page to open in browser when user clicks "download" + + + message + Coyito was here! + + + + +*/ + +ALUpdateManager::ALUpdateManager() : + mChecking(false), + mSupressAuto(false), + mFirstCheck(true), + mUpdateAvailable(false), + mUpdateVersion(""), + mUpdateURL(""), + mAddMessage("") +{ +} + +ALUpdateManager::~ALUpdateManager() +{ +} + +void ALUpdateManager::showNotification() +{ + // If we already have one, don't grief the user. + if (mNotification && mNotification->isActive()) + return; + + LLSD args; + args["CHANNEL"] = LLVersionInfo::instance().getChannel(); + args["VERSION"] = mUpdateVersion; + args["MYVERSION"] = LLVersionInfo::instance().getVersion(); + args["URL"] = mUpdateURL; + args["MESSAGE"] = mAddMessage; + if (LLStartUp::getStartupState() < STATE_LOGIN_CLEANUP) + mNotification = LLNotificationsUtil::add("AlchemyUpdateAlert", args); + else + mNotification = LLNotificationsUtil::add("AlchemyUpdateToast", args); +} + +void ALUpdateManager::handleUpdateData(const LLSD& content) +{ + // Check if we have a channel in the list + std::string channel = LLVersionInfo::instance().getChannel(); + if (!content.has(channel)) + return; + + // Make sure we have a build number + if (!content[channel].has("build")) + return; + + // Make sure the build number is newer than the one we have + int build = LLVersionInfo::instance().getBuild(); + if (content[channel]["build"].asInteger() <= build) + return; + + //Make sure we have all the remaining fields + if (!content[channel].has("version")) + return; + + if (!content[channel].has("download")) + return; + + LL_INFOS("ALUpdateManager") << "Update detected! 🥳" << LL_ENDL; + + // And now only, and only if we got this far, update the variables + mUpdateVersion = content[channel]["version"].asString(); + mUpdateURL = content[channel]["download"].asString(); + + if (content[channel].has("message")) + mAddMessage = "\n\n" + content[channel]["message"].asString(); + else + mAddMessage = ""; + + // And mark that we have a update ready! + mUpdateAvailable = true; + + showNotification(); +} + +void ALUpdateManager::launchRequest() +{ + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t + httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("ALUpdateManager", httpPolicy)); + LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest); + LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions); + + // If we change the URL + httpOpts->setFollowRedirects(true); + + // Also try again if we fail the first time, just in case + httpOpts->setRetries(1); + + static LLCachedControl AlchemyUpdateURL(gSavedSettings, "AlchemyUpdateURL"); + LL_INFOS("ALUpdateManager") << "Launching update check to " << AlchemyUpdateURL() << LL_ENDL; + LLSD result = httpAdapter->getRawAndSuspend(httpRequest, AlchemyUpdateURL); + + + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + + // Mark we aren't checking anymore first and reset the timer, just in case + // we end up bailing due to HTTP failure + ALUpdateManager::getInstance()->mChecking = false; + ALUpdateManager::getInstance()->mLastChecked.reset(); + + if (!status) + { + LL_WARNS("ALUpdateManager") << "HTTP status, " << status.toTerseString() << + ". Update check failed." << LL_ENDL; + return; + } + + const LLSD::Binary &rawBody = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary(); + std::string body(rawBody.begin(), rawBody.end()); + std::istringstream is(body); + + LLPointer parser = new LLSDXMLParser(); + LLSD data; + if(parser->parse(is, data, body.size()) == LLSDParser::PARSE_FAILURE) + { + LL_WARNS("ALUpdateManager") << "Failed to parse update info: " << ll_stream_notation_sd(result) << LL_ENDL; + return; + } + + LL_INFOS("ALUpdateManager") << "Got response: " << ll_stream_notation_sd(data) << LL_ENDL; + + if (data.has("channels")) + ALUpdateManager::getInstance()->handleUpdateData(data["channels"]); + //Handle anything else here +} + +void ALUpdateManager::checkNow() +{ + // No need to check when we are disconnected + if(gDisconnected) + return; + + // Bail out if we are already checking + if (mChecking) + return; + + mChecking = true; + + LL_INFOS("ALUpdateManager") << "Checking for update..." << LL_ENDL; + LLCoros::instance().launch("ALUpdateManager::LaunchRequest", + boost::bind(ALUpdateManager::launchRequest)); +} + +void ALUpdateManager::tryAutoCheck() +{ + if (mSupressAuto) + return; + + // Don't check if the user has requested to disable it + static LLCachedControl AlchemyUpdateEnableAutoCheck(gSavedSettings, "AlchemyUpdateEnableAutoCheck"); + if (!AlchemyUpdateEnableAutoCheck) + return; + + static LLCachedControl AlchemyUpdateCheckFrequency(gSavedSettings, "AlchemyUpdateCheckFrequency"); + if ((mLastChecked.getElapsedTimeF32() < AlchemyUpdateCheckFrequency || AlchemyUpdateCheckFrequency == .0f) + && !mFirstCheck) + return; + + mFirstCheck = false; + + LL_INFOS("ALUpdateManager") << "Attempting automatic update check..." << LL_ENDL; + checkNow(); +} + +static bool AlchemyUpdateToastlert(const LLSD& notification, const LLSD& response) +{ + std::string option = LLNotification::getSelectedOptionName(response); + + if (option == "DOWNLOAD") + { + LLWeb::loadURLExternal(ALUpdateManager::getInstance()->mUpdateURL); + } + else if (option == "CLOSE") + { + // This is the remind me later option + // We don't really do anything here, instead we just wait for the next + // tryAutoCheck. + } + else if (option == "SUPPRESS") + { + ALUpdateManager::getInstance()->mSupressAuto = true; + } + return false; +} +static LLNotificationFunctorRegistration AlchemyUpdateAlert_reg("AlchemyUpdateAlert", AlchemyUpdateToastlert); +static LLNotificationFunctorRegistration AlchemyUpdateToast_reg("AlchemyUpdateToast", AlchemyUpdateToastlert); diff --git a/indra/newview/alupdatemanager.h b/indra/newview/alupdatemanager.h new file mode 100644 index 00000000000..e33670f875b --- /dev/null +++ b/indra/newview/alupdatemanager.h @@ -0,0 +1,58 @@ +/** + * @file alupdatemanager.h + * @brief Manager for updating! + * + * $LicenseInfo:firstyear=2024&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2024, Kyler "Felix" Eastridge. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * $/LicenseInfo$ + */ + +#ifndef LL_ALUPDATEMANAGER_H +#define LL_ALUPDATEMANAGER_H + +#include "llsingleton.h" +#include "llnotificationptr.h" + +class ALUpdateManager final : public LLSingleton +{ + LLSINGLETON(ALUpdateManager); +public: + ~ALUpdateManager(); + void showNotification(); + void handleUpdateData(const LLSD& content); + static void launchRequest(); + void checkNow(); + void tryAutoCheck(); + +// Portion used for checking updates + bool mFirstCheck; + bool mSupressAuto; + bool mChecking; + LLTimer mLastChecked; + +// Portion used if we have a update + bool mUpdateAvailable; + bool getUpdateAvailable(){ return mUpdateAvailable; } + std::string mUpdateVersion; + std::string mUpdateURL; + std::string mAddMessage; // Optional message regarding the update + LLNotificationPtr mNotification; +}; + +#endif // LL_ALUPDATEMANAGER_H diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index acf2676b966..4d6abbda4d6 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -1445,6 +1445,39 @@ Value 8.0 + AlchemyUpdateCheckFrequency + + Comment + How often to check for updates + Persist + 1 + Type + F32 + Value + 3600.0 + + AlchemyUpdateEnableAutoCheck + + Comment + Enable update notifications + Persist + 1 + Type + Boolean + Value + 1 + + AlchemyUpdateURL + + Comment + URL to pull update checks from + Persist + 1 + Type + String + Value + https://crocuta.softhyena.com/alupdate.xml + ResetUserColorsOnLogout Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index e8138b6b44b..9449efd3fff 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -270,6 +270,8 @@ using namespace LL; #include "alstreaminfo.h" +#include "alupdatemanager.h" + // *FIX: These extern globals should be cleaned up. // The globals either represent state/config/resource-storage of either // this app, or another 'component' of the viewer. App globals should be @@ -4645,6 +4647,9 @@ void LLAppViewer::idle() // here. LLIMProcessing::requestOfflineMessages(); + // Check if it is time to do a update check + ALUpdateManager::getInstance()->tryAutoCheck(); + /////////////////////////////////// // // Special case idle if still starting up diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index 8915a159f50..4548dd91a32 100644 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -90,6 +90,7 @@ #include "llrect.h" #include "llstring.h" #include "alunzip.h" +#include "alupdatemanager.h" // project includes @@ -346,6 +347,7 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key) mCommitCallbackRegistrar.add("Pref.getUIColor", boost::bind(&LLFloaterPreference::getUIColor, this ,_1, _2)); mCommitCallbackRegistrar.add("Pref.MaturitySettings", boost::bind(&LLFloaterPreference::onChangeMaturity, this)); mCommitCallbackRegistrar.add("Pref.BlockList", boost::bind(&LLFloaterPreference::onClickBlockList, this)); + mCommitCallbackRegistrar.add("Pref.UpdateCheckNow", boost::bind(&LLFloaterPreference::onClickUpdateCheckNow, this)); mCommitCallbackRegistrar.add("Pref.Proxy", boost::bind(&LLFloaterPreference::onClickProxySettings, this)); mCommitCallbackRegistrar.add("Pref.TranslationSettings", boost::bind(&LLFloaterPreference::onClickTranslationSettings, this)); mCommitCallbackRegistrar.add("Pref.AutoReplace", boost::bind(&LLFloaterPreference::onClickAutoReplace, this)); @@ -961,6 +963,16 @@ void LLFloaterPreference::draw() has_first_selected = (getChildRef("enabled_popups").getFirstSelected()!=NULL); gSavedSettings.setBOOL("FirstSelectedEnabledPopups", has_first_selected); + getChild("AlchemyUpdateCheckNow")->setEnabled(!ALUpdateManager::getInstance()->mChecking); + static LLCachedControl AlchemyUpdateEnableAutoCheck(gSavedSettings, "AlchemyUpdateEnableAutoCheck"); + getChild("AlchemyUpdateLastChecked")->setVisible(AlchemyUpdateEnableAutoCheck); + if (AlchemyUpdateEnableAutoCheck) + { + std::string lastCheck = LLTrans::getString("LastCheckedNSecondsAgo"); + LLStringUtil::format(lastCheck, LLSD().with("SECONDS", static_cast(ALUpdateManager::getInstance()->mLastChecked.getElapsedTimeF32()))); + getChild("AlchemyUpdateLastChecked")->setText(lastCheck); + } + LLFloater::draw(); } @@ -2119,6 +2131,11 @@ void LLFloaterPreference::onClickBlockList() // LLSD().with("people_panel_tab_name", "blocked_panel")); } +void LLFloaterPreference::onClickUpdateCheckNow() +{ + ALUpdateManager::getInstance()->checkNow(); +} + void LLFloaterPreference::onClickProxySettings() { LLFloaterReg::showInstance("prefs_proxy"); diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h index 248bc5002cf..7f4e98a3594 100644 --- a/indra/newview/llfloaterpreference.h +++ b/indra/newview/llfloaterpreference.h @@ -182,6 +182,7 @@ class LLFloaterPreference final : public LLFloater, public LLAvatarPropertiesObs void onChangeSoundFolder(); void onChangeAnimationFolder(); void onClickBlockList(); + void onClickUpdateCheckNow(); void onClickProxySettings(); void onClickTranslationSettings(); void onClickPermsDefault(); diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index eb112756f5d..3df169ee3d3 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -13207,6 +13207,51 @@ Always Run disabled. type="notifytip"> I’m sorry Dave, I’m afraid I can’t do that + + +A new version of [APP_NAME] is available for download! + +Latest: [CHANNEL] [VERSION] +You have: [CHANNEL] [MYVERSION][MESSAGE] +
+