diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4c8d98ce2f6..5176cd6ddcd 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,6 +2,14 @@ name: Build
on:
workflow_dispatch:
+ inputs:
+ installer_type:
+ description: 'Windows installer type'
+ type: choice
+ options:
+ - velopack
+ - nsis
+ default: 'velopack'
pull_request:
push:
branches: ["main", "release/*", "project/*"]
@@ -53,6 +61,20 @@ jobs:
relnotes: ${{ steps.which-branch.outputs.relnotes }}
imagename: ${{ steps.build.outputs.imagename }}
configuration: ${{ matrix.configuration }}
+ # Windows Velopack outputs (passed to sign-pkg-windows)
+ velopack_pack_id: ${{ steps.build.outputs.velopack_pack_id }}
+ velopack_pack_version: ${{ steps.build.outputs.velopack_pack_version }}
+ velopack_pack_title: ${{ steps.build.outputs.velopack_pack_title }}
+ velopack_main_exe: ${{ steps.build.outputs.velopack_main_exe }}
+ velopack_exclude: ${{ steps.build.outputs.velopack_exclude }}
+ velopack_icon: ${{ steps.build.outputs.velopack_icon }}
+ velopack_installer_base: ${{ steps.build.outputs.velopack_installer_base }}
+ # macOS Velopack outputs (passed to sign-pkg-mac)
+ velopack_mac_pack_id: ${{ steps.build.outputs.velopack_mac_pack_id }}
+ velopack_mac_pack_version: ${{ steps.build.outputs.velopack_mac_pack_version }}
+ velopack_mac_pack_title: ${{ steps.build.outputs.velopack_mac_pack_title }}
+ velopack_mac_main_exe: ${{ steps.build.outputs.velopack_mac_main_exe }}
+ velopack_mac_bundle_id: ${{ steps.build.outputs.velopack_mac_bundle_id }}
env:
AUTOBUILD_ADDRSIZE: 64
AUTOBUILD_BUILD_ID: ${{ github.run_id }}
@@ -84,6 +106,8 @@ jobs:
# Only set variants to the one configuration: don't let build.sh loop
# over variants, let GitHub distribute variants over multiple hosts.
variants: ${{ matrix.configuration }}
+ # Pass USE_VELOPACK to CMake when using Velopack installer (default) - Windows and macOS
+ autobuild_configure_parameters: ${{ (contains(matrix.runner, 'windows') || contains(matrix.runner, 'macos')) && (github.event.inputs.installer_type || 'velopack') == 'velopack' && '-- -DUSE_VELOPACK:BOOL=ON' || '' }}
steps:
- name: Checkout code
uses: actions/checkout@v5
@@ -126,6 +150,17 @@ jobs:
with:
token: ${{ github.token }}
+ - name: Setup .NET for Velopack
+ if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack'
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '9.0.x'
+
+ - name: Install Velopack CLI
+ if: (runner.os == 'Windows' || runner.os == 'macOS') && (github.event.inputs.installer_type || 'velopack') == 'velopack'
+ shell: bash
+ run: dotnet tool install -g vpk
+
- name: Build
id: build
shell: bash
@@ -310,13 +345,21 @@ jobs:
steps:
- name: Sign and package Windows viewer
if: env.AZURE_KEY_VAULT_URI && env.AZURE_CERT_NAME && env.AZURE_CLIENT_ID && env.AZURE_CLIENT_SECRET && env.AZURE_TENANT_ID
- uses: secondlife/viewer-build-util/sign-pkg-windows@v2.0.4
+ uses: secondlife/viewer-build-util/sign-pkg-windows@geenz/velopack
with:
vault_uri: "${{ env.AZURE_KEY_VAULT_URI }}"
cert_name: "${{ env.AZURE_CERT_NAME }}"
client_id: "${{ env.AZURE_CLIENT_ID }}"
client_secret: "${{ env.AZURE_CLIENT_SECRET }}"
tenant_id: "${{ env.AZURE_TENANT_ID }}"
+ installer_type: "${{ github.event.inputs.installer_type || 'velopack' }}"
+ velopack_pack_id: "${{ needs.build.outputs.velopack_pack_id }}"
+ velopack_pack_version: "${{ needs.build.outputs.velopack_pack_version }}"
+ velopack_pack_title: "${{ needs.build.outputs.velopack_pack_title }}"
+ velopack_main_exe: "${{ needs.build.outputs.velopack_main_exe }}"
+ velopack_exclude: "${{ needs.build.outputs.velopack_exclude }}"
+ velopack_icon: "${{ needs.build.outputs.velopack_icon }}"
+ velopack_installer_base: "${{ needs.build.outputs.velopack_installer_base }}"
sign-and-package-mac:
env:
@@ -349,7 +392,7 @@ jobs:
- name: Sign and package Mac viewer
if: env.SIGNING_CERT_MACOS && env.SIGNING_CERT_MACOS_IDENTITY && env.SIGNING_CERT_MACOS_PASSWORD && steps.note-creds.outputs.note_user && steps.note-creds.outputs.note_pass && steps.note-creds.outputs.note_team
- uses: secondlife/viewer-build-util/sign-pkg-mac@v2
+ uses: secondlife/viewer-build-util/sign-pkg-mac@geenz/velopack
with:
channel: ${{ needs.build.outputs.viewer_channel }}
imagename: ${{ needs.build.outputs.imagename }}
@@ -359,6 +402,11 @@ jobs:
note_user: ${{ steps.note-creds.outputs.note_user }}
note_pass: ${{ steps.note-creds.outputs.note_pass }}
note_team: ${{ steps.note-creds.outputs.note_team }}
+ velopack_pack_id: "${{ needs.build.outputs.velopack_mac_pack_id }}"
+ velopack_pack_version: "${{ needs.build.outputs.velopack_mac_pack_version }}"
+ velopack_pack_title: "${{ needs.build.outputs.velopack_mac_pack_title }}"
+ velopack_main_exe: "${{ needs.build.outputs.velopack_mac_main_exe }}"
+ velopack_bundle_id: "${{ needs.build.outputs.velopack_mac_bundle_id }}"
post-windows-symbols:
env:
@@ -439,6 +487,10 @@ jobs:
with:
pattern: "*-metadata"
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: "*-releases"
+
- name: Rename metadata
run: |
cp Windows-metadata/autobuild-package.xml Windows-autobuild-package.xml
@@ -464,12 +516,14 @@ jobs:
generate_release_notes: true
target_commitish: ${{ github.sha }}
append_body: true
- fail_on_unmatched_files: true
+ fail_on_unmatched_files: false
files: |
macOS-installer/*.dmg
Windows-installer/*.exe
*-autobuild-package.xml
*-viewer_version.txt
+ Windows-releases/*
+ macOS-releases/*
- name: post release URL
run: |
diff --git a/.github/workflows/tag-release.yaml b/.github/workflows/tag-release.yaml
index 2922065f995..0f826222a05 100644
--- a/.github/workflows/tag-release.yaml
+++ b/.github/workflows/tag-release.yaml
@@ -21,7 +21,9 @@ on:
project:
description: "Project Name (used for channel name in project builds, and tag name for all builds)"
default: "hippo"
- # TODO - add an input for selecting another sha to build other than head of branch
+ tag_override:
+ description: "Override the tag name (optional). If the tag already exists, a numeric suffix is appended."
+ required: false
jobs:
tag-release:
@@ -34,7 +36,7 @@ jobs:
NIGHTLY_DATE=$(date --rfc-3339=date)
echo NIGHTLY_DATE=${NIGHTLY_DATE} >> ${GITHUB_ENV}
echo TAG_ID="$(echo ${{ github.sha }} | cut -c1-8)-${{ inputs.project || '${NIGHTLY_DATE}' }}" >> ${GITHUB_ENV}
- - name: Update Tag
+ - name: Create Tag
uses: actions/github-script@v8
with:
# use a real access token instead of GITHUB_TOKEN default.
@@ -44,9 +46,27 @@ jobs:
# this token will need to be renewed anually in January
github-token: ${{ secrets.LL_TAG_RELEASE_TOKEN }}
script: |
- github.rest.git.createRef({
- owner: context.repo.owner,
- repo: context.repo.repo,
- ref: "refs/tags/${{ env.VIEWER_CHANNEL }}#${{ env.TAG_ID }}",
- sha: context.sha
- })
+ const override = `${{ inputs.tag_override }}`.trim();
+ const baseTag = override || `${{ env.VIEWER_CHANNEL }}#${{ env.TAG_ID }}`;
+
+ // Try the base tag first, then append -2, -3, etc. if it already exists
+ let tag = baseTag;
+ for (let attempt = 1; ; attempt++) {
+ try {
+ await github.rest.git.createRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: `refs/tags/${tag}`,
+ sha: context.sha
+ });
+ core.info(`Created tag: ${tag}`);
+ break;
+ } catch (e) {
+ if (e.status === 422 && attempt < 10) {
+ core.info(`Tag '${tag}' already exists, trying next suffix...`);
+ tag = `${baseTag}-${attempt + 1}`;
+ } else {
+ throw e;
+ }
+ }
+ }
diff --git a/autobuild.xml b/autobuild.xml
index 7333583415a..ff05623985a 100644
--- a/autobuild.xml
+++ b/autobuild.xml
@@ -2914,6 +2914,56 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors
description
Voxelized Hierarchical Approximate Convex Decomposition
+ velopack
+
package_description
diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt
index 2ba282bdb78..c10f6ec934b 100644
--- a/indra/cmake/CMakeLists.txt
+++ b/indra/cmake/CMakeLists.txt
@@ -62,6 +62,7 @@ set(cmake_SOURCE_FILES
UI.cmake
UnixInstall.cmake
Variables.cmake
+ Velopack.cmake
VHACD.cmake
ViewerMiscLibs.cmake
VisualLeakDetector.cmake
diff --git a/indra/cmake/Python.cmake b/indra/cmake/Python.cmake
index 7cce190f6a5..39fd21c33f8 100644
--- a/indra/cmake/Python.cmake
+++ b/indra/cmake/Python.cmake
@@ -13,7 +13,7 @@ elseif (WINDOWS)
foreach(hive HKEY_CURRENT_USER HKEY_LOCAL_MACHINE)
# prefer more recent Python versions to older ones, if multiple versions
# are installed
- foreach(pyver 3.13 3.12 3.11 3.10 3.9 3.8 3.7)
+ foreach(pyver 3.14 3.13 3.12 3.11 3.10 3.9 3.8 3.7)
list(APPEND regpaths "[${hive}\\SOFTWARE\\Python\\PythonCore\\${pyver}\\InstallPath]")
endforeach()
endforeach()
diff --git a/indra/cmake/Velopack.cmake b/indra/cmake/Velopack.cmake
new file mode 100644
index 00000000000..a1dbe2cbe92
--- /dev/null
+++ b/indra/cmake/Velopack.cmake
@@ -0,0 +1,68 @@
+# -*- cmake -*-
+# Velopack installer and update framework integration
+# https://velopack.io/
+
+include_guard()
+
+# USE_VELOPACK controls whether to use Velopack for installer packaging (instead of NSIS/DMG)
+option(USE_VELOPACK "Use Velopack for installer packaging" OFF)
+
+if (WINDOWS)
+ include(Prebuilt)
+ use_prebuilt_binary(velopack)
+
+ add_library(ll::velopack INTERFACE IMPORTED)
+
+ target_include_directories(ll::velopack SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include/velopack
+ )
+
+ target_link_libraries(ll::velopack INTERFACE
+ ${ARCH_PREBUILT_DIRS_RELEASE}/velopack_libc.lib
+ )
+
+ # Windows system libraries required by Velopack
+ target_link_libraries(ll::velopack INTERFACE
+ winhttp
+ ole32
+ shell32
+ shlwapi
+ version
+ userenv
+ ws2_32
+ bcrypt
+ ntdll
+ )
+
+ target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
+
+elseif (DARWIN)
+ include(Prebuilt)
+ use_prebuilt_binary(velopack)
+
+ add_library(ll::velopack INTERFACE IMPORTED)
+
+ target_include_directories(ll::velopack SYSTEM INTERFACE
+ ${LIBS_PREBUILT_DIR}/include/velopack
+ )
+
+ target_link_libraries(ll::velopack INTERFACE
+ ${ARCH_PREBUILT_DIRS_RELEASE}/libvelopack_libc.a
+ )
+
+ # macOS system frameworks required by Velopack (Rust static library dependencies)
+ target_link_libraries(ll::velopack INTERFACE
+ "-framework Foundation"
+ "-framework Security"
+ "-framework SystemConfiguration"
+ "-framework AppKit"
+ "-framework CoreFoundation"
+ "-framework CoreServices"
+ "-framework IOKit"
+ "-liconv"
+ "-lresolv"
+ )
+
+ target_compile_definitions(ll::velopack INTERFACE LL_VELOPACK=1)
+
+endif()
diff --git a/indra/lib/python/indra/util/llmanifest.py b/indra/lib/python/indra/util/llmanifest.py
index 1bd65eb57df..0ad0b6b1a90 100755
--- a/indra/lib/python/indra/util/llmanifest.py
+++ b/indra/lib/python/indra/util/llmanifest.py
@@ -157,7 +157,8 @@ def get_default_platform(dummy):
for use by a .bat file.""",
default=None),
dict(name='versionfile',
- description="""The name of a file containing the full version number."""),
+ description="""The name of a file containing the full version number.""",
+ default=None),
]
def usage(arguments, srctree=""):
diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp
index 0407d6c3e94..111ad4322e8 100644
--- a/indra/llcommon/workqueue.cpp
+++ b/indra/llcommon/workqueue.cpp
@@ -38,7 +38,8 @@ LL::WorkQueueBase::WorkQueueBase(const std::string& name, bool auto_shutdown)
{
// Register for "LLApp" events so we can implicitly close() on viewer shutdown
std::string listener_name = "WorkQueue:" + getKey();
- LLEventPumps::instance().obtain("LLApp").listen(
+ LLEventPumps* pump = LLEventPumps::getInstance();
+ pump->obtain("LLApp").listen(
listener_name,
[this](const LLSD& stat)
{
@@ -54,14 +55,25 @@ LL::WorkQueueBase::WorkQueueBase(const std::string& name, bool auto_shutdown)
// Store the listener name so we can unregister in the destructor
mListenerName = listener_name;
+ mPumpHandle = pump->getHandle();
}
}
LL::WorkQueueBase::~WorkQueueBase()
{
- if (!mListenerName.empty() && !LLEventPumps::wasDeleted())
+ if (!mListenerName.empty() && !mPumpHandle.isDead())
{
- LLEventPumps::instance().obtain("LLApp").stopListening(mListenerName);
+ // Due to shutdown order issues, use handle, not a singleton
+ // and ignore fiber issue.
+ try
+ {
+ LLEventPumps* pump = mPumpHandle.get();
+ pump->obtain("LLApp").stopListening(mListenerName);
+ }
+ catch (const boost::fibers::lock_error&)
+ {
+ // Likely mutex is down, ignore
+ }
}
}
diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h
index 573203a5b35..69f3286c1b9 100644
--- a/indra/llcommon/workqueue.h
+++ b/indra/llcommon/workqueue.h
@@ -14,6 +14,7 @@
#include "llcoros.h"
#include "llexception.h"
+#include "llhandle.h"
#include "llinstancetracker.h"
#include "llinstancetrackersubclass.h"
#include "threadsafeschedule.h"
@@ -22,6 +23,9 @@
#include // std::function
#include
+class LLEventPumps;
+
+
namespace LL
{
@@ -202,6 +206,8 @@ namespace LL
// Name used for the LLApp event listener (empty if not registered)
std::string mListenerName;
+ // Due to shutdown order issues, store by handle
+ LLHandle mPumpHandle;
};
/*****************************************************************************
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 0949a3b59f2..670450fd6b1 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -43,6 +43,7 @@ include(TinyEXR)
include(ThreeJS)
include(Tracy)
include(UI)
+include(Velopack)
include(ViewerMiscLibs)
include(ViewerManager)
include(VisualLeakDetector)
@@ -659,6 +660,7 @@ set(viewer_SOURCE_FILES
llurllineeditorctrl.cpp
llurlwhitelist.cpp
llversioninfo.cpp
+ llvvmquery.cpp
llviewchildren.cpp
llviewerassetstats.cpp
llviewerassetstorage.cpp
@@ -1337,6 +1339,7 @@ set(viewer_HEADER_FILES
llurllineeditorctrl.h
llurlwhitelist.h
llversioninfo.h
+ llvvmquery.h
llviewchildren.h
llviewerassetstats.h
llviewerassetstorage.h
@@ -1456,6 +1459,8 @@ if (DARWIN)
LIST(APPEND viewer_SOURCE_FILES llappviewermacosx-objc.h)
LIST(APPEND viewer_SOURCE_FILES llfilepicker_mac.mm)
LIST(APPEND viewer_HEADER_FILES llfilepicker_mac.h)
+ LIST(APPEND viewer_SOURCE_FILES llvelopack.cpp)
+ LIST(APPEND viewer_HEADER_FILES llvelopack.h)
set_source_files_properties(
llappviewermacosx-objc.mm
@@ -1518,16 +1523,19 @@ if (WINDOWS)
list(APPEND viewer_SOURCE_FILES
llappviewerwin32.cpp
+ llvelopack.cpp
llwindebug.cpp
)
set_source_files_properties(
llappviewerwin32.cpp
+ llvelopack.cpp
PROPERTIES
COMPILE_DEFINITIONS "${VIEWER_CHANNEL_VERSION_DEFINES}"
)
list(APPEND viewer_HEADER_FILES
llappviewerwin32.h
+ llvelopack.h
llwindebug.h
)
@@ -1941,6 +1949,7 @@ if (WINDOWS)
"--discord=${USE_DISCORD}"
"--openal=${USE_OPENAL}"
"--tracy=${USE_TRACY}"
+ "--velopack=${USE_VELOPACK}"
--build=${CMAKE_CURRENT_BINARY_DIR}
--buildtype=$
"--channel=${VIEWER_CHANNEL}"
@@ -2064,6 +2073,10 @@ if (USE_DISCORD)
target_link_libraries(${VIEWER_BINARY_NAME} ll::discord_sdk )
endif ()
+if (TARGET ll::velopack)
+ target_link_libraries(${VIEWER_BINARY_NAME} ll::velopack )
+endif ()
+
if( TARGET ll::intel_memops )
target_link_libraries(${VIEWER_BINARY_NAME} ll::intel_memops )
endif()
@@ -2261,9 +2274,11 @@ if (DARWIN)
--arch=${ARCH}
--artwork=${ARTWORK_DIR}
"--bugsplat=${BUGSPLAT_DB}"
+ --bundleid=${MACOSX_BUNDLE_GUI_IDENTIFIER}
"--discord=${USE_DISCORD}"
"--openal=${USE_OPENAL}"
"--tracy=${USE_TRACY}"
+ "--velopack=${USE_VELOPACK}"
--build=${CMAKE_CURRENT_BINARY_DIR}
--buildtype=$
"--channel=${VIEWER_CHANNEL}"
diff --git a/indra/newview/VIEWER_VERSION.txt b/indra/newview/VIEWER_VERSION.txt
index 2aaedf99442..124b7a2cd06 100644
--- a/indra/newview/VIEWER_VERSION.txt
+++ b/indra/newview/VIEWER_VERSION.txt
@@ -1 +1 @@
-26.1.0
+26.1.1
diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml
index 372a84743fb..aa04d3017f7 100644
--- a/indra/newview/app_settings/settings.xml
+++ b/indra/newview/app_settings/settings.xml
@@ -4237,7 +4237,17 @@
Value
0.0.0
-
+ PreviousInstallChecked
+
+ Comment
+ Whether viewer checked previous install on the same channel for NSIS
+ Persist
+ 1
+ Type
+ Boolean
+ Value
+ 0
+
LimitDragDistance
Comment
@@ -13057,11 +13067,11 @@
UpdaterShowReleaseNotes
Comment
- Enables displaying of the Release notes in a web floater after update.
+ Enables displaying of the Release notes in a web floater after update. 0 - don't show, 1 - show, 2 - show even for test viewers
Persist
1
Type
- Boolean
+ S32
Value
1
diff --git a/indra/newview/installers/windows/installer_template.nsi b/indra/newview/installers/windows/installer_template.nsi
index 0e366980185..07ed0d0824b 100644
--- a/indra/newview/installers/windows/installer_template.nsi
+++ b/indra/newview/installers/windows/installer_template.nsi
@@ -767,8 +767,21 @@ Function un.UserSettingsFiles
StrCmp $DO_UNINSTALL_V2 "true" Keep # Don't remove user's settings files on auto upgrade
-# Ask if user wants to keep data files or not
-MessageBox MB_YESNO|MB_ICONQUESTION $(RemoveDataFilesMB) IDYES Remove IDNO Keep
+ClearErrors
+Push $0
+${GetParameters} $COMMANDLINE
+${GetOptionsS} $COMMANDLINE "/clrusrfiles" $0
+# GetOptionsS returns an error if option does not exist, jump past Goto.
+IfErrors +3 0
+ Pop $0
+ Goto Remove
+
+Pop $0
+ClearErrors
+
+ifSilent Keep 0
+ # Ask if user wants to keep data files or not
+ MessageBox MB_YESNO|MB_ICONQUESTION $(RemoveDataFilesMB) IDYES Remove IDNO Keep
Remove:
Push $0
@@ -864,11 +877,25 @@ RMDir "$INSTDIR"
IfFileExists "$INSTDIR" FOLDERFOUND NOFOLDER
FOLDERFOUND:
+ifSilent NOFOLDER 0
MessageBox MB_OK $(DeleteProgramFilesMB) /SD IDOK IDOK NOFOLDER
NOFOLDER:
-MessageBox MB_YESNO $(DeleteRegistryKeysMB) IDYES DeleteKeys IDNO NoDelete
+ClearErrors
+Push $0
+${GetParameters} $COMMANDLINE
+${GetOptionsS} $COMMANDLINE "/clearreg" $0
+# GetOptionsS returns an error if option does not exist, jump past Goto.
+IfErrors +3 0
+ Pop $0
+ Goto DeleteKeys
+
+Pop $0
+ClearErrors
+
+ifSilent NoDelete 0
+ MessageBox MB_YESNO $(DeleteRegistryKeysMB) IDYES DeleteKeys IDNO NoDelete
DeleteKeys:
DeleteRegKey SHELL_CONTEXT "SOFTWARE\Classes\x-grid-location-info"
@@ -912,21 +939,7 @@ Function .onInstSuccess
Call CheckWindowsServPack # Warn if not on the latest SP before asking to launch.
StrCmp $SKIP_AUTORUN "true" +2;
- # Assumes SetOutPath $INSTDIR
- # Run INSTEXE (our updater), passing VIEWER_EXE plus the command-line
- # arguments built into our shortcuts. This gives the updater a chance
- # to verify that the viewer we just installed is appropriate for the
- # running system -- or, if not, to download and install a different
- # viewer. For instance, if a user running 32-bit Windows installs a
- # 64-bit viewer, it cannot run on this system. But since the updater
- # is a 32-bit executable even in the 64-bit viewer package, the
- # updater can detect the problem and adapt accordingly.
- # Once everything is in order, the updater will run the specified
- # viewer with the specified params.
- # Quote the updater executable and the viewer executable because each
- # must be a distinct command-line token, but DO NOT quote the language
- # string because it must decompose into separate command-line tokens.
- Exec '"$INSTDIR\$INSTEXE" precheck "$INSTDIR\$VIEWER_EXE" $SHORTCUT_LANG_PARAM'
+ Exec '"$INSTDIR\$VIEWER_EXE" $SHORTCUT_LANG_PARAM'
#
FunctionEnd
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index 9a421972e58..d1c60eee401 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -98,6 +98,11 @@
#include "llurlmatch.h"
#include "lltextutil.h"
#include "lllogininstance.h"
+#include "llvvmquery.h"
+
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
#include "llprogressview.h"
#include "llvocache.h"
#include "lldiskcache.h"
@@ -382,9 +387,6 @@ const std::string ERROR_MARKER_FILE_NAME("SecondLife.error_marker");
const std::string LOGOUT_MARKER_FILE_NAME("SecondLife.logout_marker");
static std::string gLaunchFileOnQuit;
-// Used on Win32 for other apps to identify our window (eg, win_setup)
-const char* const VIEWER_WINDOW_CLASSNAME = "Second Life";
-
//----------------------------------------------------------------------------
// List of entries from strings.xml to always replace
@@ -656,7 +658,6 @@ LLAppViewer::LLAppViewer()
mPurgeCacheOnExit(false),
mPurgeUserDataOnExit(false),
mSecondInstance(false),
- mUpdaterNotFound(false),
mSavedFinalSnapshot(false),
mSavePerAccountSettings(false), // don't save settings on logout unless login succeeded.
mQuitRequested(false),
@@ -1112,68 +1113,17 @@ bool LLAppViewer::init()
gGLActive = false;
-#if LL_RELEASE_FOR_DOWNLOAD
- // Skip updater if this is a non-interactive instance
+//#if LL_RELEASE_FOR_DOWNLOAD
+ // Launch VVM update check
if (!gSavedSettings.getBOOL("CmdLineSkipUpdater") && !gNonInteractive)
{
- LLProcess::Params updater;
- updater.desc = "updater process";
- // Because it's the updater, it MUST persist beyond the lifespan of the
- // viewer itself.
- updater.autokill = false;
- std::string updater_file;
-#if LL_WINDOWS
- updater_file = "SLVersionChecker.exe";
- updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, updater_file);
-#elif LL_DARWIN
- updater_file = "SLVersionChecker";
- updater.executable = gDirUtilp->add(gDirUtilp->getAppRODataDir(), "updater", updater_file);
-#else
- updater_file = "SLVersionChecker";
- updater.executable = gDirUtilp->getExpandedFilename(LL_PATH_EXECUTABLE, updater_file);
-#endif
- // add LEAP mode command-line argument to whichever of these we selected
- updater.args.add("leap");
- // UpdaterServiceSettings
- if (gSavedSettings.getBOOL("FirstLoginThisInstall"))
- {
- // Befor first login, treat this as 'manual' updates,
- // updater won't install anything, but required updates
- updater.args.add("0");
- }
- else
- {
- updater.args.add(stringize(gSavedSettings.getU32("UpdaterServiceSetting")));
- }
- // channel
- updater.args.add(LLVersionInfo::instance().getChannel());
- // testok
- updater.args.add(stringize(gSavedSettings.getBOOL("UpdaterWillingToTest")));
- // ForceAddressSize
- updater.args.add(stringize(gSavedSettings.getU32("ForceAddressSize")));
-
- try
- {
- // Run the updater. An exception from launching the updater should bother us.
- LLLeap::create(updater, true);
- mUpdaterNotFound = false;
- }
- catch (...)
- {
- LLUIString details = LLNotifications::instance().getGlobalString("LLLeapUpdaterFailure");
- details.setArg("[UPDATER_APP]", updater_file);
- OSMessageBox(
- details.getString(),
- LLStringUtil::null,
- OSMB_OK);
- mUpdaterNotFound = true;
- }
+ initVVMUpdateCheck();
}
else
{
LL_WARNS("InitInfo") << "Skipping updater check." << LL_ENDL;
}
-#endif //LL_RELEASE_FOR_DOWNLOAD
+//#endif //LL_RELEASE_FOR_DOWNLOAD
{
// Iterate over --leap command-line options. But this is a bit tricky: if
@@ -1711,6 +1661,16 @@ void LLAppViewer::flushLFSIO()
bool LLAppViewer::cleanup()
{
+#if LL_VELOPACK
+ // Apply any pending Velopack update before shutdown
+ if (velopack_is_update_pending())
+ {
+ LL_INFOS("AppInit") << "Applying pending Velopack update on shutdown..." << LL_ENDL;
+ velopack_apply_pending_update(velopack_should_restart_after_update());
+ }
+ velopack_cleanup();
+#endif
+
//ditch LLVOAvatarSelf instance
gAgentAvatarp = NULL;
@@ -3147,7 +3107,7 @@ bool LLAppViewer::initWindow()
LLViewerWindow::Params window_params;
window_params
.title(gWindowTitle)
- .name(VIEWER_WINDOW_CLASSNAME)
+ .name(sWindowClass)
.x(gSavedSettings.getS32("WindowX"))
.y(gSavedSettings.getS32("WindowY"))
.width(gSavedSettings.getU32("WindowWidth"))
@@ -3272,16 +3232,6 @@ bool LLAppViewer::initWindow()
return true;
}
-bool LLAppViewer::isUpdaterMissing()
-{
- return mUpdaterNotFound;
-}
-
-bool LLAppViewer::waitForUpdater()
-{
- return !gSavedSettings.getBOOL("CmdLineSkipUpdater") && !mUpdaterNotFound && !gNonInteractive;
-}
-
void LLAppViewer::writeDebugInfo(bool isStatic)
{
#if LL_WINDOWS && LL_BUGSPLAT
diff --git a/indra/newview/llappviewer.h b/indra/newview/llappviewer.h
index 6b0d3e0b27c..cbe8be7741a 100644
--- a/indra/newview/llappviewer.h
+++ b/indra/newview/llappviewer.h
@@ -117,9 +117,6 @@ class LLAppViewer : public LLApp
bool quitRequested() { return mQuitRequested; }
bool logoutRequestSent() { return mLogoutRequestSent; }
bool isSecondInstance() { return mSecondInstance; }
- bool isUpdaterMissing(); // In use by tests
- bool waitForUpdater();
-
void writeDebugInfo(bool isStatic=true);
void setServerReleaseNotesURL(const std::string& url) { mServerReleaseNotesURL = url; }
@@ -287,6 +284,14 @@ class LLAppViewer : public LLApp
virtual void sendOutOfDiskSpaceNotification();
+protected:
+
+ // NSIS relies on this to detect if viewer is up.
+ // NSIS's method is somewhat unreliable since window
+ // can close long before cleanup is done.
+ // sendURLToOtherInstance also relies on this to detect if viewer is up.
+ static constexpr const char* sWindowClass = "Second Life";
+
private:
bool doFrame();
@@ -327,7 +332,6 @@ class LLAppViewer : public LLApp
static LLAppViewer* sInstance;
bool mSecondInstance; // Is this a second instance of the app?
- bool mUpdaterNotFound; // True when attempt to start updater failed
std::string mMarkerFileName;
LLAPRFile mMarkerFile; // A file created to indicate the app is running.
diff --git a/indra/newview/llappviewerwin32.cpp b/indra/newview/llappviewerwin32.cpp
index 0620b625d9b..94a5f7951e4 100644
--- a/indra/newview/llappviewerwin32.cpp
+++ b/indra/newview/llappviewerwin32.cpp
@@ -72,6 +72,11 @@
#include
#include
+// Velopack installer and update framework
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
+
// Bugsplat (http://bugsplat.com) crash reporting tool
#ifdef LL_BUGSPLAT
#include "BugSplat.h"
@@ -220,7 +225,6 @@ LONG WINAPI catchallCrashHandler(EXCEPTION_POINTERS * /*ExceptionInfo*/)
return 0;
}
-const std::string LLAppViewerWin32::sWindowClass = "Second Life";
/*
This function is used to print to the command line a text message
@@ -424,6 +428,31 @@ int APIENTRY WINMAIN(HINSTANCE hInstance,
PWSTR pCmdLine,
int nCmdShow)
{
+#if LL_VELOPACK
+ // Velopack MUST be initialized first - it may handle install/uninstall
+ // commands and exit the process before we do anything else.
+ if (!velopack_initialize())
+ {
+ // 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
+
// Call Tracy first thing to have it allocate memory
// https://github.com/wolfpld/tracy/issues/196
LL_PROFILER_FRAME_END;
@@ -933,7 +962,7 @@ bool LLAppViewerWin32::restoreErrorTrap()
bool LLAppViewerWin32::sendURLToOtherInstance(const std::string& url)
{
wchar_t window_class[256]; /* Flawfinder: ignore */ // Assume max length < 255 chars.
- mbstowcs(window_class, sWindowClass.c_str(), 255);
+ mbstowcs(window_class, sWindowClass, 255);
window_class[255] = 0;
// Use the class instead of the window name.
HWND other_window = FindWindow(window_class, NULL);
diff --git a/indra/newview/llappviewerwin32.h b/indra/newview/llappviewerwin32.h
index 3fad53ec72b..0741758a0c6 100644
--- a/indra/newview/llappviewerwin32.h
+++ b/indra/newview/llappviewerwin32.h
@@ -59,8 +59,6 @@ class LLAppViewerWin32 : public LLAppViewer
std::string generateSerialNumber();
- static const std::string sWindowClass;
-
private:
void disableWinErrorReporting();
diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp
index e9d68723d37..0358233637e 100644
--- a/indra/newview/lllogininstance.cpp
+++ b/indra/newview/lllogininstance.cpp
@@ -277,11 +277,6 @@ void LLLoginInstance::constructAuthParams(LLPointer user_credentia
mRequestData["params"] = request_params;
mRequestData["options"] = requested_options;
mRequestData["http_params"] = http_params;
-#if LL_RELEASE_FOR_DOWNLOAD
- mRequestData["wait_for_updater"] = LLAppViewer::instance()->waitForUpdater();
-#else
- mRequestData["wait_for_updater"] = false;
-#endif
}
bool LLLoginInstance::handleLoginEvent(const LLSD& event)
@@ -316,13 +311,6 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
// Login has failed.
// Figure out why and respond...
LLSD response = event["data"];
- LLSD updater = response["updater"];
-
- // Always provide a response to the updater, if in fact the updater
- // contacted us, if in fact the ping contains a 'reply' key. Most code
- // paths tell it not to proceed with updating.
- ResponsePtr resp(std::make_shared
- (LLSDMap("update", false), updater));
std::string reason_response = response["reason"].asString();
std::string message_response = response["message"].asString();
@@ -385,26 +373,15 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
}
else if(reason_response == "update")
{
- // This can happen if the user clicked Login quickly, before we heard
- // back from the Viewer Version Manager, but login failed because
- // login.cgi is insisting on a required update. We were called with an
- // event that bundles both the login.cgi 'response' and the
- // synchronization event from the 'updater'.
+ // login.cgi rejected login and requires an update. Since Velopack
+ // handles updates now, the best we can do here is tell the user
+ // to download the update manually via the release notes URL.
std::string login_version = response["message_args"]["VERSION"];
- std::string vvm_version = updater["VERSION"];
- std::string relnotes = updater["URL"];
LL_WARNS("LLLogin") << "Login failed because an update to version " << login_version << " is required." << LL_ENDL;
- // vvm_version might be empty because we might not have gotten
- // SLVersionChecker's LoginSync handshake. But if it IS populated, it
- // should (!) be the same as the version we got from login.cgi.
- if ((! vvm_version.empty()) && vvm_version != login_version)
- {
- LL_WARNS("LLLogin") << "VVM update version " << vvm_version
- << " differs from login version " << login_version
- << "; presenting VVM version to match release notes URL"
- << LL_ENDL;
- login_version = vvm_version;
- }
+
+ // Try to use the release notes URL from the VVM query if available,
+ // otherwise fall back to constructing one from the version.
+ std::string relnotes = LLVersionInfo::instance().getReleaseNotes();
if (relnotes.empty() || relnotes.find("://") == std::string::npos)
{
relnotes = LLTrans::getString("RELEASE_NOTES_BASE_URL");
@@ -420,32 +397,11 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
args["VERSION"] = login_version;
args["URL"] = relnotes;
- if (updater.isUndefined())
- {
- // If the updater failed to shake hands, better advise the user to
- // download the update him/herself.
- LLNotificationsUtil::add(
- "RequiredUpdate",
- args,
- updater,
- boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
- }
- else
- {
- // If we've heard from the updater that an update is required,
- // then display the prompt that assures the user we'll take care
- // of it. This is the one case in which we bind 'resp':
- // instead of destroying our Response object (and thus sending a
- // negative reply to the updater) as soon as we exit this
- // function, bind our shared_ptr so it gets passed into
- // syncWithUpdater. That ensures that the response is delayed
- // until the user has responded to the notification.
- LLNotificationsUtil::add(
- "PauseForUpdate",
- args,
- updater,
- boost::bind(&LLLoginInstance::syncWithUpdater, this, resp, _1, _2));
- }
+ LLNotificationsUtil::add(
+ "RequiredUpdate",
+ args,
+ LLSD(),
+ boost::bind(&LLLoginInstance::handleLoginDisallowed, this, _1, _2));
}
else if(reason_response == "mfa_challenge")
{
@@ -479,19 +435,6 @@ void LLLoginInstance::handleLoginFailure(const LLSD& event)
}
}
-void LLLoginInstance::syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response)
-{
- LL_INFOS("LLLogin") << "LLLoginInstance::syncWithUpdater" << LL_ENDL;
- // 'resp' points to an instance of LLEventAPI::Response that will be
- // destroyed as soon as we return and the notification response functor is
- // unregistered. Modify it so that it tells the updater to go ahead and
- // perform the update. Naturally, if we allowed the user a choice as to
- // whether to proceed or not, this assignment would reflect the user's
- // selection.
- (*resp)["update"] = true;
- attemptComplete();
-}
-
void LLLoginInstance::handleLoginDisallowed(const LLSD& notification, const LLSD& response)
{
attemptComplete();
diff --git a/indra/newview/lllogininstance.h b/indra/newview/lllogininstance.h
index 54ce51720f7..551ad92d336 100644
--- a/indra/newview/lllogininstance.h
+++ b/indra/newview/lllogininstance.h
@@ -28,8 +28,6 @@
#define LL_LLLOGININSTANCE_H
#include "lleventdispatcher.h"
-#include "lleventapi.h"
-#include // std::shared_ptr
#include "llsecapi.h"
class LLLogin;
class LLEventStream;
@@ -72,10 +70,7 @@ class LLLoginInstance : public LLSingleton
void saveMFAHash(LLSD const& response);
private:
- typedef std::shared_ptr ResponsePtr;
void constructAuthParams(LLPointer user_credentials);
- void updateApp(bool mandatory, const std::string& message);
- bool updateDialogCallback(const LLSD& notification, const LLSD& response);
bool handleLoginEvent(const LLSD& event);
void handleLoginFailure(const LLSD& event);
@@ -83,7 +78,6 @@ class LLLoginInstance : public LLSingleton
void handleDisconnect(const LLSD& event);
void handleIndeterminate(const LLSD& event);
void handleLoginDisallowed(const LLSD& notification, const LLSD& response);
- void syncWithUpdater(ResponsePtr resp, const LLSD& notification, const LLSD& response);
bool handleTOSResponse(bool v, const std::string& key);
void showMFAChallange(const std::string& message);
diff --git a/indra/newview/llpanellogin.cpp b/indra/newview/llpanellogin.cpp
index fe9145bf712..0a585722f24 100644
--- a/indra/newview/llpanellogin.cpp
+++ b/indra/newview/llpanellogin.cpp
@@ -37,6 +37,9 @@
#include "llappviewer.h"
#include "llbutton.h"
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
#include "llcheckboxctrl.h"
#include "llcommandhandler.h" // for secondlife:///app/login/
#include "llcombobox.h"
@@ -936,6 +939,19 @@ void LLPanelLogin::handleMediaEvent(LLPluginClassMedia* /*self*/, EMediaEvent ev
// static
void LLPanelLogin::onClickConnect(bool commit_fields)
{
+#if LL_VELOPACK
+ // In theory, you should never be able to get here.
+ // If there's a required update, try as you might you're not supposed to actually close the downloading update dialog.
+ // But just in case...
+ if (velopack_is_required_update_in_progress())
+ {
+ LLSD args;
+ args["VERSION"] = velopack_get_required_update_version();
+ LLNotificationsUtil::add("DownloadingUpdate", args);
+ return;
+ }
+#endif
+
if (sInstance && sInstance->mCallback)
{
if (commit_fields)
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index 59d97943e3c..c23b493ad07 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -29,6 +29,11 @@
#include "llappviewer.h"
#include "llstartup.h"
+#if LL_VELOPACK && LL_WINDOWS
+#include "llvelopack.h"
+#include
+#endif
+
#if LL_WINDOWS
# include // _spawnl()
#else
@@ -266,6 +271,7 @@ std::unique_ptr LLStartUp::sPhases(new LLViewerStats::P
void login_show();
void login_callback(S32 option, void* userdata);
+void uninstall_nsis_if_required();
void show_release_notes_if_required();
void show_first_run_dialog();
bool first_run_dialog_callback(const LLSD& notification, const LLSD& response);
@@ -921,6 +927,7 @@ bool idle_startup()
LL_DEBUGS("AppInit") << "PeekMessage processed" << LL_ENDL;
#endif
do_startup_frame();
+ uninstall_nsis_if_required();
timeout.reset();
return false;
}
@@ -2605,6 +2612,67 @@ void release_notes_coro(const std::string url)
LLWeb::loadURLInternal(url);
}
+/**
+* Check if this is a fresh velopack install and
+* if uninstallation of old viewer is needed.
+*/
+void uninstall_nsis_if_required()
+{
+#if LL_VELOPACK && LL_WINDOWS
+ bool checked_for_legacy_install = gSavedSettings.getBOOL("PreviousInstallChecked");
+ if (checked_for_legacy_install)
+ {
+ return;
+ }
+ gSavedSettings.setBOOL("PreviousInstallChecked", true);
+
+ LL_INFOS() << "Looking for previous NSIS installs" << LL_ENDL;
+
+ S32 found_major = 0;
+ S32 found_minor = 0;
+ S32 found_patch = 0;
+ U64 found_build = 0;
+
+ if (!get_nsis_version(found_major, found_minor, found_patch, found_build))
+ {
+ return;
+ }
+
+ LLVersionInfo* ver_inst = LLVersionInfo::getInstance();
+
+ if (found_major > ver_inst->getMajor())
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+ return;
+ }
+
+ if (found_major == ver_inst->getMajor()
+ && found_minor > ver_inst->getMinor())
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+ return;
+ }
+
+ if (found_major == ver_inst->getMajor()
+ && found_minor == ver_inst->getMinor()
+ && found_patch > ver_inst->getPatch())
+ {
+ LL_INFOS() << "Found installed nsis version that is newer" << found_major << "." << found_minor << "." << found_patch << "." << found_build << LL_ENDL;
+ return;
+ }
+
+ // Assume that nsis is going to be something like x.x.x, while velopack is x.x.(x+1),
+ // 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();
+
+ LLSD args;
+ args["VERSION"] = llformat("%d.%d.%d", found_major, found_minor, found_patch);
+ LLNotificationsUtil::add("FoundLegacyNsisInstallation", args);
+#endif
+}
+
void validate_release_notes_coro(const std::string url)
{
LLVersionInfo& versionInfo(LLVersionInfo::instance());
@@ -2638,15 +2706,24 @@ void show_release_notes_if_required()
// below. If viewer release notes stop working, might be because that
// LLEventMailDrop got moved out of LLVersionInfo and hasn't yet been
// instantiated.
- if (!release_notes_shown && (LLVersionInfo::instance().getChannelAndVersion() != gLastRunVersion)
- && LLVersionInfo::instance().getViewerMaturity() != LLVersionInfo::TEST_VIEWER // don't show Release Notes for the test builds
- && gSavedSettings.getBOOL("UpdaterShowReleaseNotes")
- && !gSavedSettings.getBOOL("FirstLoginThisInstall"))
+ if (release_notes_shown
+ || LLVersionInfo::instance().getChannelAndVersion() == gLastRunVersion
+ || gSavedSettings.getBOOL("FirstLoginThisInstall")) // New users don't need to see release notes
+ {
+ return;
+ }
+ S32 mode = gSavedSettings.getS32("UpdaterShowReleaseNotes");
+ if (mode == 0)
+ {
+ return;
+ }
+ if (mode == 2 // Show even for test builds
+ || LLVersionInfo::instance().getViewerMaturity() != LLVersionInfo::TEST_VIEWER) // don't show Release Notes for the test builds
+
{
#if LL_RELEASE_FOR_DOWNLOAD
- if (!gSavedSettings.getBOOL("CmdLineSkipUpdater")
- && !LLAppViewer::instance()->isUpdaterMissing())
+ if (!gSavedSettings.getBOOL("CmdLineSkipUpdater"))
{
// Instantiate a "relnotes" listener which assumes any arriving event
// is the release notes URL string. Since "relnotes" is an
diff --git a/indra/newview/llvelopack.cpp b/indra/newview/llvelopack.cpp
new file mode 100644
index 00000000000..28e989c4bae
--- /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
diff --git a/indra/newview/llvelopack.h b/indra/newview/llvelopack.h
new file mode 100644
index 00000000000..d04d0db7dce
--- /dev/null
+++ b/indra/newview/llvelopack.h
@@ -0,0 +1,59 @@
+/**
+ * @file llvelopack.h
+ * @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$
+ */
+
+#ifndef LL_LLVELOPACK_H
+#define LL_LLVELOPACK_H
+
+#if LL_VELOPACK
+
+#include
+#include
+
+bool velopack_initialize();
+void velopack_check_for_updates(const std::string& required_version, const std::string& relnotes_url);
+std::string velopack_get_current_version();
+bool velopack_is_update_pending();
+bool velopack_is_required_update_in_progress();
+std::string velopack_get_required_update_version();
+bool velopack_should_restart_after_update();
+void velopack_request_restart_after_update();
+void velopack_apply_pending_update(bool restart = true);
+void velopack_set_update_url(const std::string& url);
+void velopack_set_progress_callback(std::function callback);
+void velopack_cleanup();
+
+#if LL_WINDOWS
+void clear_nsis_links();
+bool get_nsis_version(
+ int& nsis_major,
+ int& nsis_minor,
+ int& nsis_patch,
+ uint64_t& nsis_build);
+#endif
+
+#endif // LL_VELOPACK
+#endif
+// EOF
diff --git a/indra/newview/llversioninfo.cpp b/indra/newview/llversioninfo.cpp
index 4e8320b72a4..178f10257d7 100644
--- a/indra/newview/llversioninfo.cpp
+++ b/indra/newview/llversioninfo.cpp
@@ -54,7 +54,7 @@ LLVersionInfo::LLVersionInfo():
mWorkingChannelName(LL_TO_STRING(LL_VIEWER_CHANNEL)),
build_configuration(LLBUILD_CONFIG), // set in indra/cmake/BuildVersion.cmake
// instantiate an LLEventMailDrop with canonical name to listen for news
- // from SLVersionChecker
+ // from the Viewer Version Manager
mPump{new LLEventMailDrop("relnotes")},
// immediately listen on mPump, store arriving URL into mReleaseNotes
mStore{new LLStoreListener(*mPump, mReleaseNotes)}
diff --git a/indra/newview/llversioninfo.h b/indra/newview/llversioninfo.h
index 237b37a084c..a2b93597e66 100644
--- a/indra/newview/llversioninfo.h
+++ b/indra/newview/llversioninfo.h
@@ -112,8 +112,8 @@ class LLVersionInfo: public LLSingleton
std::string mReleaseNotes;
// Store unique_ptrs to the next couple things so we don't have to explain
// to every consumer of this header file all the details of each.
- // mPump is the LLEventMailDrop on which we listen for SLVersionChecker to
- // post the release-notes URL from the Viewer Version Manager.
+ // mPump is the LLEventMailDrop on which we listen for the
+ // release-notes URL from the Viewer Version Manager.
std::unique_ptr mPump;
// mStore is an adapter that stores the release-notes URL in mReleaseNotes.
std::unique_ptr> mStore;
diff --git a/indra/newview/llviewernetwork.cpp b/indra/newview/llviewernetwork.cpp
index 890580ddff2..6cb3aee20cc 100644
--- a/indra/newview/llviewernetwork.cpp
+++ b/indra/newview/llviewernetwork.cpp
@@ -575,6 +575,7 @@ std::string LLGridManager::getGridLoginID()
std::string LLGridManager::getUpdateServiceURL()
{
+ auto env_update_service = LLStringUtil::getoptenv("SL_UPDATE_SERVICE");
std::string update_url_base = gSavedSettings.getString("CmdLineUpdateService");;
if ( !update_url_base.empty() )
{
@@ -582,6 +583,13 @@ std::string LLGridManager::getUpdateServiceURL()
<< "Update URL base overridden from command line: " << update_url_base
<< LL_ENDL;
}
+ else if (env_update_service && env_update_service->find("http") != std::string::npos)
+ {
+ update_url_base = *env_update_service;
+ LL_INFOS("UpdaterService", "GridManager")
+ << "Update URL base overridden from SL_UPDATE_SERVICE environment variable: " << update_url_base
+ << LL_ENDL;
+ }
else if ( mGridList[mGrid].has(GRID_UPDATE_SERVICE_URL) )
{
update_url_base = mGridList[mGrid][GRID_UPDATE_SERVICE_URL].asString();
diff --git a/indra/newview/llviewerstats.cpp b/indra/newview/llviewerstats.cpp
index d39d4662053..5193514fe8a 100644
--- a/indra/newview/llviewerstats.cpp
+++ b/indra/newview/llviewerstats.cpp
@@ -783,7 +783,11 @@ void send_viewer_stats(bool include_preferences)
fail["failed_resends"] = (S32) gMessageSystem->mFailedResendPackets;
fail["off_circuit"] = (S32) gMessageSystem->mOffCircuitPackets;
fail["invalid"] = (S32) gMessageSystem->mInvalidOnCircuitPackets;
- fail["missing_updater"] = (S32) LLAppViewer::instance()->isUpdaterMissing();
+#if LL_VELOPACK
+ fail["missing_updater"] = false;
+#else
+ fail["missing_updater"] = true;
+#endif
LLSD &inventory = body["inventory"];
inventory["usable"] = gInventory.isInventoryUsable();
diff --git a/indra/newview/llvvmquery.cpp b/indra/newview/llvvmquery.cpp
new file mode 100644
index 00000000000..12dcc1d04dd
--- /dev/null
+++ b/indra/newview/llvvmquery.cpp
@@ -0,0 +1,189 @@
+/**
+ * @file llvvmquery.cpp
+ * @brief Query the Viewer Version Manager (VVM) for update information
+ *
+ * $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$
+ */
+
+#include "llviewerprecompiledheaders.h"
+#include "llvvmquery.h"
+
+#include "llcorehttputil.h"
+#include "llcoros.h"
+#include "llevents.h"
+#include "llviewernetwork.h"
+#include "llversioninfo.h"
+#include "llviewercontrol.h"
+#include "llhasheduniqueid.h"
+#include "lluri.h"
+#include "llsys.h"
+
+#if LL_VELOPACK
+#include "llvelopack.h"
+#endif
+
+namespace
+{
+ std::string get_platform_string()
+ {
+#if LL_WINDOWS
+ return "win64";
+#elif LL_DARWIN
+ return "mac64";
+#elif LL_LINUX
+ return "lnx64";
+#else
+ return "unknown";
+#endif
+ }
+
+ std::string get_platform_version()
+ {
+ return LLOSInfo::instance().getOSVersionString();
+ }
+
+ std::string get_machine_id()
+ {
+ unsigned char id[MD5HEX_STR_SIZE];
+ if (llHashedUniqueID(id))
+ {
+ return std::string(reinterpret_cast(id));
+ }
+ return "unknown";
+ }
+
+ void query_vvm_coro()
+ {
+ // Get base URL from grid manager
+ std::string base_url = LLGridManager::getInstance()->getUpdateServiceURL();
+
+ // We use this for dev testing when working with VVM and working on the updater. Not advisable to uncomment it.
+ //std::string base_url = "https://update.qa.secondlife.io/update";
+
+ if (base_url.empty())
+ {
+ LL_WARNS("VVM") << "No update service URL configured" << LL_ENDL;
+ return;
+ }
+
+ // Gather parameters for VVM query
+ std::string channel = LLVersionInfo::instance().getChannel();
+
+ // We use this for dev testing when working with VVM and working on the updater. Not advisable to uncomment it.
+ // std::string channel = "QA Target for Velopack";
+
+ std::string version = LLVersionInfo::instance().getVersion();
+ std::string platform = get_platform_string();
+ std::string platform_version = get_platform_version();
+ std::string test_ok = gSavedSettings.getBOOL("UpdaterWillingToTest") ? "testok" : "testno";
+ std::string machine_id = get_machine_id();
+
+ // Build URL: {base}/v1.2/{channel}/{version}/{platform}/{platform_version}/{testok}/{uuid}
+ std::string url = base_url + "/v1.2/" +
+ LLURI::escape(channel) + "/" +
+ LLURI::escape(version) + "/" +
+ platform + "/" +
+ LLURI::escape(platform_version) + "/" +
+ test_ok + "/" +
+ machine_id;
+
+ LL_INFOS("VVM") << "Querying VVM: " << url << LL_ENDL;
+
+ // Make HTTP GET request
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t adapter =
+ std::make_shared("VVMQuery", httpPolicy);
+ LLCore::HttpRequest::ptr_t request = std::make_shared();
+
+ LLSD result = adapter->getAndSuspend(request, url);
+
+ // Check HTTP status
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+ if (!status)
+ {
+ if (status.getType() == 404)
+ {
+ LL_INFOS("VVM") << "Unmanaged channel, no updates available" << LL_ENDL;
+ return;
+ }
+ LL_WARNS("VVM") << "VVM query failed: " << status.toString() << LL_ENDL;
+ return;
+ }
+
+ // Read whether this update is required or optional
+ bool update_required = result["required"].asBoolean();
+ std::string relnotes = result["more_info"].asString();
+
+ // Extract update URL for current platform
+ LLSD platforms = result["platforms"];
+ if (platforms.has(platform))
+ {
+ std::string update_url = platforms[platform]["url"].asString();
+#if LL_VELOPACK
+ std::string velopack_url = platforms[platform]["velopack_url"].asString();
+ U32 updater_service = gSavedSettings.getU32("UpdaterServiceSetting");
+ std::string required_version = update_required ? result["version"].asString() : "";
+ // Skip network check if no required version AND user only wants mandatory updates
+ if (!velopack_url.empty() && (update_required || updater_service != 0))
+ {
+ LL_INFOS("VVM") << "Velopack feed URL: " << velopack_url
+ << " required_version: " << required_version << LL_ENDL;
+ velopack_set_update_url(velopack_url);
+
+ LLCoros::instance().launch("VelopackUpdateCheck",
+ [required_version, relnotes]()
+ {
+ velopack_check_for_updates(required_version, relnotes);
+ });
+ }
+ else if (!velopack_url.empty())
+ {
+ LL_INFOS("VVM") << "Optional update skipped (UpdaterServiceSetting=0)" << LL_ENDL;
+ }
+ else
+#endif
+ if (!update_url.empty())
+ {
+ LL_INFOS("VVM") << "Update available at: " << update_url << LL_ENDL;
+ }
+ }
+ else
+ {
+ LL_INFOS("VVM") << "No update available for platform: " << platform << LL_ENDL;
+ }
+
+ // Post release notes URL to the relnotes event pump
+ if (!relnotes.empty())
+ {
+ LL_INFOS("VVM") << "Release notes URL: " << relnotes << LL_ENDL;
+ LLEventPumps::instance().obtain("relnotes").post(relnotes);
+ }
+ }
+}
+
+void initVVMUpdateCheck()
+{
+ LL_INFOS("VVM") << "Initializing VVM update check" << LL_ENDL;
+ LLCoros::instance().launch("VVMUpdateCheck", &query_vvm_coro);
+}
diff --git a/indra/newview/llvvmquery.h b/indra/newview/llvvmquery.h
new file mode 100644
index 00000000000..977d82af643
--- /dev/null
+++ b/indra/newview/llvvmquery.h
@@ -0,0 +1,42 @@
+/**
+ * @file llvvmquery.h
+ * @brief Query the Viewer Version Manager (VVM) for update information
+ *
+ * $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$
+ */
+
+#ifndef LL_LLVVMQUERY_H
+#define LL_LLVVMQUERY_H
+
+/**
+ * Initialize the VVM update check.
+ *
+ * This launches a coroutine that queries the Viewer Version Manager (VVM)
+ * to check for available updates. If an update is available, it configures
+ * Velopack with the update URL and initiates the update check/download.
+ *
+ * The release notes URL from the VVM response is posted to the "relnotes"
+ * event pump for display.
+ */
+void initVVMUpdateCheck();
+
+#endif // LL_LLVVMQUERY_H
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index 82e2229d76d..3e3baa7e98d 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -200,6 +200,16 @@ No tutorial is currently available.
yestext="OK"/>
+
+ [APP_NAME] found an installation of an older version [VERSION]. To uninstall the older version, please follow [https://community.secondlife.com/knowledgebase/english/how-to-uninstall-and-reinstall-second-life-r524 this manual].
+
+
+
+
+Downloading update [VERSION]...
+The viewer will restart once the download is complete.
+
+
3 and self.args['version'][3]:
+ pack_version += '-' + self.args['version'][3]
+ pack_title = self.app_name() # Display name with spaces
+ pack_dir = self.get_dst_prefix()
+ main_exe = self.final_exe()
+ installer_base = self.installer_base_name()
+ exclude_pattern = r'.*\.pdb|.*\.map|.*\.bat|.*\.exp|.*\.lib|.*\.nsi|.*\.tar\.xz|secondlife-bin\..*|.*_Setup\.exe|.*-Setup\.exe'
+
+ # Channel-specific icon for the Velopack installer.
+ # CMake copies icons/{channel}/secondlife.ico to res/ll_icon.ico at configure time.
+ # Try the CMake-generated copy first, fall back to the source icon.
+ icon_path = os.path.join(self.get_src_prefix(), 'res', 'll_icon.ico')
+ if not os.path.exists(icon_path):
+ icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.ico')
+
+ # In CI, defer Velopack packaging to the sign step where Azure credentials
+ # are available. Emit metadata as GitHub outputs so the sign step can run
+ # vpk pack with --signTemplate, producing a package with signed executables.
+ if os.getenv('GITHUB_ACTIONS'):
+ # Copy the icon into pack_dir so it's included in the Windows-app artifact
+ icon_filename = ''
+ if os.path.exists(icon_path):
+ icon_filename = os.path.basename(icon_path)
+ icon_dest = os.path.join(pack_dir, icon_filename)
+ shutil.copy2(icon_path, icon_dest)
+ print("Copied icon %s to %s" % (icon_path, icon_dest))
+ else:
+ print("WARNING: Icon not found at %s" % icon_path)
+
+ # Emit metadata for the sign step
+ self.set_github_output('velopack_pack_id', pack_id)
+ self.set_github_output('velopack_pack_version', pack_version)
+ self.set_github_output('velopack_pack_title', pack_title)
+ self.set_github_output('velopack_main_exe', main_exe)
+ self.set_github_output('velopack_icon', icon_filename)
+ self.set_github_output('velopack_installer_base', installer_base)
+ self.set_github_output('velopack_exclude', exclude_pattern)
+ # Set package_file so llmanifest's touched.bat logic doesn't crash
+ self.package_file = installer_base + '_Setup.exe'
+ print("CI mode: Velopack packaging deferred to sign step")
+ return
+
+ # Local builds: run vpk pack directly (unsigned)
+ vpk_args = [
+ 'vpk', 'pack',
+ '--packId', pack_id,
+ '--packVersion', pack_version,
+ '--packDir', pack_dir,
+ '--mainExe', main_exe,
+ '--packTitle', pack_title,
+ '--exclude', exclude_pattern,
+ # Suppress Velopack's built-in shortcut creation; we create our own
+ # shortcuts in llvelopack.cpp on_after_install hook instead.
+ '--shortcuts', '',
+ ]
+
+ # Add icon — CMake copies the channel-appropriate secondlife.ico to res/ll_icon.ico
+ if os.path.exists(icon_path):
+ print("Using icon: %s" % icon_path)
+ vpk_args.extend(['--icon', icon_path])
+ else:
+ print("WARNING: Icon not found at %s — Setup.exe will have no icon" % icon_path)
+
+ print("Running Velopack packaging: %s" % ' '.join(vpk_args))
+
+ # Run vpk command
+ import subprocess
+ result = subprocess.run(vpk_args, cwd=os.path.dirname(pack_dir), capture_output=True, text=True)
+ if result.stdout:
+ print("vpk stdout: %s" % result.stdout)
+ if result.stderr:
+ print("vpk stderr: %s" % result.stderr)
+ if result.returncode != 0:
+ raise ManifestError("Velopack packaging failed with code %d" % result.returncode)
+
+ # Velopack outputs to a Releases directory
+ releases_dir = os.path.join(os.path.dirname(pack_dir), 'Releases')
+
+ # Move the setup exe INTO pack_dir so it's included in the Windows-app artifact
+ # IMPORTANT: Use hyphen format (-Setup.exe) to avoid the *_Setup.exe exclusion pattern
+ # in viewer_app output (line ~538). The underscore pattern excludes NSIS installers
+ # which are rebuilt during signing, but Velopack installers are created here.
+ # Velopack creates: {packId}-win-Setup.exe
+ velopack_setup = os.path.join(releases_dir, '%s-win-Setup.exe' % pack_id)
+ self.package_file = installer_base + '_Setup.exe'
+ our_setup = os.path.join(pack_dir, self.package_file)
+ if os.path.exists(velopack_setup):
+ shutil.move(velopack_setup, our_setup)
+ print("Moved %s to %s" % (velopack_setup, our_setup))
+
+ # Rename the portable zip to include the version number
+ # Velopack creates: {packId}-win-Portable.zip
+ velopack_portable = os.path.join(releases_dir, '%s-win-Portable.zip' % pack_id)
+ if os.path.exists(velopack_portable):
+ our_portable = os.path.join(releases_dir, installer_base + '_Portable.zip')
+ shutil.move(velopack_portable, our_portable)
+ print("Moved %s to %s" % (velopack_portable, our_portable))
+
+ # Output the Releases directory path for artifact upload (contains nupkg, RELEASES for updates)
+ self.set_github_output('velopack_releases', releases_dir)
+
+ def nsis_package_finish(self):
+ """Package the viewer using NSIS installer (legacy)"""
# a standard map of strings for replacing in the templates
substitution_strings = {
'version' : '.'.join(self.args['version']),
@@ -781,7 +889,7 @@ def package_finish(self):
substitution_strings['installer_file'] = installer_file
version_vars = """
- !define INSTEXE "SLVersionChecker.exe"
+ !define INSTEXE "%(final_exe)s"
!define VERSION "%(version_short)s"
!define VERSION_LONG "%(version)s"
!define VERSION_DASHES "%(version_dashes)s"
@@ -967,15 +1075,6 @@ def construct(self):
with self.prefix(src=self.icon_path(), dst="") :
self.path("secondlife.icns")
- # Copy in the updater script and helper modules
- self.path(src=os.path.join(pkgdir, 'VMP'), dst="updater")
-
- with self.prefix(src="", dst=os.path.join("updater", "icons")):
- self.path2basename(self.icon_path(), "secondlife.ico")
- with self.prefix(src="vmp_icons", dst=""):
- self.path("*.png")
- self.path("*.gif")
-
with self.prefix(src_dst="cursors_mac"):
self.path("*.tif")
@@ -1127,6 +1226,123 @@ def package_finish(self):
arcname=self.app_name() + ".app")
self.set_github_output_path('viewer_app', tarpath)
+ # Generate Velopack update packages if enabled
+ # This creates the nupkg and RELEASES files needed for auto-updates
+ # Distribution is still via DMG, but updates use Velopack
+ if self.args.get('velopack', 'OFF') == 'ON':
+ self.velopack_package_finish()
+
+ def velopack_package_finish(self):
+ """Generate Velopack update packages for macOS.
+
+ This creates the nupkg and releases.json files needed for auto-updates.
+ Distribution is still via DMG - Velopack only handles the update infrastructure.
+ """
+ # packId determines install identification - same as Windows for consistency
+ pack_id = self.app_name_oneword() # "SecondLife", "SecondLifeBeta", etc.
+ # Velopack requires SemVer2. Use major.minor.patch-buildnumber so that
+ # Velopack can distinguish builds and order them correctly.
+ pack_version = '.'.join(self.args['version'][:3])
+ if len(self.args['version']) > 3 and self.args['version'][3]:
+ pack_version += '-' + self.args['version'][3]
+ pack_title = self.app_name() # Display name with spaces
+
+ # The .app bundle path (e.g., "/path/to/Second Life Release.app")
+ app_bundle = self.get_dst_prefix()
+ # Bundle ID from args (e.g., "com.secondlife.viewer")
+ bundle_id = self.args.get('bundleid', 'com.secondlife.indra.viewer')
+
+ # Icon path for macOS
+ icon_path = os.path.join(self.get_src_prefix(), self.icon_path(), 'secondlife.icns')
+
+ # The main executable inside Contents/MacOS/ is named after the channel
+ main_exe = self.channel()
+
+ # In CI, defer Velopack packaging to the sign step where code signing
+ # credentials are available. Emit metadata as GitHub outputs so the
+ # sign step can run vpk pack after signing the app bundle.
+ if os.getenv('GITHUB_ACTIONS'):
+ self.set_github_output('velopack_mac_pack_id', pack_id)
+ self.set_github_output('velopack_mac_pack_version', pack_version)
+ self.set_github_output('velopack_mac_pack_title', pack_title)
+ self.set_github_output('velopack_mac_main_exe', main_exe)
+ self.set_github_output('velopack_mac_bundle_id', bundle_id)
+ print("CI mode: macOS Velopack packaging deferred to sign step")
+ return
+
+ # Local builds: run vpk pack directly (unsigned)
+
+ # Parent directory containing the .app bundle - this is where we run vpk from
+ # and where the Releases directory will be created
+ work_dir = os.path.dirname(app_bundle)
+
+ # Output directory for releases - clean it first to avoid version conflicts
+ releases_dir = os.path.join(work_dir, 'Releases')
+ if os.path.exists(releases_dir):
+ print("Cleaning existing Releases directory: %s" % releases_dir)
+ shutil.rmtree(releases_dir)
+
+ # Build vpk command for macOS
+ # See: https://docs.velopack.io/reference/cli/content/vpk-osx
+ vpk_args = [
+ 'vpk', 'pack',
+ '--packId', pack_id,
+ '--packVersion', pack_version,
+ '--packDir', app_bundle,
+ '--packTitle', pack_title,
+ '--mainExe', main_exe, # Executable name inside Contents/MacOS/
+ '--bundleId', bundle_id,
+ '--outputDir', releases_dir,
+ '--noInst', # Don't generate .pkg installer - we use DMG for distribution
+ '--verbose', # Show detailed output
+ ]
+
+ # Add icon if exists
+ if os.path.exists(icon_path):
+ vpk_args.extend(['--icon', icon_path])
+
+ print("Running Velopack packaging for macOS:")
+ print(" Command: %s" % ' '.join(vpk_args))
+ print(" Working directory: %s" % work_dir)
+ print(" App bundle: %s" % app_bundle)
+ print(" Main executable: %s" % main_exe)
+
+ # Run vpk command
+ result = subprocess.run(vpk_args, cwd=work_dir, capture_output=True, text=True)
+
+ # Always print output for debugging
+ if result.stdout:
+ print("vpk stdout:\n%s" % result.stdout)
+ if result.stderr:
+ print("vpk stderr:\n%s" % result.stderr)
+
+ if result.returncode != 0:
+ raise ManifestError("Velopack packaging failed with code %d" % result.returncode)
+
+ # Verify the Releases directory was created and contains expected files
+ if not os.path.exists(releases_dir):
+ raise ManifestError("Velopack releases directory not found: %s" % releases_dir)
+
+ # List what was created
+ releases_contents = os.listdir(releases_dir)
+ print("Velopack releases directory contents: %s" % releases_contents)
+
+ # Verify we have the expected files (nupkg and releases JSON)
+ nupkg_files = [f for f in releases_contents if f.endswith('.nupkg')]
+ json_files = [f for f in releases_contents if f.endswith('.json')]
+
+ if not nupkg_files:
+ raise ManifestError("No .nupkg files found in releases directory")
+ if not json_files:
+ raise ManifestError("No releases JSON files found in releases directory")
+
+ print("Generated %d nupkg file(s): %s" % (len(nupkg_files), nupkg_files))
+ print("Generated %d JSON file(s): %s" % (len(json_files), json_files))
+
+ # Output the Releases directory path for artifact upload
+ self.set_github_output('velopack_releases', releases_dir)
+ print("Velopack releases directory: %s" % releases_dir)
+
class LinuxManifest(ViewerManifest):
build_data_json_platform = 'lnx'
@@ -1324,6 +1540,7 @@ def construct(self):
dict(name='discord', description="""Indication discord social sdk libraries are needed""", default='OFF'),
dict(name='openal', description="""Indication openal libraries are needed""", default='OFF'),
dict(name='tracy', description="""Indication tracy profiler is enabled""", default='OFF'),
+ dict(name='velopack', description="""Use Velopack installer instead of NSIS""", default='OFF'),
]
try:
main(extra=extra_arguments)
diff --git a/indra/viewer_components/login/lllogin.cpp b/indra/viewer_components/login/lllogin.cpp
index 37b70964c3a..144f8078526 100644
--- a/indra/viewer_components/login/lllogin.cpp
+++ b/indra/viewer_components/login/lllogin.cpp
@@ -34,7 +34,6 @@
#include "llcoros.h"
#include "llevents.h"
-#include "lleventfilter.h"
#include "lleventcoro.h"
#include "llexception.h"
#include "stringize.h"
@@ -133,16 +132,6 @@ void LLLogin::Impl::connect(const std::string& uri, const LLSD& login_params)
LL_DEBUGS("LLLogin") << " connected with uri '" << uri << "', login_params " << login_params << LL_ENDL;
}
-namespace
-{
-// Instantiate this rendezvous point at namespace scope so it's already
-// present no matter how early the updater might post to it.
-// Use an LLEventMailDrop, which has future-like semantics: regardless of the
-// relative order in which post() or listen() are called, it delivers each
-// post() event to its listener(s) until one of them consumes that event.
-static LLEventMailDrop sSyncPoint("LoginSync");
-}
-
void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
{
LLSD printable_params = hidePasswd(login_params);
@@ -225,58 +214,7 @@ void LLLogin::Impl::loginCoro(std::string uri, LLSD login_params)
}
else
{
- // Synchronize here with the updater. We synchronize here rather
- // than in the fail.login handler, which actually examines the
- // response from login.cgi, because here we are definitely in a
- // coroutine and can definitely use suspendUntilBlah(). Whoever's
- // listening for fail.login might not be.
-
- // If the reason for login failure is that we must install a
- // required update, we definitely want to pass control to the
- // updater to manage that for us. We'll handle any other login
- // failure ourselves, as usual. We figure that no matter where you
- // are in the world, or what kind of network you're on, we can
- // reasonably expect the Viewer Version Manager to respond more or
- // less as quickly as login.cgi. This synchronization is only
- // intended to smooth out minor races between the two services.
- // But what if the updater crashes? Use a timeout so that
- // eventually we'll tire of waiting for it and carry on as usual.
- // Given the above, it can be a fairly short timeout, at least
- // from a human point of view.
-
- // Since sSyncPoint is an LLEventMailDrop, we DEFINITELY want to
- // consume the posted event.
- LLCoros::OverrideConsuming oc(true);
LLSD responses(mAuthResponse["responses"]);
- LLSD updater;
-
- if (printable_params["wait_for_updater"].asBoolean())
- {
- std::string reason_response = responses["data"]["reason"].asString();
- // Timeout should produce the isUndefined() object passed here.
- if (reason_response == "update")
- {
- LL_INFOS("LLLogin") << "Login failure, waiting for sync from updater" << LL_ENDL;
- updater = llcoro::suspendUntilEventOnWithTimeout(sSyncPoint, 10, LLSD());
- }
- else
- {
- LL_DEBUGS("LLLogin") << "Login failure, waiting for sync from updater" << LL_ENDL;
- updater = llcoro::suspendUntilEventOnWithTimeout(sSyncPoint, 3, LLSD());
- }
- if (updater.isUndefined())
- {
- LL_WARNS("LLLogin") << "Failed to hear from updater, proceeding with fail.login"
- << LL_ENDL;
- }
- else
- {
- LL_DEBUGS("LLLogin") << "Got responses from updater and login.cgi" << LL_ENDL;
- }
- }
-
- // Let the fail.login handler deal with empty updater response.
- responses["updater"] = updater;
sendProgressEvent("offline", "fail.login", responses);
}
return; // Done!