535 lines
18 KiB
C++
535 lines
18 KiB
C++
/**
|
|
* @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 "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;
|
|
|
|
//
|
|
// 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 const wchar_t* VIEWER_EXE_NAME = L"SecondLifeViewer.exe";
|
|
|
|
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()
|
|
{
|
|
// LL_VIEWER_CHANNEL is defined at compile time via CMake (e.g., "Second Life Test")
|
|
return LL_TO_WSTRING(LL_VIEWER_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);
|
|
}
|
|
}
|
|
|
|
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_uninstaller_path(wchar_t* path_buffer, DWORD bufSize, S32 cur_major_ver, S32 cur_minor_ver, S32 cur_patch_ver, U64 cur_build_ver)
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
int nsis_major = 0, nsis_minor = 0, nsis_patch = 0;
|
|
uint64_t nsis_build = 0;
|
|
parse_version(version_buf, nsis_major, nsis_minor, nsis_patch, nsis_build);
|
|
|
|
// Compare numerically
|
|
if ((nsis_major > cur_major_ver) ||
|
|
(nsis_major == cur_major_ver && nsis_minor > cur_minor_ver) ||
|
|
(nsis_major == cur_major_ver && nsis_minor == cur_minor_ver && nsis_patch > cur_patch_ver) ||
|
|
// Assume that bigger build number means newer version, which is not always true but works for our purposes
|
|
(nsis_major == cur_major_ver && nsis_minor == cur_minor_ver && nsis_patch == cur_patch_ver && nsis_build > cur_build_ver))
|
|
{
|
|
LL_INFOS() << "Found installed nsis version that is newer" << nsis_major << "." << nsis_minor << "." << nsis_patch << LL_ENDL;
|
|
RegCloseKey(hkey);
|
|
return false;
|
|
}
|
|
|
|
LONG rv = RegGetValueW(hkey, nullptr, L"UninstallString", RRF_RT_REG_SZ, &type, path_buffer, &bufSize);
|
|
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();
|
|
std::wstring key_path = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + 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"\\" + VIEWER_EXE_NAME;
|
|
std::wstring uninstall_cmd = L"\"" + install_dir + L"\\Update.exe\" --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"http://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));
|
|
|
|
DWORD estimated_size = 120000;
|
|
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\\" + 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"\\" + 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_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"\\" + VIEWER_EXE_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_protocol_handler(PROTOCOL_SECONDLIFE, L"URL:Second Life", exe_path);
|
|
register_protocol_handler(PROTOCOL_GRID_INFO, L"URL:Second Life", exe_path);
|
|
register_uninstall_info(install_dir, app_name, version);
|
|
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_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));
|
|
}
|
|
}
|
|
|
|
//
|
|
// Public API - Cross-platform
|
|
//
|
|
|
|
bool velopack_initialize()
|
|
{
|
|
vpkc_set_logger(on_log_message, nullptr);
|
|
|
|
#if LL_WINDOWS || LL_DARWIN
|
|
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;
|
|
}
|
|
|
|
void velopack_check_for_updates()
|
|
{
|
|
if (sUpdateUrl.empty())
|
|
{
|
|
LL_DEBUGS("Velopack") << "No update URL set, skipping update check" << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
if (!sUpdateManager)
|
|
{
|
|
vpkc_update_options_t options = {};
|
|
options.AllowVersionDowngrade = false;
|
|
options.ExplicitChannel = nullptr;
|
|
|
|
if (!vpkc_new_update_manager(sUpdateUrl.c_str(), &options, nullptr, &sUpdateManager))
|
|
{
|
|
LL_WARNS("Velopack") << "Failed to create update manager" << LL_ENDL;
|
|
return;
|
|
}
|
|
}
|
|
|
|
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") << "Update available, downloading..." << LL_ENDL;
|
|
if (vpkc_download_updates(sUpdateManager, update_info, on_progress, nullptr))
|
|
{
|
|
if (sPendingUpdate)
|
|
{
|
|
vpkc_free_update_info(sPendingUpdate);
|
|
}
|
|
sPendingUpdate = update_info;
|
|
LL_INFOS("Velopack") << "Update downloaded and pending" << LL_ENDL;
|
|
}
|
|
else
|
|
{
|
|
LL_WARNS("Velopack") << "Failed to download update" << LL_ENDL;
|
|
vpkc_free_update_info(update_info);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("Velopack") << "No update available (result=" << result << ")" << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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_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
|