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 + + platforms + + windows64 + + archive + + creds + github + hash + 91abbc360640b5b2e0a4c001a36ad411a9a42602 + hash_algorithm + sha1 + url + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/380583560 + + name + windows64 + + darwin64 + + archive + + creds + github + hash + 05563a79bdeb83d66a72ac1e97587dc2a8f64511 + hash_algorithm + sha1 + url + https://api.github.com/repos/secondlife-3p/3p-velopack/releases/assets/380583554 + + name + darwin64 + + + license + MIT + license_file + LICENSES/velopack.txt + copyright + Velopack Ltd. + version + 40232ef.23500976684 + name + velopack + description + Velopack C/C++ Library + 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!