summaryrefslogtreecommitdiff
path: root/indra/newview/llvelopack.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview/llvelopack.cpp')
-rw-r--r--indra/newview/llvelopack.cpp1201
1 files changed, 1201 insertions, 0 deletions
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 <boost/json.hpp>
+#include <fstream>
+#include <unordered_map>
+#include "llnotificationsutil.h"
+#include "llviewercontrol.h"
+#include "llappviewer.h"
+#include "llcoros.h"
+
+#include "Velopack.h"
+
+#if LL_WINDOWS
+#include <windows.h>
+#include <shlobj.h>
+#include <shobjidl.h>
+#include <shlwapi.h>
+#include <objbase.h>
+#include <filesystem>
+
+#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<void(int)> 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<std::string, std::string> 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<LLCoreHttpUtil::HttpCoroutineAdapter>("VelopackSource", httpPolicy);
+ auto httpRequest = std::make_shared<LLCore::HttpRequest>();
+ auto httpOpts = std::make_shared<LLCore::HttpOptions>();
+ 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<LLCoreHttpUtil::HttpCoroutineAdapter>("VelopackDownload", httpPolicy);
+ auto httpRequest = std::make_shared<LLCore::HttpRequest>();
+ auto httpOpts = std::make_shared<LLCore::HttpOptions>();
+ 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<const char*>(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<char*>(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<char*>(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<std::wstring>(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<int>(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 <bundle>/Contents/MacOS/<exe>
+ // 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 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<package xmlns=\"http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd\">\n"
+ " <metadata>\n"
+ " <id>" + pack_id + "</id>\n"
+ " <version>" + current_version + "</version>\n"
+ " <title>" + app_name + "</title>\n"
+ " </metadata>\n"
+ "</package>\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<char*>(bundle_root.c_str());
+ locator.CurrentBinaryDir = const_cast<char*>(exe_dir.c_str());
+ locator.ManifestPath = const_cast<char*>(manifest_path.c_str());
+ locator.PackagesDir = const_cast<char*>(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<void(int)> callback)
+{
+ sProgressCallback = callback;
+}
+
+#endif // LL_VELOPACK