From 46412a6bfcf232790b9a57931d3efafcbf758511 Mon Sep 17 00:00:00 2001 From: "Jonathan \"Geenz\" Goodman" Date: Tue, 7 Apr 2026 19:12:59 -0400 Subject: Release/26.1.1 (#5530) * Integrate Velopack installer and update framework * Add Velopack update support for macOS and VVM integration * Update Velopack version and dependencies * Improve Velopack packaging for macOS * #5346 Uninstall older non-velopack viewer (#5363) * #5335 Fix silent uninstall asking about registry * #5346 Uninstall older non-velopack viewer * Use runtime viewer exe name, handle Velopack URL * Velopack download failure diagnostic (#5520) * Velopack download failure diagnostic * Fix up velopack downloading updates. Handle updates internally then hand them off to velopack. (#5524) * More velopack changes. Should download updates properly now. * Don't include NSI files * Restore optional updates, refine viewer restart behavior. (#5527) * Add support for optional updates. * Don't restart the viewer after the update unless it was optional. * Setup UpdaterServiceSetting with velopack properly. * Refine the restart behavior a bit - readd the old "the viewer must update" UX. * If the update is still downloading, close should just reopen the downloading dialog. --------- Co-authored-by: Jonathan "Geenz" Goodman * Remove SLVersionChecker from the viewer with velopack. (#5528) * Remove SLVersionChecker updater integration * Ensure that the portable install has the correct version number. * Don't produce shortcuts with VPK - we do this with our post install. * Bump viewer version from 26.1.0 to 26.1.1 * Potential fix for uninstaller not being functional. * Fix for UpdaterServiceSetting being ignored. * Filter for release channel when generating shortcuts. * Add some more logging for icons on Windows builds. * More VPK logging. * Move velopack packaging in CI to the sign and package step. * Enable velopack downgrade and skip older updates * Move the version required checking into velopack's checks. * Potential fix for downgrade prompts. * Make sure our macOS flow mirrors Windows. * Make sure to use the dev version of the mac sign and package. * p#553 Only one of two uninstallers displayed * #5346 Don't force user to shutdown velopack build for NSIS uninstall * #5346 Ignore option for the uninstall dialog * #5346 Fix early exit crash * #5346 Properly reset version flag. * Add some autodetect logic on macOS. * p#564 Clear legacy links * p#553 Handle uninstall records * p#549 Permit testing release notes on a test build * p#564 Remake nsis to velopack update flow * p#564 Remake nsis to velopack update flow #2 * p#564 Fix incorrect value type * p#553 Clear velopack's own registry entry in favor of a custom one * #5346 Resolve duplicated window class name * Bump to 2.1.0 of sign and package. --------- Co-authored-by: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> --- indra/newview/llvelopack.cpp | 1201 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1201 insertions(+) create mode 100644 indra/newview/llvelopack.cpp (limited to 'indra/newview/llvelopack.cpp') diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp new file mode 100644 index 0000000000..28e989c4ba --- /dev/null +++ b/indra/newview/llvelopack.cpp @@ -0,0 +1,1201 @@ +/** + * @file llvelopack.cpp + * @brief Velopack installer and update framework integration + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * 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 + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#if LL_VELOPACK + +#include "llviewerprecompiledheaders.h" +#include "llvelopack.h" +#include "llstring.h" +#include "llcorehttputil.h" +#include "llversioninfo.h" + +#include +#include +#include +#include "llnotificationsutil.h" +#include "llviewercontrol.h" +#include "llappviewer.h" +#include "llcoros.h" + +#include "Velopack.h" + +#if LL_WINDOWS +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shell32.lib") +#endif // LL_WINDOWS + +// Common state +static std::string sUpdateUrl; +static std::function sProgressCallback; +static vpkc_update_manager_t* sUpdateManager = nullptr; +static vpkc_update_info_t* sPendingUpdate = nullptr; // Downloaded, ready to apply +static vpkc_update_info_t* sPendingCheckInfo = nullptr; // Checked, awaiting user response +static vpkc_update_source_t* sUpdateSource = nullptr; +static LLNotificationPtr sDownloadingNotification; +static bool sRestartAfterUpdate = false; +static bool sIsRequired = false; // Is the pending check a required update? +static std::string sReleaseNotesUrl; +static std::string sTargetVersion; // Velopack's actual target version + +// Forward declarations +static void show_required_update_prompt(); +static void show_downloading_notification(const std::string& version); +static void ensure_update_manager(bool allow_downgrade); +static void velopack_download_pending_update(); +static std::unordered_map sAssetUrlMap; // basename -> original absolute URL + +// +// Custom update source helpers +// + +static std::string extract_basename(const std::string& url) +{ + // Strip query params / fragment + std::string path = url; + auto qpos = path.find('?'); + if (qpos != std::string::npos) path = path.substr(0, qpos); + auto fpos = path.find('#'); + if (fpos != std::string::npos) path = path.substr(0, fpos); + + auto spos = path.rfind('/'); + if (spos != std::string::npos && spos + 1 < path.size()) + return path.substr(spos + 1); + return path; +} + +static void rewrite_asset_urls(boost::json::value& jv) +{ + if (jv.is_object()) + { + auto& obj = jv.as_object(); + auto it = obj.find("FileName"); + if (it != obj.end() && it->value().is_string()) + { + std::string filename(it->value().as_string()); + if (filename.find("://") != std::string::npos) + { + std::string basename = extract_basename(filename); + sAssetUrlMap[basename] = filename; + it->value() = basename; + LL_DEBUGS("Velopack") << "Rewrote FileName: " << basename << LL_ENDL; + } + } + for (auto& kv : obj) + { + rewrite_asset_urls(kv.value()); + } + } + else if (jv.is_array()) + { + for (auto& elem : jv.as_array()) + { + rewrite_asset_urls(elem); + } + } +} + +static std::string rewrite_release_feed(const std::string& json_str) +{ + boost::json::value jv = boost::json::parse(json_str); + rewrite_asset_urls(jv); + return boost::json::serialize(jv); +} + +static std::string download_url_raw(const std::string& url) +{ + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + auto httpAdapter = std::make_shared("VelopackSource", httpPolicy); + auto httpRequest = std::make_shared(); + auto httpOpts = std::make_shared(); + httpOpts->setFollowRedirects(true); + + LLSD result = httpAdapter->getRawAndSuspend(httpRequest, url, httpOpts); + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + if (!status) + { + LL_WARNS("Velopack") << "HTTP request failed for " << url << ": " << status.toString() << LL_ENDL; + return {}; + } + + const LLSD::Binary& rawBody = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary(); + return std::string(rawBody.begin(), rawBody.end()); +} + +static bool download_url_to_file(const std::string& url, const std::string& local_path) +{ + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + auto httpAdapter = std::make_shared("VelopackDownload", httpPolicy); + auto httpRequest = std::make_shared(); + auto httpOpts = std::make_shared(); + httpOpts->setFollowRedirects(true); + httpOpts->setTransferTimeout(1200); + + LLSD result = httpAdapter->getRawAndSuspend(httpRequest, url, httpOpts); + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + if (!status) + { + LL_WARNS("Velopack") << "Download failed for " << url << ": " << status.toString() << LL_ENDL; + return false; + } + + const LLSD::Binary& rawBody = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary(); + llofstream outFile(local_path, std::ios::binary | std::ios::trunc); + if (!outFile.is_open()) + { + LL_WARNS("Velopack") << "Failed to open file for writing: " << local_path << LL_ENDL; + return false; + } + outFile.write(reinterpret_cast(rawBody.data()), rawBody.size()); + outFile.close(); + return true; +} + +// +// Custom source callbacks +// + +static char* custom_get_release_feed(void* user_data, const char* releases_name) +{ + std::string base = sUpdateUrl; + if (!base.empty() && base.back() == '/') + base.pop_back(); + std::string url = base + "/" + releases_name; + LL_INFOS("Velopack") << "Fetching release feed: " << url << LL_ENDL; + + std::string json_str = download_url_raw(url); + if (json_str.empty()) + { + return nullptr; + } + + try + { + std::string rewritten = rewrite_release_feed(json_str); + char* result = static_cast(malloc(rewritten.size() + 1)); + if (result) + { + memcpy(result, rewritten.c_str(), rewritten.size() + 1); + } + return result; + } + catch (const std::exception& e) + { + LL_WARNS("Velopack") << "Failed to parse/rewrite release feed: " << e.what() << LL_ENDL; + // Return original unmodified feed as fallback + char* result = static_cast(malloc(json_str.size() + 1)); + if (result) + { + memcpy(result, json_str.c_str(), json_str.size() + 1); + } + return result; + } +} + +static void custom_free_release_feed(void* user_data, char* feed) +{ + free(feed); +} + +static std::string sPreDownloadedAssetPath; + +static bool custom_download_asset(void* user_data, + const vpkc_asset_t* asset, + const char* local_path, + size_t progress_callback_id) +{ + // The asset has already been downloaded at the coroutine level (before vpkc_download_updates). + // This callback just copies the pre-downloaded file to where Velopack expects it. + // We cannot use getRawAndSuspend here — coroutine context is lost through the Rust FFI boundary. + if (sPreDownloadedAssetPath.empty()) + { + LL_WARNS("Velopack") << "No pre-downloaded asset available" << LL_ENDL; + return false; + } + + std::string filename = asset->FileName ? asset->FileName : ""; + LL_INFOS("Velopack") << "Download asset callback: filename=" << filename + << " local_path=" << local_path + << " size=" << asset->Size << LL_ENDL; + vpkc_source_report_progress(progress_callback_id, 0); + + std::ifstream src(sPreDownloadedAssetPath, std::ios::binary); + llofstream dst(local_path, std::ios::binary | std::ios::trunc); + if (!src.is_open() || !dst.is_open()) + { + LL_WARNS("Velopack") << "Failed to open files for copy" << LL_ENDL; + return false; + } + + dst << src.rdbuf(); + dst.close(); + src.close(); + + vpkc_source_report_progress(progress_callback_id, 100); + LL_INFOS("Velopack") << "Asset copy complete" << LL_ENDL; + return true; +} + +// +// Platform-specific helpers and hooks +// + +#if LL_WINDOWS + +static const wchar_t* PROTOCOL_SECONDLIFE = L"secondlife"; +static const wchar_t* PROTOCOL_GRID_INFO = L"x-grid-location-info"; +static std::wstring get_viewer_exe_name() +{ + return ll_convert(gDirUtilp->getExecutableFilename()); +} + +static std::wstring get_install_dir() +{ + wchar_t path[MAX_PATH]; + GetModuleFileNameW(NULL, path, MAX_PATH); + PathRemoveFileSpecW(path); + return path; +} + +static std::wstring get_app_name() +{ + // Match viewer_manifest.py app_name() logic: release channel uses "Viewer" + // suffix instead of "Release" for display purposes (shortcuts, uninstall, etc.) + std::wstring channel = LL_TO_WSTRING(LL_VIEWER_CHANNEL); + std::wstring release_suffix = L" Release"; + if (channel.size() >= release_suffix.size() && + channel.compare(channel.size() - release_suffix.size(), release_suffix.size(), release_suffix) == 0) + { + channel.replace(channel.size() - release_suffix.size(), release_suffix.size(), L" Viewer"); + } + return channel; +} + +static std::wstring get_app_name_oneword() +{ + std::wstring name = get_app_name(); + name.erase(std::remove(name.begin(), name.end(), L' '), name.end()); + return name; +} + +static std::wstring get_start_menu_path() +{ + wchar_t path[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_PROGRAMS, NULL, 0, path))) + { + return path; + } + return L""; +} + +static std::wstring get_desktop_path() +{ + wchar_t path[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_DESKTOPDIRECTORY, NULL, 0, path))) + { + return path; + } + return L""; +} + +static bool create_shortcut(const std::wstring& shortcut_path, + const std::wstring& target_path, + const std::wstring& arguments, + const std::wstring& description, + const std::wstring& icon_path) +{ + HRESULT hr = CoInitialize(NULL); + if (FAILED(hr)) return false; + + IShellLinkW* shell_link = nullptr; + hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, (void**)&shell_link); + if (SUCCEEDED(hr)) + { + shell_link->SetPath(target_path.c_str()); + shell_link->SetArguments(arguments.c_str()); + shell_link->SetDescription(description.c_str()); + shell_link->SetIconLocation(icon_path.c_str(), 0); + + wchar_t work_dir[MAX_PATH]; + wcscpy_s(work_dir, target_path.c_str()); + PathRemoveFileSpecW(work_dir); + shell_link->SetWorkingDirectory(work_dir); + + IPersistFile* persist_file = nullptr; + hr = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); + if (SUCCEEDED(hr)) + { + hr = persist_file->Save(shortcut_path.c_str(), TRUE); + persist_file->Release(); + } + shell_link->Release(); + } + + CoUninitialize(); + return SUCCEEDED(hr); +} + +static void register_protocol_handler(const std::wstring& protocol, + const std::wstring& description, + const std::wstring& exe_path) +{ + std::wstring key_path = L"SOFTWARE\\Classes\\" + protocol; + HKEY hkey; + + if (RegCreateKeyExW(HKEY_CURRENT_USER, key_path.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS) + { + RegSetValueExW(hkey, NULL, 0, REG_SZ, + (BYTE*)description.c_str(), (DWORD)((description.size() + 1) * sizeof(wchar_t))); + RegSetValueExW(hkey, L"URL Protocol", 0, REG_SZ, (BYTE*)L"", sizeof(wchar_t)); + RegCloseKey(hkey); + } + + std::wstring icon_key_path = key_path + L"\\DefaultIcon"; + if (RegCreateKeyExW(HKEY_CURRENT_USER, icon_key_path.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS) + { + std::wstring icon_value = L"\"" + exe_path + L"\""; + RegSetValueExW(hkey, NULL, 0, REG_SZ, + (BYTE*)icon_value.c_str(), (DWORD)((icon_value.size() + 1) * sizeof(wchar_t))); + RegCloseKey(hkey); + } + + std::wstring cmd_key_path = key_path + L"\\shell\\open\\command"; + if (RegCreateKeyExW(HKEY_CURRENT_USER, cmd_key_path.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS) + { + std::wstring cmd_value = L"\"" + exe_path + L"\" -url \"%1\""; + RegSetValueExW(hkey, NULL, 0, REG_EXPAND_SZ, + (BYTE*)cmd_value.c_str(), (DWORD)((cmd_value.size() + 1) * sizeof(wchar_t))); + RegCloseKey(hkey); + } +} + +void clear_nsis_links() +{ + wchar_t path[MAX_PATH]; + + // 1. The 'start' shortcuts set by nsis would be global, like app shortcut: + // C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Second Life Viewer\Second Life Viewer.lnk + // But it isn't just one link, it's a whole directory that needs to be removed. + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, path))) + { + std::wstring start_menu_path = path; + std::wstring folder_path = start_menu_path + L"\\" + get_app_name(); + + std::error_code ec; + std::filesystem::path dir(folder_path); + if (std::filesystem::exists(dir, ec)) + { + std::filesystem::remove_all(dir, ec); + if (ec) + { + LL_WARNS("Velopack") << "Failed to remove NSIS start menu directory: " + << ll_convert_wide_to_string(folder_path) << LL_ENDL; + } + } + } + + // 2. Desktop link, also a global one. + // C:\Users\Public\Desktop + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_DESKTOPDIRECTORY, NULL, 0, path))) + { + std::wstring desktop_path = path; + std::wstring shortcut_path = desktop_path + L"\\" + get_app_name() + L".lnk"; + if (!DeleteFileW(shortcut_path.c_str())) + { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND) + { + LL_WARNS("Velopack") << "Failed to delete NSIS desktop shortcut: " + << ll_convert_wide_to_string(shortcut_path) + << " (error: " << error << ")" << LL_ENDL; + } + } + } +} + +static void parse_version(const wchar_t* version_str, int& major, int& minor, int& patch, uint64_t& build) +{ + major = minor = patch = 0; + build = 0; + if (!version_str) return; + // Use swscanf for wide strings + swscanf(version_str, L"%d.%d.%d.%llu", &major, &minor, &patch, &build); +} + +bool get_nsis_version( + int& nsis_major, + int& nsis_minor, + int& nsis_patch, + uint64_t& nsis_build) +{ + // Test for presence of NSIS viewer registration, then + // attempt to read uninstall info + std::wstring app_name_oneword = get_app_name_oneword(); + std::wstring uninstall_key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword; + HKEY hkey; + LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, uninstall_key_path.c_str(), 0, KEY_READ, &hkey); + if (result != ERROR_SUCCESS) + { + return false; + } + + // Read DisplayVersion + wchar_t version_buf[64] = { 0 }; + DWORD version_buf_size = sizeof(version_buf); + DWORD type = 0; + LONG ver_rv = RegGetValueW(hkey, nullptr, L"DisplayVersion", RRF_RT_REG_SZ, &type, version_buf, &version_buf_size); + + if (ver_rv != ERROR_SUCCESS) + { + RegCloseKey(hkey); + return false; + } + + parse_version(version_buf, nsis_major, nsis_minor, nsis_patch, nsis_build); + + // Make sure it actually exists and not a dead entry. + wchar_t path_buffer[MAX_PATH] = { 0 }; + DWORD path_buf_size = sizeof(path_buffer); + LONG rv = RegGetValueW(hkey, nullptr, L"UninstallString", RRF_RT_REG_SZ, &type, path_buffer, &path_buf_size); + RegCloseKey(hkey); + if (rv != ERROR_SUCCESS) + { + return false; + } + size_t len = wcslen(path_buffer); + if (len > 0) + { + if (path_buffer[0] == L'\"') + { + // Likely to contain leading " + memmove(path_buffer, path_buffer + 1, len * sizeof(wchar_t)); + } + wchar_t* pos = wcsstr(path_buffer, L"uninst.exe"); + if (pos) + { + // Likely to contain trailing " + pos[wcslen(L"uninst.exe")] = L'\0'; + } + } + std::error_code ec; + std::filesystem::path path(path_buffer); + if (!std::filesystem::exists(path, ec)) + { + return false; + } + + // Todo: check codesigning? + + return true; +} + +static void unregister_protocol_handler(const std::wstring& protocol) +{ + std::wstring key_path = L"SOFTWARE\\Classes\\" + protocol; + RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str()); +} + +static void register_uninstall_info(const std::wstring& install_dir, + const std::wstring& app_name, + const std::wstring& version) +{ + std::wstring app_name_oneword = get_app_name_oneword(); + // Clears velopack's recently created 'uninstall' registry entry. + // We are going to use a custom one. + // Note that velopack doesn't know about our custom entry. + std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + app_name_oneword; + RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str()); + // Use a unique key name to avoid conflicts with any existing NSIS-based uninstall info, + // which can cause only one of the two entries to show up in the Add/Remove Programs list. + // The UI will show DisplayName, so the key name itself is not important to be user-friendly. + key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Vlpk" + app_name_oneword; + HKEY hkey; + + if (RegCreateKeyExW(HKEY_CURRENT_USER, key_path.c_str(), 0, NULL, + REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hkey, NULL) == ERROR_SUCCESS) + { + std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name(); + // Update.exe lives one level above the current\ directory where the viewer exe runs + std::filesystem::path update_exe = std::filesystem::path(install_dir).parent_path() / L"Update.exe"; + std::wstring uninstall_cmd = L"\"" + update_exe.wstring() + L"\" --uninstall"; + + RegSetValueExW(hkey, L"DisplayName", 0, REG_SZ, + (BYTE*)app_name.c_str(), (DWORD)((app_name.size() + 1) * sizeof(wchar_t))); + RegSetValueExW(hkey, L"DisplayVersion", 0, REG_SZ, + (BYTE*)version.c_str(), (DWORD)((version.size() + 1) * sizeof(wchar_t))); + RegSetValueExW(hkey, L"Publisher", 0, REG_SZ, + (BYTE*)L"Linden Research, Inc.", 44); + RegSetValueExW(hkey, L"UninstallString", 0, REG_SZ, + (BYTE*)uninstall_cmd.c_str(), (DWORD)((uninstall_cmd.size() + 1) * sizeof(wchar_t))); + RegSetValueExW(hkey, L"DisplayIcon", 0, REG_SZ, + (BYTE*)exe_path.c_str(), (DWORD)((exe_path.size() + 1) * sizeof(wchar_t))); + + std::wstring link_url = L"https://support.secondlife.com/contact-support/"; + RegSetValueExW(hkey, L"HelpLink", 0, REG_SZ, + (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t))); + + link_url = L"https://secondlife.com/whatis/"; + RegSetValueExW(hkey, L"URLInfoAbout", 0, REG_SZ, + (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t))); + + link_url = L"https://secondlife.com/support/downloads/"; + RegSetValueExW(hkey, L"URLUpdateInfo", 0, REG_SZ, + (BYTE*)link_url.c_str(), (DWORD)((link_url.size() + 1) * sizeof(wchar_t))); + + DWORD no_modify = 1; + RegSetValueExW(hkey, L"NoModify", 0, REG_DWORD, (BYTE*)&no_modify, sizeof(DWORD)); + RegSetValueExW(hkey, L"NoRepair", 0, REG_DWORD, (BYTE*)&no_modify, sizeof(DWORD)); + + // Format YYYYMMDD + wchar_t dateStr[9]; + time_t t = time(NULL); + struct tm tm; + localtime_s(&tm, &t); + wcsftime(dateStr, 9, L"%Y%m%d", &tm); + RegSetValueExW(hkey, L"InstallDate", 0, REG_SZ, (BYTE*)dateStr, (DWORD)((wcslen(dateStr) + 1) * sizeof(wchar_t))); // Let Windows fill in the install date + + // 800 MB, inaccurate, but for a rough idea. + // We can check folder size here, but it would take time and + // information is of low importance. + DWORD estimated_size = 800000; + RegSetValueExW(hkey, L"EstimatedSize", 0, REG_DWORD, (BYTE*)&estimated_size, sizeof(DWORD)); + + RegCloseKey(hkey); + } +} + +static void unregister_uninstall_info() +{ + std::wstring app_name_oneword = get_app_name_oneword(); + std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Vlpk" + app_name_oneword; + RegDeleteTreeW(HKEY_CURRENT_USER, key_path.c_str()); +} + +static void create_shortcuts(const std::wstring& install_dir, const std::wstring& app_name) +{ + std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name(); + std::wstring start_menu_dir = get_start_menu_path() + L"\\" + app_name; + std::wstring desktop_path = get_desktop_path(); + + CreateDirectoryW(start_menu_dir.c_str(), NULL); + + create_shortcut(start_menu_dir + L"\\" + app_name + L".lnk", + exe_path, L"", app_name, exe_path); + + create_shortcut(desktop_path + L"\\" + app_name + L".lnk", + exe_path, L"", app_name, exe_path); +} + +static void remove_shortcuts(const std::wstring& app_name) +{ + std::wstring start_menu_dir = get_start_menu_path() + L"\\" + app_name; + std::wstring desktop_path = get_desktop_path(); + + DeleteFileW((start_menu_dir + L"\\" + app_name + L".lnk").c_str()); + RemoveDirectoryW(start_menu_dir.c_str()); + DeleteFileW((desktop_path + L"\\" + app_name + L".lnk").c_str()); +} + +static void on_first_run(void* p_user_data, const char* app_version) +{ + // Velopack first executes 'after install' hook, then writes registry, + // then executes 'on first run' hook. + // As we need to clear velopack's 'uninstall' registry entry and use + // our own, clean it here instead of on_after_install. + + std::wstring install_dir = get_install_dir(); + std::wstring app_name = get_app_name(); + + int len = MultiByteToWideChar(CP_UTF8, 0, app_version, -1, NULL, 0); + std::wstring version(len, 0); + MultiByteToWideChar(CP_UTF8, 0, app_version, -1, &version[0], len); + + register_uninstall_info(install_dir, app_name, version); +} + +static void on_after_install(void* user_data, const char* app_version) +{ + std::wstring install_dir = get_install_dir(); + std::wstring app_name = get_app_name(); + std::wstring exe_path = install_dir + L"\\" + get_viewer_exe_name(); + + register_protocol_handler(PROTOCOL_SECONDLIFE, L"URL:Second Life", exe_path); + register_protocol_handler(PROTOCOL_GRID_INFO, L"URL:Second Life", exe_path); + create_shortcuts(install_dir, app_name); +} + +static void on_before_uninstall(void* user_data, const char* app_version) +{ + std::wstring app_name = get_app_name(); + + unregister_protocol_handler(PROTOCOL_SECONDLIFE); + unregister_protocol_handler(PROTOCOL_GRID_INFO); + unregister_uninstall_info(); + remove_shortcuts(app_name); +} + +static void on_log_message(void* user_data, const char* level, const char* message) +{ + OutputDebugStringA("[Velopack] "); + OutputDebugStringA(level); + OutputDebugStringA(": "); + OutputDebugStringA(message); + OutputDebugStringA("\n"); +} + +#elif LL_DARWIN + +// macOS-specific hooks +// TODO: Implement protocol handler registration via Launch Services +// TODO: Implement app bundle management + +static void on_first_run(void* user_data, const char* app_version) +{ +} + +static void on_after_install(void* user_data, const char* app_version) +{ + // macOS handles protocol registration via Info.plist CFBundleURLTypes + // No additional registration needed at runtime + LL_INFOS("Velopack") << "macOS post-install hook called for version: " << app_version << LL_ENDL; +} + +static void on_before_uninstall(void* user_data, const char* app_version) +{ + LL_INFOS("Velopack") << "macOS pre-uninstall hook called for version: " << app_version << LL_ENDL; +} + +static void on_log_message(void* user_data, const char* level, const char* message) +{ + LL_INFOS("Velopack") << "[" << level << "] " << message << LL_ENDL; +} + +#endif // LL_WINDOWS / LL_DARWIN + +// +// Common progress callback +// + +static void on_progress(void* user_data, size_t progress) +{ + if (sProgressCallback) + { + sProgressCallback(static_cast(progress)); + } +} + +static void on_vpk_log(void* p_user_data, + const char* psz_level, + const char* psz_message) +{ + LL_DEBUGS("Velopack") << ll_safe_string(psz_message) << LL_ENDL; +} + +// +// Version comparison helper +// + +// Compare running version against a VVM version string "major.minor.patch.build". +// Returns -1 if running < vvm, 0 if equal, 1 if running > vvm. +static int compare_running_version(const std::string& vvm_version) +{ + S32 major = 0, minor = 0, patch = 0; + U64 build = 0; + sscanf(vvm_version.c_str(), "%d.%d.%d.%llu", &major, &minor, &patch, &build); + + const LLVersionInfo& vi = LLVersionInfo::instance(); + S32 cur_major = vi.getMajor(); + S32 cur_minor = vi.getMinor(); + S32 cur_patch = vi.getPatch(); + U64 cur_build = vi.getBuild(); + + if (cur_major != major) return cur_major < major ? -1 : 1; + if (cur_minor != minor) return cur_minor < minor ? -1 : 1; + if (cur_patch != patch) return cur_patch < patch ? -1 : 1; + if (cur_build != build) return cur_build < build ? -1 : 1; + return 0; +} + +// +// Update manager lifecycle +// + +static void ensure_update_manager(bool allow_downgrade) +{ + if (sUpdateManager) + return; + + vpkc_update_options_t options = {}; + options.AllowVersionDowngrade = allow_downgrade; + options.ExplicitChannel = nullptr; + + if (!sUpdateSource) + { + sUpdateSource = vpkc_new_source_custom_callback( + custom_get_release_feed, + custom_free_release_feed, + custom_download_asset, + nullptr); + } + + vpkc_locator_config_t* locator_ptr = nullptr; + +#if LL_DARWIN + // Try auto-detection first (works when the app bundle was packaged by vpk + // and has UpdateMac + sq.version already present) + if (!vpkc_new_update_manager_with_source(sUpdateSource, &options, nullptr, &sUpdateManager)) + { + char err[512]; + vpkc_get_last_error(err, sizeof(err)); + LL_INFOS("Velopack") << "Auto-detect failed (" << ll_safe_string(err) + << "), falling back to explicit locator" << LL_ENDL; + + // Auto-detection failed — construct an explicit locator. + // This handles legacy DMG installs that don't have Velopack's + // install state (UpdateMac, sq.version) in the bundle. + vpkc_locator_config_t locator = {}; + + // The executable lives at /Contents/MacOS/ + // The app bundle root is two levels up from the executable directory. + std::string exe_dir = gDirUtilp->getExecutableDir(); + std::string bundle_root = exe_dir + "/../.."; + char resolved[PATH_MAX]; + if (realpath(bundle_root.c_str(), resolved)) + { + bundle_root = resolved; + } + + // Construct a version string in Velopack SemVer format: major.minor.patch-build + const LLVersionInfo& vi = LLVersionInfo::instance(); + std::string current_version = llformat("%d.%d.%d-%llu", + vi.getMajor(), vi.getMinor(), vi.getPatch(), vi.getBuild()); + + // Create a minimal sq.version manifest so Velopack knows our version. + // Proper vpk-packaged builds have this in the bundle already. + std::string manifest_path = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, "sq.version"); + { + std::string app_name = LLVersionInfo::instance().getChannel(); + std::string pack_id = app_name; + pack_id.erase(std::remove(pack_id.begin(), pack_id.end(), ' '), pack_id.end()); + + std::string nuspec = "\n" + "\n" + " \n" + " " + pack_id + "\n" + " " + current_version + "\n" + " " + app_name + "\n" + " \n" + "\n"; + + llofstream manifest_file(manifest_path, std::ios::trunc); + if (manifest_file.is_open()) + { + manifest_file << nuspec; + manifest_file.close(); + } + } + + std::string packages_dir = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, "velopack-packages"); + LLFile::mkdir(packages_dir); + + locator.RootAppDir = const_cast(bundle_root.c_str()); + locator.CurrentBinaryDir = const_cast(exe_dir.c_str()); + locator.ManifestPath = const_cast(manifest_path.c_str()); + locator.PackagesDir = const_cast(packages_dir.c_str()); + locator.UpdateExePath = nullptr; + locator.IsPortable = false; + + locator_ptr = &locator; + + LL_INFOS("Velopack") << "Explicit locator: RootAppDir=" << bundle_root + << " CurrentBinaryDir=" << exe_dir + << " Version=" << current_version << LL_ENDL; + + if (!vpkc_new_update_manager_with_source(sUpdateSource, &options, locator_ptr, &sUpdateManager)) + { + char err2[512]; + vpkc_get_last_error(err2, sizeof(err2)); + LL_WARNS("Velopack") << "Failed to create update manager: " << ll_safe_string(err2) << LL_ENDL; + } + } + return; +#endif + + // Windows: Velopack auto-detection works because the viewer is installed + // by Velopack's Setup.exe which creates the proper install structure. + if (!vpkc_new_update_manager_with_source(sUpdateSource, &options, nullptr, &sUpdateManager)) + { + char err[512]; + vpkc_get_last_error(err, sizeof(err)); + LL_WARNS("Velopack") << "Failed to create update manager: " << ll_safe_string(err) << LL_ENDL; + } +} + +// +// Public API - Cross-platform +// + +bool velopack_initialize() +{ + vpkc_set_logger(on_log_message, nullptr); + vpkc_app_set_auto_apply_on_startup(false); + +#if LL_WINDOWS || LL_DARWIN + vpkc_app_set_hook_first_run(on_first_run); + vpkc_app_set_hook_after_install(on_after_install); + vpkc_app_set_hook_before_uninstall(on_before_uninstall); +#endif + + vpkc_app_run(nullptr); + return true; +} + +// Downloads the update that was found during the check phase. +// Operates on sPendingCheckInfo which was set by velopack_check_for_updates. +static void velopack_download_pending_update() +{ + if (!sUpdateManager || !sPendingCheckInfo) + { + LL_WARNS("Velopack") << "No pending check info to download" << LL_ENDL; + return; + } + + LL_DEBUGS("Velopack") << "Setting up detailed logging"; + vpkc_set_logger(on_vpk_log, nullptr); + LL_CONT << LL_ENDL; + LL_INFOS("Velopack") << "Downloading update..." << LL_ENDL; + + // Pre-download the nupkg at the coroutine level where getRawAndSuspend works. + // The download callback inside the Rust FFI cannot use coroutine HTTP. + std::string asset_filename = sPendingCheckInfo->TargetFullRelease->FileName + ? sPendingCheckInfo->TargetFullRelease->FileName : ""; + std::string asset_url; + auto url_it = sAssetUrlMap.find(asset_filename); + if (url_it != sAssetUrlMap.end()) + { + asset_url = url_it->second; + } + else + { + std::string base = sUpdateUrl; + if (!base.empty() && base.back() == '/') + base.pop_back(); + asset_url = base + "/" + asset_filename; + } + + sPreDownloadedAssetPath = gDirUtilp->getExpandedFilename(LL_PATH_TEMP, asset_filename); + LL_INFOS("Velopack") << "Pre-downloading " << asset_url + << " to " << sPreDownloadedAssetPath << LL_ENDL; + + if (!download_url_to_file(asset_url, sPreDownloadedAssetPath)) + { + LL_WARNS("Velopack") << "Failed to pre-download update asset" << LL_ENDL; + sPreDownloadedAssetPath.clear(); + return; + } + + LL_INFOS("Velopack") << "Pre-download complete, handing to Velopack" << LL_ENDL; + if (vpkc_download_updates(sUpdateManager, sPendingCheckInfo, on_progress, nullptr)) + { + if (sPendingUpdate) + { + vpkc_free_update_info(sPendingUpdate); + } + sPendingUpdate = sPendingCheckInfo; + sPendingCheckInfo = nullptr; // Ownership transferred + LL_INFOS("Velopack") << "Update downloaded and pending" << LL_ENDL; + } + else + { + char descr[512]; + vpkc_get_last_error(descr, sizeof(descr)); + LL_WARNS("Velopack") << "Failed to download update: " << ll_safe_string((const char*)descr) << LL_ENDL; + } +} + +static void on_downloading_closed(const LLSD& notification, const LLSD& response) +{ + sDownloadingNotification = nullptr; + if (sIsRequired) + { + // User closed the downloading dialog during a required update — re-show it + show_downloading_notification(sTargetVersion); + } +} + +static void show_downloading_notification(const std::string& version) +{ + LLSD args; + args["VERSION"] = version; + sDownloadingNotification = LLNotificationsUtil::add("DownloadingUpdate", args, LLSD(), on_downloading_closed); +} + +static void dismiss_downloading_notification() +{ + if (sDownloadingNotification) + { + LLNotificationsUtil::cancel(sDownloadingNotification); + sDownloadingNotification = nullptr; + } +} + +static void on_required_update_response(const LLSD& notification, const LLSD& response) +{ + LL_INFOS("Velopack") << "Required update acknowledged, starting download" << LL_ENDL; + show_downloading_notification(sTargetVersion); + LLCoros::instance().launch("VelopackRequiredUpdate", []() + { + velopack_download_pending_update(); + dismiss_downloading_notification(); + if (velopack_is_update_pending()) + { + LL_INFOS("Velopack") << "Required update downloaded, quitting to apply" << LL_ENDL; + velopack_request_restart_after_update(); + LLAppViewer::instance()->requestQuit(); + } + }); +} + +static void on_optional_update_response(const LLSD& notification, const LLSD& response) +{ + S32 option = LLNotificationsUtil::getSelectedOption(notification, response); + if (option == 0) // "Install" + { + LL_INFOS("Velopack") << "User accepted optional update, starting download" << LL_ENDL; + show_downloading_notification(sTargetVersion); + LLCoros::instance().launch("VelopackOptionalUpdate", []() + { + velopack_download_pending_update(); + dismiss_downloading_notification(); + if (velopack_is_update_pending()) + { + LL_INFOS("Velopack") << "Optional update downloaded, quitting to apply" << LL_ENDL; + velopack_request_restart_after_update(); + LLAppViewer::instance()->requestQuit(); + } + }); + } + else + { + LL_INFOS("Velopack") << "User declined optional update (option=" << option << ")" << LL_ENDL; + // Free the check info since user declined + if (sPendingCheckInfo) + { + vpkc_free_update_info(sPendingCheckInfo); + sPendingCheckInfo = nullptr; + } + } +} + +static void show_required_update_prompt() +{ + LLSD args; + args["VERSION"] = sTargetVersion; + args["URL"] = sReleaseNotesUrl; + LLNotificationsUtil::add("PauseForUpdate", args, LLSD(), on_required_update_response); +} + +void velopack_check_for_updates(const std::string& required_version, const std::string& relnotes_url) +{ + if (sUpdateUrl.empty()) + { + LL_DEBUGS("Velopack") << "No update URL set, skipping update check" << LL_ENDL; + return; + } + + // Allow downgrades only for rollbacks: VVM requires a version that's + // strictly lower than what we're running (e.g., a retracted build). + bool has_required = !required_version.empty(); + int ver_cmp = has_required ? compare_running_version(required_version) : 0; + bool allow_downgrade = ver_cmp > 0; // running > required → rollback scenario + ensure_update_manager(allow_downgrade); + if (!sUpdateManager) + return; + + // Ask Velopack to check its feed — this is the source of truth + vpkc_update_info_t* update_info = nullptr; + vpkc_update_check_t result = vpkc_check_for_updates(sUpdateManager, &update_info); + + if (result != UPDATE_AVAILABLE || !update_info) + { + LL_INFOS("Velopack") << "No update available from feed (result=" << result << ")" << LL_ENDL; + return; + } + + // Extract the actual target version from Velopack's feed + std::string target_version = update_info->TargetFullRelease->Version + ? update_info->TargetFullRelease->Version : ""; + LL_INFOS("Velopack") << "Update available: " << target_version + << " (required_version=" << required_version << ")" << LL_ENDL; + + // Store state for the prompt/download phase + sReleaseNotesUrl = relnotes_url; + sTargetVersion = target_version; + if (sPendingCheckInfo) + { + vpkc_free_update_info(sPendingCheckInfo); + } + sPendingCheckInfo = update_info; + + // Determine if this is mandatory: running version is below VVM's required floor + bool is_required = ver_cmp < 0; // running < required → must update + sIsRequired = is_required; + + if (is_required) + { + LL_INFOS("Velopack") << "Required update (running below " << required_version + << "), prompting user for " << target_version << LL_ENDL; + show_required_update_prompt(); + return; + } + + // Optional update — check user preference + U32 updater_setting = gSavedSettings.getU32("UpdaterServiceSetting"); + + if (updater_setting == 3) + { + // "Install each update automatically" — download silently, apply on quit + LL_INFOS("Velopack") << "Optional update to " << target_version + << ", downloading automatically (UpdaterServiceSetting=3)" << LL_ENDL; + velopack_download_pending_update(); + return; + } + + // Default / value 1: "Ask me when an optional update is ready to install" + LL_INFOS("Velopack") << "Optional update available (" << target_version << "), prompting user" << LL_ENDL; + LLSD args; + args["VERSION"] = target_version; + args["URL"] = relnotes_url; + LLNotificationsUtil::add("PromptOptionalUpdate", args, LLSD(), on_optional_update_response); +} + +std::string velopack_get_current_version() +{ + if (!sUpdateManager) + { + return ""; + } + + char version[64]; + size_t len = vpkc_get_current_version(sUpdateManager, version, sizeof(version)); + if (len > 0) + { + return std::string(version, len); + } + return ""; +} + +bool velopack_is_update_pending() +{ + return sPendingUpdate != nullptr; +} + +bool velopack_is_required_update_in_progress() +{ + return sIsRequired && sPendingCheckInfo != nullptr; +} + +std::string velopack_get_required_update_version() +{ + return sTargetVersion; +} + +bool velopack_should_restart_after_update() +{ + return sRestartAfterUpdate; +} + +void velopack_request_restart_after_update() +{ + sRestartAfterUpdate = true; +} + +void velopack_apply_pending_update(bool restart) +{ + if (!sUpdateManager || !sPendingUpdate || !sPendingUpdate->TargetFullRelease) + { + LL_WARNS("Velopack") << "Cannot apply update: no pending update or manager" << LL_ENDL; + return; + } + + LL_INFOS("Velopack") << "Applying pending update (restart=" << restart << ")" << LL_ENDL; + vpkc_wait_exit_then_apply_updates(sUpdateManager, + sPendingUpdate->TargetFullRelease, + false, + restart, + nullptr, 0); +} + +void velopack_cleanup() +{ + if (sUpdateManager) + { + vpkc_free_update_manager(sUpdateManager); + sUpdateManager = nullptr; + } + if (sUpdateSource) + { + vpkc_free_source(sUpdateSource); + sUpdateSource = nullptr; + } + if (sPendingUpdate) + { + vpkc_free_update_info(sPendingUpdate); + sPendingUpdate = nullptr; + } + if (sPendingCheckInfo) + { + vpkc_free_update_info(sPendingCheckInfo); + sPendingCheckInfo = nullptr; + } + sAssetUrlMap.clear(); +} + +void velopack_set_update_url(const std::string& url) +{ + sUpdateUrl = url; + LL_INFOS("Velopack") << "Update URL set to: " << url << LL_ENDL; +} + +void velopack_set_progress_callback(std::function callback) +{ + sProgressCallback = callback; +} + +#endif // LL_VELOPACK -- cgit v1.3 From 49790ad857fbbee05f786bf8c29477a6805c5b0d Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:21:52 +0300 Subject: #5628 Update taskbar's link to point to velopack --- indra/newview/llstartup.cpp | 5 +- indra/newview/llvelopack.cpp | 188 ++++++++++++++++++++++++++++++++++++++++--- indra/newview/llvelopack.h | 5 +- 3 files changed, 185 insertions(+), 13 deletions(-) (limited to 'indra/newview/llvelopack.cpp') diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index c23b493ad0..72acb9beae 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -2632,8 +2632,9 @@ void uninstall_nsis_if_required() S32 found_minor = 0; S32 found_patch = 0; U64 found_build = 0; + std::string nsis_path; - if (!get_nsis_version(found_major, found_minor, found_patch, found_build)) + if (!get_nsis_version(found_major, found_minor, found_patch, found_build, nsis_path)) { return; } @@ -2665,7 +2666,7 @@ void uninstall_nsis_if_required() // so there is no point to check build. LL_INFOS() << "Found NSIS install " << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL; - clear_nsis_links(); + clear_nsis_links(nsis_path); LLSD args; args["VERSION"] = llformat("%d.%d.%d", found_major, found_minor, found_patch); diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index 28e989c4ba..7a013dafb5 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -330,14 +330,14 @@ static std::wstring get_desktop_path() return L""; } -static bool create_shortcut(const std::wstring& shortcut_path, +static HRESULT create_shortcut(const std::wstring& shortcut_path, const std::wstring& target_path, const std::wstring& arguments, const std::wstring& description, const std::wstring& icon_path) { HRESULT hr = CoInitialize(NULL); - if (FAILED(hr)) return false; + if (FAILED(hr)) return hr; IShellLinkW* shell_link = nullptr; hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, @@ -365,7 +365,7 @@ static bool create_shortcut(const std::wstring& shortcut_path, } CoUninitialize(); - return SUCCEEDED(hr); + return hr; } static void register_protocol_handler(const std::wstring& protocol, @@ -405,9 +405,105 @@ static void register_protocol_handler(const std::wstring& protocol, } } -void clear_nsis_links() +static bool get_shortcut_target(const std::wstring& lnk_path, std::wstring& target_path_str) +{ + // Resolve the shortcut to check its target + wchar_t target_path[MAX_PATH] = { 0 }; + HRESULT hr = CoInitialize(NULL); + bool res = false; + if (SUCCEEDED(hr)) + { + IShellLinkW* psl = nullptr; + hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (void**)&psl); + if (SUCCEEDED(hr)) + { + IPersistFile* ppf = nullptr; + hr = psl->QueryInterface(IID_IPersistFile, (void**)&ppf); + if (SUCCEEDED(hr)) + { + hr = ppf->Load(lnk_path.c_str(), STGM_READ); + if (SUCCEEDED(hr)) + { + // Resolve() in theory may retarget to a different executable, + // but we should be fine as long as folder stays the same, + // otherwise we shouldn't clear it. + hr = psl->Resolve(NULL, SLR_NO_UI | SLR_NOUPDATE | SLR_ANY_MATCH); + if (SUCCEEDED(hr)) + { + hr = psl->GetPath(target_path, MAX_PATH, nullptr, 0); + if (SUCCEEDED(hr)) + { + target_path_str = std::wstring(target_path); + res = true; + } + } + } + ppf->Release(); + } + psl->Release(); + } + CoUninitialize(); + } + return res; +} + +static bool paths_are_equal(const std::wstring& path1, const std::wstring& path2) +{ + try + { + std::error_code ec1, ec2; + std::filesystem::path p1(path1); + std::filesystem::path p2(path2); + + // Get canonical (absolute, normalized) paths + auto canonical1 = std::filesystem::canonical(p1, ec1); + auto canonical2 = std::filesystem::canonical(p2, ec2); + + // If either path doesn't exist, fall back to string comparison + if (ec1 || ec2) + { + // Normalize case for comparison + std::wstring lower1 = path1; + std::wstring lower2 = path2; + std::transform(lower1.begin(), lower1.end(), lower1.begin(), ::towlower); + std::transform(lower2.begin(), lower2.end(), lower2.begin(), ::towlower); + while (!lower1.empty() && (lower1.back() == L'\\' || lower1.back() == L'/')) + { + lower1.pop_back(); + } + while (!lower2.empty() && (lower2.back() == L'\\' || lower2.back() == L'/')) + { + lower2.pop_back(); + } + return lower1 == lower2; + } + + // Use filesystem::equivalent which handles all path variations + return std::filesystem::equivalent(canonical1, canonical2, ec1); + } + catch (const std::filesystem::filesystem_error&) + { + // Fallback to case-insensitive string comparison + std::wstring lower1 = path1; + std::wstring lower2 = path2; + std::transform(lower1.begin(), lower1.end(), lower1.begin(), ::towlower); + std::transform(lower2.begin(), lower2.end(), lower2.begin(), ::towlower); + while (!lower1.empty() && (lower1.back() == L'\\' || lower1.back() == L'/')) + { + lower1.pop_back(); + } + while (!lower2.empty() && (lower2.back() == L'\\' || lower2.back() == L'/')) + { + lower2.pop_back(); + } + return lower1 == lower2; + } +} + +void clear_nsis_links(const std::string& nsis_folder_path) { wchar_t path[MAX_PATH]; + std::wstring app_name = get_app_name(); // 1. The 'start' shortcuts set by nsis would be global, like app shortcut: // C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Second Life Viewer\Second Life Viewer.lnk @@ -415,7 +511,7 @@ void clear_nsis_links() if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_PROGRAMS, NULL, 0, path))) { std::wstring start_menu_path = path; - std::wstring folder_path = start_menu_path + L"\\" + get_app_name(); + std::wstring folder_path = start_menu_path + L"\\" + app_name; std::error_code ec; std::filesystem::path dir(folder_path); @@ -431,11 +527,11 @@ void clear_nsis_links() } // 2. Desktop link, also a global one. - // C:\Users\Public\Desktop + // C:\Users\Public\Desktop\Second Life Viewer.lnk if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_COMMON_DESKTOPDIRECTORY, NULL, 0, path))) { std::wstring desktop_path = path; - std::wstring shortcut_path = desktop_path + L"\\" + get_app_name() + L".lnk"; + std::wstring shortcut_path = desktop_path + L"\\" + app_name + L".lnk"; if (!DeleteFileW(shortcut_path.c_str())) { DWORD error = GetLastError(); @@ -447,6 +543,70 @@ void clear_nsis_links() } } } + + // 3. Taskbar link, which is user-specific and located at: + // %AppData%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Second Life.lnk + // Note that it can be a link to velopack already, but since + // we aren't removing, but recreating, it shouldn't be an issue. + if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, path))) + { + std::wstring taskbar_path = path; + // Hardcoded, because this name is the default window name and isn't going to change in NSIS + taskbar_path += L"\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar\\Second Life.lnk"; + + if (PathFileExistsW(taskbar_path.c_str())) + { + // First try to overwrite shortcut + std::wstring current_exe = ll_convert(gDirUtilp->getExecutablePathAndName()); + + HRESULT hr = create_shortcut(taskbar_path, current_exe, L"", app_name, current_exe); + if (FAILED(hr)) + { + LL_WARNS("Velopack") << "Failed to update taskbar shortcut, error " << std::hex << hr << LL_ENDL; + // Try deleting and if possible recreating separately. We should not leave hanging shortcuts behind. + // It is not warranted that the shortcut points to NSIS, so check the destination first. + std::wstring target_path; + if (get_shortcut_target(taskbar_path, target_path)) + { + // Strip the filename part to get just the folder path + std::filesystem::path trim_path(target_path); + if (trim_path.has_filename()) + { + target_path = trim_path.parent_path().wstring(); + } + std::wstring nsis_path_w = ll_convert(nsis_folder_path); + if (paths_are_equal(nsis_path_w, target_path)) + { + if (!DeleteFileW(taskbar_path.c_str())) + { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND) + { + LL_WARNS("Velopack") << "Failed to delete NSIS taskbar shortcut: " + << ll_convert_wide_to_string(taskbar_path) + << " (error: " << error << ")" << LL_ENDL; + } + } + else + { + // Deleted user created link, recreate it with new target if possible. + LL_INFOS("Velopack") << "Updating taskbar shortcut to point to current exe" << LL_ENDL; + HRESULT hr = create_shortcut(taskbar_path, current_exe, L"", app_name, current_exe); + if (FAILED(hr)) + { + LL_WARNS("Velopack") << "Failed to re-create taskbar shortcut, error: " << std::hex << hr << ", NSIS shortcut was removed" << LL_ENDL; + } + } + } + } + } + else + { + LL_INFOS("Velopack") << "Successfully updated taskbar shortcut to point to current exe" << LL_ENDL; + } + } + // else user didn't create a taskbar shortcut. + } } static void parse_version(const wchar_t* version_str, int& major, int& minor, int& patch, uint64_t& build) @@ -462,7 +622,8 @@ bool get_nsis_version( int& nsis_major, int& nsis_minor, int& nsis_patch, - uint64_t& nsis_build) + uint64_t& nsis_build, + std::string& nsis_path) { // Test for presence of NSIS viewer registration, then // attempt to read uninstall info @@ -515,12 +676,21 @@ bool get_nsis_version( } std::error_code ec; std::filesystem::path path(path_buffer); + // check if uninstaller exists if (!std::filesystem::exists(path, ec)) { return false; } - // Todo: check codesigning? + // We found a valid NSIS installation, trim the path to get the folder + wchar_t* pos = wcsstr(path_buffer, L"uninst.exe"); + if (pos) + { + // Trim uninst.exe from the path by setting null terminator before it + *pos = L'\0'; + } + // Note that it has a trailing backslash. + nsis_path = ll_convert_wide_to_string(path_buffer); return true; } diff --git a/indra/newview/llvelopack.h b/indra/newview/llvelopack.h index d04d0db7dc..449330f68d 100644 --- a/indra/newview/llvelopack.h +++ b/indra/newview/llvelopack.h @@ -46,12 +46,13 @@ void velopack_set_progress_callback(std::function callback); void velopack_cleanup(); #if LL_WINDOWS -void clear_nsis_links(); +void clear_nsis_links(const std::string& nsis_folder_path); bool get_nsis_version( int& nsis_major, int& nsis_minor, int& nsis_patch, - uint64_t& nsis_build); + uint64_t& nsis_build, + std::string& nsis_folder_path); #endif #endif // LL_VELOPACK -- cgit v1.3 From bca3449582988d8df40eb8d6d2788e7b9158316b Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Fri, 17 Apr 2026 20:08:08 +0300 Subject: #5628 Update taskbar's link to point to velopack #2 --- indra/newview/llvelopack.cpp | 136 ++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 60 deletions(-) (limited to 'indra/newview/llvelopack.cpp') diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index 7a013dafb5..f1e5c004c6 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -500,6 +500,73 @@ static bool paths_are_equal(const std::wstring& path1, const std::wstring& path2 } } +static void update_taskbar_shortcut(const std::string& nsis_folder_path, const std::wstring& appdata_path, const std::wstring& app_name, const std::wstring& link_name) +{ + std::wstring taskbar_path = appdata_path; + taskbar_path += L"\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar\\"; + taskbar_path += link_name; + + if (!PathFileExistsW(taskbar_path.c_str())) + { + // User didn't create one + return; + } + + // Verify we are removing or updating the right thing. + // It is not warranteed that the shortcut points to NSIS. + std::wstring target_path; + if (get_shortcut_target(taskbar_path, target_path)) + { + // Strip the filename part to get just the folder path + std::filesystem::path trim_path(target_path); + if (trim_path.has_filename()) + { + target_path = trim_path.parent_path().wstring(); + } + std::wstring nsis_path_w = ll_convert(nsis_folder_path); + if (!paths_are_equal(nsis_path_w, target_path)) + { + // Shortcut points to something else, don't mess with it + LL_INFOS("Velopack") << "Found a matching shortcut, but it points to a different channel. Expected: " + << ll_convert_wide_to_string(nsis_path_w) + << ", actual: " << ll_convert_wide_to_string(target_path) + << LL_ENDL; + return; + } + } + + // First try to overwrite shortcut + std::wstring current_exe = ll_convert(gDirUtilp->getExecutablePathAndName()); + HRESULT hr = create_shortcut(taskbar_path, current_exe, L"", app_name, current_exe); + if (SUCCEEDED(hr)) + { + LL_INFOS("Velopack") << "Successfully updated taskbar shortcut to point to current exe" << LL_ENDL; + return; + } + + // Try deleting and if possible, recreating separately. We should not leave hanging shortcuts behind. + if (!DeleteFileW(taskbar_path.c_str())) + { + DWORD error = GetLastError(); + if (error != ERROR_FILE_NOT_FOUND) + { + LL_WARNS("Velopack") << "Failed to delete NSIS taskbar shortcut: " + << ll_convert_wide_to_string(taskbar_path) + << " (error: " << error << ")" << LL_ENDL; + } + } + else + { + // Deleted user created link, recreate it with new target if possible. + LL_INFOS("Velopack") << "Updating taskbar shortcut to point to current exe" << LL_ENDL; + HRESULT hr = create_shortcut(taskbar_path, current_exe, L"", app_name, current_exe); + if (FAILED(hr)) + { + LL_WARNS("Velopack") << "Failed to re-create taskbar shortcut, error: " << std::hex << hr << ", NSIS shortcut was removed" << LL_ENDL; + } + } +} + void clear_nsis_links(const std::string& nsis_folder_path) { wchar_t path[MAX_PATH]; @@ -544,68 +611,17 @@ void clear_nsis_links(const std::string& nsis_folder_path) } } - // 3. Taskbar link, which is user-specific and located at: - // %AppData%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Second Life.lnk - // Note that it can be a link to velopack already, but since - // we aren't removing, but recreating, it shouldn't be an issue. + // 3. Taskbar links, which are user-specific and located at: + // %AppData%\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\ + // Note that it can be a link to velopack already or to a different NSIS viewer. + // Name might also be different based on method of creation. if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA, NULL, 0, path))) { - std::wstring taskbar_path = path; - // Hardcoded, because this name is the default window name and isn't going to change in NSIS - taskbar_path += L"\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar\\Second Life.lnk"; - - if (PathFileExistsW(taskbar_path.c_str())) - { - // First try to overwrite shortcut - std::wstring current_exe = ll_convert(gDirUtilp->getExecutablePathAndName()); - - HRESULT hr = create_shortcut(taskbar_path, current_exe, L"", app_name, current_exe); - if (FAILED(hr)) - { - LL_WARNS("Velopack") << "Failed to update taskbar shortcut, error " << std::hex << hr << LL_ENDL; - // Try deleting and if possible recreating separately. We should not leave hanging shortcuts behind. - // It is not warranted that the shortcut points to NSIS, so check the destination first. - std::wstring target_path; - if (get_shortcut_target(taskbar_path, target_path)) - { - // Strip the filename part to get just the folder path - std::filesystem::path trim_path(target_path); - if (trim_path.has_filename()) - { - target_path = trim_path.parent_path().wstring(); - } - std::wstring nsis_path_w = ll_convert(nsis_folder_path); - if (paths_are_equal(nsis_path_w, target_path)) - { - if (!DeleteFileW(taskbar_path.c_str())) - { - DWORD error = GetLastError(); - if (error != ERROR_FILE_NOT_FOUND) - { - LL_WARNS("Velopack") << "Failed to delete NSIS taskbar shortcut: " - << ll_convert_wide_to_string(taskbar_path) - << " (error: " << error << ")" << LL_ENDL; - } - } - else - { - // Deleted user created link, recreate it with new target if possible. - LL_INFOS("Velopack") << "Updating taskbar shortcut to point to current exe" << LL_ENDL; - HRESULT hr = create_shortcut(taskbar_path, current_exe, L"", app_name, current_exe); - if (FAILED(hr)) - { - LL_WARNS("Velopack") << "Failed to re-create taskbar shortcut, error: " << std::hex << hr << ", NSIS shortcut was removed" << LL_ENDL; - } - } - } - } - } - else - { - LL_INFOS("Velopack") << "Successfully updated taskbar shortcut to point to current exe" << LL_ENDL; - } - } - // else user didn't create a taskbar shortcut. + update_taskbar_shortcut(nsis_folder_path, path, app_name, L"Second Life.lnk"); // Window class name + update_taskbar_shortcut(nsis_folder_path, path, app_name, L"Second Life(1).lnk"); // Just in case user somehow did it twice and removed first + update_taskbar_shortcut(nsis_folder_path, path, app_name, L"SecondLifeViewer.lnk"); // Executable name + update_taskbar_shortcut(nsis_folder_path, path, app_name, L"secondlife-bin.lnk"); // Debug builds + update_taskbar_shortcut(nsis_folder_path, path, app_name, app_name + L".lnk"); // Default name } } -- cgit v1.3 From 9cdd52df3c166a4cd7c0ac495f0ca435af458503 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev <117672381+akleshchev@users.noreply.github.com> Date: Sat, 18 Apr 2026 04:25:18 +0300 Subject: #5628 Fix setting cleanup on install --- indra/newview/llappviewerwin32.cpp | 16 +--------------- indra/newview/llvelopack.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) (limited to 'indra/newview/llvelopack.cpp') diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp index 8f11b219f5..6b26127925 100644 --- a/indra/newview/llappviewerwin32.cpp +++ b/indra/newview/llappviewerwin32.cpp @@ -447,22 +447,8 @@ int APIENTRY WINMAIN(HINSTANCE hInstance, // commands and exit the process before we do anything else. if (!velopack_initialize()) { + // Obsolete? Always return true // Velopack handled the invocation (install/uninstall hook) - - // Drop install related settings - gDirUtilp->initAppDirs("SecondLife"); - - std::string user_settings_path = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "settings.xml"); - LLControlGroup settings("global"); - if (settings.loadFromFile(user_settings_path)) - { - // If user reinstalls or updates, we want to recheck for nsis leftovers. - if (settings.controlExists("PreviousInstallChecked")) - { - settings.setBOOL("PreviousInstallChecked", false); - } - settings.saveToFile(user_settings_path, true); - } return 0; } #endif diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp index f1e5c004c6..6a667925b3 100644 --- a/indra/newview/llvelopack.cpp +++ b/indra/newview/llvelopack.cpp @@ -833,6 +833,33 @@ static void on_first_run(void* p_user_data, const char* app_version) MultiByteToWideChar(CP_UTF8, 0, app_version, -1, &version[0], len); register_uninstall_info(install_dir, app_name, version); + + // Drop install related settings + // Unfortunately gDirUtilp is not initialized yet and it's shouldn't + // be possible to change location of the settings. For now it's simpler + // to hardcode the location. + std::optional app_data = LLStringUtil::getoptenv("APPDATA"); + if (app_data) + { + // Strip trailing delimiter if present + std::string app_data_path = *app_data; + if (!app_data_path.empty() && (app_data_path.back() == '\\' || app_data_path.back() == '/')) + { + app_data_path.pop_back(); + } + + std::string user_settings_path = app_data_path + "\\SecondLife\\user_settings\\settings.xml"; + LLControlGroup settings("global"); + if (settings.loadFromFile(user_settings_path)) + { + // If user reinstalls or updates, we want to recheck for nsis leftovers. + if (settings.controlExists("PreviousInstallChecked")) + { + settings.setBOOL("PreviousInstallChecked", false); + } + settings.saveToFile(user_settings_path, true); + } + } } static void on_after_install(void* user_data, const char* app_version) -- cgit v1.3