From 851bc1ba26ca74531f2012540ff480b17f31fd5c Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:38:26 -0400 Subject: [PATCH 1/8] Add Vivaldi as a fleet-maintained app (macOS) Adds Vivaldi browser as a Fleet Maintained App for macOS (darwin only). Windows is not included because Vivaldi stable is absent from the microsoft/winget-pkgs repository (only the Snapshot edition is in WinGet). macOS uses the enricher pattern to override Homebrew's tar.xz download with Vivaldi's direct DMG URL, keeping installer_format as "dmg" so the standard hdiutil-based install script is generated unchanged. --- .../ingesters/homebrew/external_refs/main.go | 1 + .../homebrew/external_refs/vivaldi.go | 16 +++++++++++++ .../inputs/homebrew/vivaldi.json | 8 +++++++ ee/maintained-apps/outputs/apps.json | 17 +++++++++----- .../outputs/vivaldi/darwin.json | 21 ++++++++++++++++++ .../SoftwarePage/components/icons/Vivaldi.tsx | 14 ++++++++++++ .../SoftwarePage/components/icons/index.ts | 2 ++ .../images/app-icon-vivaldi-60x60@2x.png | Bin 0 -> 18827 bytes 8 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 ee/maintained-apps/ingesters/homebrew/external_refs/vivaldi.go create mode 100644 ee/maintained-apps/inputs/homebrew/vivaldi.json create mode 100644 ee/maintained-apps/outputs/vivaldi/darwin.json create mode 100644 frontend/pages/SoftwarePage/components/icons/Vivaldi.tsx create mode 100644 website/assets/images/app-icon-vivaldi-60x60@2x.png diff --git a/ee/maintained-apps/ingesters/homebrew/external_refs/main.go b/ee/maintained-apps/ingesters/homebrew/external_refs/main.go index 8809d586b89..37cf5a7cad2 100644 --- a/ee/maintained-apps/ingesters/homebrew/external_refs/main.go +++ b/ee/maintained-apps/ingesters/homebrew/external_refs/main.go @@ -21,6 +21,7 @@ var Funcs = map[string][]func(*maintained_apps.FMAManifestApp) (*maintained_apps "parallels/darwin": {ParallelsVersionShortener}, "github/darwin": {GitHubDesktopVersionShortener}, "camtasia/darwin": {CamtasiaVersionTransformer}, + "vivaldi/darwin": {VivaldiDMGInstaller}, "warp/darwin": {WarpDirectInstaller}, } diff --git a/ee/maintained-apps/ingesters/homebrew/external_refs/vivaldi.go b/ee/maintained-apps/ingesters/homebrew/external_refs/vivaldi.go new file mode 100644 index 00000000000..ba135dcc598 --- /dev/null +++ b/ee/maintained-apps/ingesters/homebrew/external_refs/vivaldi.go @@ -0,0 +1,16 @@ +package externalrefs + +import ( + maintained_apps "github.com/fleetdm/fleet/v4/ee/maintained-apps" +) + +func VivaldiDMGInstaller(app *maintained_apps.FMAManifestApp) (*maintained_apps.FMAManifestApp, error) { + // Override installer URL to use Vivaldi's direct DMG instead of Homebrew's tar.xz + // The tar.xz format is not supported by the Fleet ingester's install script generator. + // Version is kept from Homebrew (not set to "latest") + app.InstallerURL = "https://downloads.vivaldi.com/stable/Vivaldi." + app.Version + ".universal.dmg" + // Set SHA256 to "no_check" since we're using a different installer URL than Homebrew + app.SHA256 = "no_check" + + return app, nil +} diff --git a/ee/maintained-apps/inputs/homebrew/vivaldi.json b/ee/maintained-apps/inputs/homebrew/vivaldi.json new file mode 100644 index 00000000000..5b758687f70 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/vivaldi.json @@ -0,0 +1,8 @@ +{ + "name": "Vivaldi", + "slug": "vivaldi/darwin", + "unique_identifier": "com.vivaldi.Vivaldi", + "token": "vivaldi", + "installer_format": "dmg", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index e58854bf457..b1b98f91194 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -83,7 +83,7 @@ "slug": "adobe-creative-cloud/darwin", "platform": "darwin", "unique_identifier": "com.adobe.acc.AdobeCreativeCloud", - "description": "Adobe Creative Cloud is a platform for creative software management and installation, including access to Adobe’s suite of tools like Photoshop, Illustrator, and Premiere Pro." + "description": "Adobe Creative Cloud is a platform for creative software management and installation, including access to Adobe\u2019s suite of tools like Photoshop, Illustrator, and Premiere Pro." }, { "name": "Adobe Digital Editions", @@ -118,14 +118,14 @@ "slug": "airtame/darwin", "platform": "darwin", "unique_identifier": "com.airtame.airtame-application", - "description": "Airtame is an app that makes shared screens easy to use — for screen sharing, conferencing, digital signage, and alerts." + "description": "Airtame is an app that makes shared screens easy to use \u2014 for screen sharing, conferencing, digital signage, and alerts." }, { "name": "Airtame", "slug": "airtame/windows", "platform": "windows", "unique_identifier": "Airtame 4.15.0", - "description": "Airtame is an app that makes shared screens easy to use — for screen sharing, conferencing, digital signage, and alerts." + "description": "Airtame is an app that makes shared screens easy to use \u2014 for screen sharing, conferencing, digital signage, and alerts." }, { "name": "Amazon Chime", @@ -475,14 +475,14 @@ "slug": "cursor/darwin", "platform": "darwin", "unique_identifier": "com.todesktop.230313mzl4w4u92", - "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors — all inside your editor." + "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors \u2014 all inside your editor." }, { "name": "Cursor", "slug": "cursor/windows", "platform": "windows", "unique_identifier": "Cursor", - "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors — all inside your editor." + "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors \u2014 all inside your editor." }, { "name": "Cyberduck", @@ -1709,6 +1709,13 @@ "unique_identifier": "Microsoft Visual Studio Code", "description": "Microsoft Visual Studio Code (VS Code) is an open-source, lightweight, and powerful code editor." }, + { + "name": "Vivaldi", + "slug": "vivaldi/darwin", + "platform": "darwin", + "unique_identifier": "com.vivaldi.Vivaldi", + "description": "Vivaldi is a feature-rich web browser built on Chromium, offering extensive customization options and a built-in email client." + }, { "name": "VLC media player", "slug": "vlc/darwin", diff --git a/ee/maintained-apps/outputs/vivaldi/darwin.json b/ee/maintained-apps/outputs/vivaldi/darwin.json new file mode 100644 index 00000000000..aaee409614c --- /dev/null +++ b/ee/maintained-apps/outputs/vivaldi/darwin.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "7.8.3925.76", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.vivaldi.Vivaldi';" + }, + "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.76.universal.dmg", + "install_script_ref": "d8e37a25", + "uninstall_script_ref": "83339974", + "sha256": "no_check", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "83339974": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nquit_application 'com.vivaldi.Vivaldi'\nsudo rm -rf \"$APPDIR/Vivaldi.app\"\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/Vivaldi Helper (Renderer)_*.diag'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/Vivaldi_*.diag'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/Vivaldi_*.plist'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.vivaldi.Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Caches/Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.vivaldi.Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.vivaldi.Vivaldi.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.vivaldi.Vivaldi.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.vivaldi.Vivaldi'\n", + "d8e37a25": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'com.vivaldi.Vivaldi'\nif [ -d \"$APPDIR/Vivaldi.app\" ]; then\n\tsudo mv \"$APPDIR/Vivaldi.app\" \"$TMPDIR/Vivaldi.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Vivaldi.app\" \"$APPDIR\"\nrelaunch_application 'com.vivaldi.Vivaldi'\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/Vivaldi.tsx b/frontend/pages/SoftwarePage/components/icons/Vivaldi.tsx new file mode 100644 index 00000000000..65911b8b45f --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Vivaldi.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const Vivaldi = (props: SVGProps) => ( + + + +); +export default Vivaldi; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index cf46f24449a..ba5f1a93b3a 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -218,6 +218,7 @@ import Utm from "./Utm"; import VirtualBox from "./VirtualBox"; import Viscosity from "./Viscosity"; import VisualStudioCode from "./VisualStudioCode"; +import Vivaldi from "./Vivaldi"; import Vlc from "./Vlc"; import VncViewer from "./VncViewer"; import WacomCenter from "./WacomCenter"; @@ -472,6 +473,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { viscosity: Viscosity, "vnc viewer": VncViewer, "visual studio code": VisualStudioCode, + vivaldi: Vivaldi, vlc: Vlc, "wacom center": WacomCenter, "wacom tablet": WacomCenter, diff --git a/website/assets/images/app-icon-vivaldi-60x60@2x.png b/website/assets/images/app-icon-vivaldi-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0d9620d2aa5ef2066c2d69cf7c937a64319d28a5 GIT binary patch literal 18827 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 Hu0mjfG%lu? literal 0 HcmV?d00001 From f663047861935d18400074a81ee21d98023b11dc Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:22:05 -0400 Subject: [PATCH 2/8] Add Vivaldi as fleet-maintained app (macOS + Windows) and fix apps.json ASCII Add Vivaldi browser as a fleet-maintained app for both macOS (homebrew, tar.xz format) and Windows (winget, exe installer with custom install and uninstall scripts). Add tar.xz extraction support to the homebrew ingester scripts. Add frontend icon and app-icon asset. Fix non-ASCII characters in apps.json: replace curly apostrophe (U+2019) with straight apostrophe in Adobe Creative Cloud description, and replace em-dashes (U+2014) with hyphens in Airtame and Cursor descriptions. --- .../inputs/winget/scripts/vivaldi_install.ps1 | 78 ++++++++ .../winget/scripts/vivaldi_uninstall.ps1 | 173 ++++++++++++++++++ ee/maintained-apps/inputs/winget/vivaldi.json | 12 ++ ee/maintained-apps/outputs/apps.json | 19 +- .../outputs/vivaldi/windows.json | 21 +++ .../SoftwarePage/components/icons/index.ts | 1 + 6 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 create mode 100644 ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 create mode 100644 ee/maintained-apps/inputs/winget/vivaldi.json create mode 100644 ee/maintained-apps/outputs/vivaldi/windows.json diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 new file mode 100644 index 00000000000..55cae284998 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 @@ -0,0 +1,78 @@ +$exeFilePath = "${env:INSTALLER_PATH}" + +$exitCode = 0 + +try { + +# Copy the installer to a public folder so that all can access it +# users +$exeFilename = Split-Path $exeFilePath -leaf +Copy-Item -Path $exeFilePath -Destination "${env:PUBLIC}" -Force +$exeFilePath = "${env:PUBLIC}\$exeFilename" + +# Task properties. The task will be started by the logged in user +$action = New-ScheduledTaskAction -Execute "$exeFilePath" ` + -Argument "--vivaldi-silent --do-not-launch-chrome" +$trigger = New-ScheduledTaskTrigger -AtLogOn +$userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries + +# Create a task object with the properties defined above +$task = New-ScheduledTask -Action $action -Trigger $trigger ` + -Settings $settings + +# Register the task +$taskName = "fleet-install-$exeFilename" +Register-ScheduledTask "$taskName" -InputObject $task -User "$userName" + +# keep track of the start time to cancel if taking too long to start +$startDate = Get-Date + +# Start the task now that it is ready +Start-ScheduledTask -TaskName "$taskName" -TaskPath "\" + +# Wait for the task to be running +$state = (Get-ScheduledTask -TaskName "$taskName").State +Write-Host "ScheduledTask is '$state'" + +while ($state -ne "Running") { + Write-Host "ScheduledTask is '$state'. Waiting to run .exe..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 1 + $state = (Get-ScheduledTask -TaskName "$taskName").State +} + +# Wait for the task to be done +$state = (Get-ScheduledTask -TaskName "$taskName").State +while ($state -eq "Running") { + Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 10 + $state = (Get-ScheduledTask -TaskName "$taskName").State +} + +# Remove task +Write-Host "Removing ScheduledTask: $taskName." +Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} finally { + # Remove installer + Remove-Item -Path $exeFilePath -Force +} + +Exit $exitCode diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 new file mode 100644 index 00000000000..836219c38b1 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 @@ -0,0 +1,173 @@ +$softwareName = "Vivaldi" + +# Script to uninstall software as the current logged-in user. +$userScript = @' + +# Define acceptable/expected exit codes +$ExpectedExitCodes = @(0, 19) + +$softwareName = "Vivaldi" + +# Using the exact software name here is recommended to avoid +# uninstalling unintended software. +$softwareNameLike = "*$softwareName*" + +# Some uninstallers require additional flags to run silently. +# Each uninstaller might use a different argument (usually it's "/S" or "/s") +$uninstallArgs = "--vivaldi-silent --force-uninstall" + +$uninstallCommand = "" +$exitCode = 0 + +try { + +$userKey = ` + 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' +[array]$uninstallKeys = Get-ChildItem ` + -Path @($userKey) ` + -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath } + +$foundUninstaller = $false +foreach ($key in $uninstallKeys) { + # If needed, add -notlike to the comparison to exclude certain similar + # software + if ($key.DisplayName -like $softwareNameLike) { + $foundUninstaller = $true + # Get the uninstall command. Some uninstallers do not include + # 'QuietUninstallString' and require a flag to run silently. + $uninstallCommand = if ($key.QuietUninstallString) { + $key.QuietUninstallString + } else { + $key.UninstallString + } + + # The uninstall command may contain command and args, like: + # "C:\Program Files\Software\uninstall.exe" --uninstall --silent + # Split the command and args + $splitArgs = $uninstallCommand.Split('"') + if ($splitArgs.Length -gt 1) { + if ($splitArgs.Length -eq 3) { + $uninstallArgs = "$( $splitArgs[2] ) $uninstallArgs".Trim() + } elseif ($splitArgs.Length -gt 3) { + Throw ` + "Uninstall command contains multiple quoted strings. " + + "Please update the uninstall script.`n" + + "Uninstall command: $uninstallCommand" + } + $uninstallCommand = $splitArgs[1] + } + Write-Host "Uninstall command: $uninstallCommand" + Write-Host "Uninstall args: $uninstallArgs" + + $processOptions = @{ + FilePath = $uninstallCommand + PassThru = $true + Wait = $true + } + if ($uninstallArgs -ne '') { + $processOptions.ArgumentList = "$uninstallArgs" + } + + # Start the process and track the exit code + $process = Start-Process @processOptions + $exitCode = $process.ExitCode + + # Prints the exit code + Write-Host "Uninstall exit code: $exitCode" + # Exit the loop once the software is found and uninstalled. + break + } +} + +if (-not $foundUninstaller) { + Write-Host "Uninstaller for '$softwareName' not found." + $exitCode = 1 +} + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} + +# Treat acceptable exit codes as success +if ($ExpectedExitCodes -contains $exitCode) { + Exit 0 +} else { + Exit $exitCode +} +'@ + +$exitCode = 0 + +# Create a script in a public folder so that it can be accessed by all users. +$uninstallScriptPath = "${env:PUBLIC}/uninstall-$softwareName.ps1" +$taskName = "fleet-uninstall-$softwareName" +try { + Set-Content -Path $uninstallScriptPath -Value $userScript -Force + + # Task properties. The task will be started by the logged in user + $action = New-ScheduledTaskAction -Execute "PowerShell.exe" ` + -Argument "$uninstallScriptPath" + $trigger = New-ScheduledTaskTrigger -AtLogOn + $userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries + + # Create a task object with the properties defined above + $task = New-ScheduledTask -Action $action -Trigger $trigger ` + -Settings $settings + + # Register the task + Register-ScheduledTask "$taskName" -InputObject $task -User "$userName" + + # keep track of the start time to cancel if taking too long to start + $startDate = Get-Date + + # Start the task now that it is ready + Start-ScheduledTask -TaskName "$taskName" -TaskPath "\" + + # Wait for the task to be running + $state = (Get-ScheduledTask -TaskName "$taskName").State + Write-Host "ScheduledTask is '$state'" + + while ($state -ne "Running") { + Write-Host "ScheduledTask is '$state'. Waiting to uninstall..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 1 + $state = (Get-ScheduledTask -TaskName "$taskName").State + } + + # Wait for the task to be done + $state = (Get-ScheduledTask -TaskName "$taskName").State + while ($state -eq "Running") { + Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..." + + $endDate = Get-Date + $elapsedTime = New-Timespan -Start $startDate -End $endDate + if ($elapsedTime.TotalSeconds -gt 120) { + Throw "Timed-out waiting for scheduled task state." + } + + Start-Sleep -Seconds 10 + $state = (Get-ScheduledTask -TaskName "$taskName").State + } + +} catch { + Write-Host "Error: $_" + $exitCode = 1 +} finally { + # Remove task + Write-Host "Removing ScheduledTask: $taskName." + Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false + + # Remove user script + Remove-Item -Path $uninstallScriptPath -Force +} + +Exit $exitCode diff --git a/ee/maintained-apps/inputs/winget/vivaldi.json b/ee/maintained-apps/inputs/winget/vivaldi.json new file mode 100644 index 00000000000..7228f380902 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/vivaldi.json @@ -0,0 +1,12 @@ +{ + "name": "Vivaldi", + "slug": "vivaldi/windows", + "package_identifier": "Vivaldi.Vivaldi", + "unique_identifier": "Vivaldi", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "exe", + "installer_scope": "user", + "default_categories": ["Browsers"] +} diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index b1b98f91194..3279a0b41f2 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -83,7 +83,7 @@ "slug": "adobe-creative-cloud/darwin", "platform": "darwin", "unique_identifier": "com.adobe.acc.AdobeCreativeCloud", - "description": "Adobe Creative Cloud is a platform for creative software management and installation, including access to Adobe\u2019s suite of tools like Photoshop, Illustrator, and Premiere Pro." + "description": "Adobe Creative Cloud is a platform for creative software management and installation, including access to Adobe's suite of tools like Photoshop, Illustrator, and Premiere Pro." }, { "name": "Adobe Digital Editions", @@ -118,14 +118,14 @@ "slug": "airtame/darwin", "platform": "darwin", "unique_identifier": "com.airtame.airtame-application", - "description": "Airtame is an app that makes shared screens easy to use \u2014 for screen sharing, conferencing, digital signage, and alerts." + "description": "Airtame is an app that makes shared screens easy to use - for screen sharing, conferencing, digital signage, and alerts." }, { "name": "Airtame", "slug": "airtame/windows", "platform": "windows", "unique_identifier": "Airtame 4.15.0", - "description": "Airtame is an app that makes shared screens easy to use \u2014 for screen sharing, conferencing, digital signage, and alerts." + "description": "Airtame is an app that makes shared screens easy to use - for screen sharing, conferencing, digital signage, and alerts." }, { "name": "Amazon Chime", @@ -475,14 +475,14 @@ "slug": "cursor/darwin", "platform": "darwin", "unique_identifier": "com.todesktop.230313mzl4w4u92", - "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors \u2014 all inside your editor." + "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors - all inside your editor." }, { "name": "Cursor", "slug": "cursor/windows", "platform": "windows", "unique_identifier": "Cursor", - "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors \u2014 all inside your editor." + "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors - all inside your editor." }, { "name": "Cyberduck", @@ -1714,7 +1714,14 @@ "slug": "vivaldi/darwin", "platform": "darwin", "unique_identifier": "com.vivaldi.Vivaldi", - "description": "Vivaldi is a feature-rich web browser built on Chromium, offering extensive customization options and a built-in email client." + "description": "Vivaldi is a customizable web browser designed for power users." + }, + { + "name": "Vivaldi", + "slug": "vivaldi/windows", + "platform": "windows", + "unique_identifier": "Vivaldi", + "description": "Vivaldi is a customizable web browser designed for power users." }, { "name": "VLC media player", diff --git a/ee/maintained-apps/outputs/vivaldi/windows.json b/ee/maintained-apps/outputs/vivaldi/windows.json new file mode 100644 index 00000000000..eeeed9b6eba --- /dev/null +++ b/ee/maintained-apps/outputs/vivaldi/windows.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "7.8.3925.76", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Vivaldi' AND publisher = 'Vivaldi Technologies AS.';" + }, + "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.76.x64.exe", + "install_script_ref": "ab030635", + "uninstall_script_ref": "532c3250", + "sha256": "b47bad7839492127bde8c7409e8f7773ca551eb812684d851c3534957e424802", + "default_categories": [ + "Browsers" + ] + } + ], + "refs": { + "532c3250": "$softwareName = \"Vivaldi\"\n\n# Script to uninstall software as the current logged-in user.\n$userScript = @'\n\n# Define acceptable/expected exit codes\n$ExpectedExitCodes = @(0, 19)\n\n$softwareName = \"Vivaldi\"\n\n# Using the exact software name here is recommended to avoid\n# uninstalling unintended software.\n$softwareNameLike = \"*$softwareName*\"\n\n# Some uninstallers require additional flags to run silently.\n# Each uninstaller might use a different argument (usually it's \"/S\" or \"/s\")\n$uninstallArgs = \"--vivaldi-silent --force-uninstall\"\n\n$uninstallCommand = \"\"\n$exitCode = 0\n\ntry {\n\n$userKey = `\n 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($userKey) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start the process and track the exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\n# Treat acceptable exit codes as success\nif ($ExpectedExitCodes -contains $exitCode) {\n Exit 0\n} else {\n Exit $exitCode\n}\n'@\n\n$exitCode = 0\n\n# Create a script in a public folder so that it can be accessed by all users.\n$uninstallScriptPath = \"${env:PUBLIC}/uninstall-$softwareName.ps1\"\n$taskName = \"fleet-uninstall-$softwareName\"\ntry {\n Set-Content -Path $uninstallScriptPath -Value $userScript -Force\n\n # Task properties. The task will be started by the logged in user\n $action = New-ScheduledTaskAction -Execute \"PowerShell.exe\" `\n -Argument \"$uninstallScriptPath\"\n $trigger = New-ScheduledTaskTrigger -AtLogOn\n $userName = (Get-CimInstance Win32_Process -Filter 'name = \"explorer.exe\"' | Invoke-CimMethod -MethodName getowner).User\n $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n\n # Create a task object with the properties defined above\n $task = New-ScheduledTask -Action $action -Trigger $trigger `\n -Settings $settings\n\n # Register the task\n Register-ScheduledTask \"$taskName\" -InputObject $task -User \"$userName\"\n\n # keep track of the start time to cancel if taking too long to start\n $startDate = Get-Date\n\n # Start the task now that it is ready\n Start-ScheduledTask -TaskName \"$taskName\" -TaskPath \"\\\"\n\n # Wait for the task to be running\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n Write-Host \"ScheduledTask is '$state'\"\n\n while ($state -ne \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting to uninstall...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 1\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n }\n\n # Wait for the task to be done\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n while ($state -eq \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting for .exe to complete...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 10\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n }\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n} finally {\n # Remove task\n Write-Host \"Removing ScheduledTask: $taskName.\"\n Unregister-ScheduledTask -TaskName \"$taskName\" -Confirm:$false\n\n # Remove user script\n Remove-Item -Path $uninstallScriptPath -Force\n}\n\nExit $exitCode\n", + "ab030635": "$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n$exitCode = 0\n\ntry {\n\n# Copy the installer to a public folder so that all can access it\n# users\n$exeFilename = Split-Path $exeFilePath -leaf\nCopy-Item -Path $exeFilePath -Destination \"${env:PUBLIC}\" -Force\n$exeFilePath = \"${env:PUBLIC}\\$exeFilename\"\n\n# Task properties. The task will be started by the logged in user\n$action = New-ScheduledTaskAction -Execute \"$exeFilePath\" `\n -Argument \"--vivaldi-silent --do-not-launch-chrome\"\n$trigger = New-ScheduledTaskTrigger -AtLogOn\n$userName = (Get-CimInstance Win32_Process -Filter 'name = \"explorer.exe\"' | Invoke-CimMethod -MethodName getowner).User\n$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n\n# Create a task object with the properties defined above\n$task = New-ScheduledTask -Action $action -Trigger $trigger `\n -Settings $settings\n\n# Register the task\n$taskName = \"fleet-install-$exeFilename\"\nRegister-ScheduledTask \"$taskName\" -InputObject $task -User \"$userName\"\n\n# keep track of the start time to cancel if taking too long to start\n$startDate = Get-Date\n\n# Start the task now that it is ready\nStart-ScheduledTask -TaskName \"$taskName\" -TaskPath \"\\\"\n\n# Wait for the task to be running\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nWrite-Host \"ScheduledTask is '$state'\"\n\nwhile ($state -ne \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting to run .exe...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 1\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Wait for the task to be done\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nwhile ($state -eq \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting for .exe to complete...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 10\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Remove task\nWrite-Host \"Removing ScheduledTask: $taskName.\"\nUnregister-ScheduledTask -TaskName \"$taskName\" -Confirm:$false\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n} finally {\n # Remove installer\n Remove-Item -Path $exeFilePath -Force\n}\n\nExit $exitCode\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index ba5f1a93b3a..106c52d8d50 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -471,6 +471,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { utm: Utm, virtualbox: VirtualBox, viscosity: Viscosity, + vivaldi: Vivaldi, "vnc viewer": VncViewer, "visual studio code": VisualStudioCode, vivaldi: Vivaldi, From 971cb41c31e2af100e7d654c3ef742e933d1d519 Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:38:53 -0400 Subject: [PATCH 3/8] Fix Vivaldi Windows uninstall: use Chromium flags directly instead of scheduled task pattern --- .../winget/scripts/vivaldi_uninstall.ps1 | 203 ++++-------------- .../outputs/vivaldi/windows.json | 12 +- 2 files changed, 50 insertions(+), 165 deletions(-) diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 index 836219c38b1..c353a0dbc5e 100644 --- a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 @@ -1,173 +1,58 @@ -$softwareName = "Vivaldi" - -# Script to uninstall software as the current logged-in user. -$userScript = @' - -# Define acceptable/expected exit codes -$ExpectedExitCodes = @(0, 19) - -$softwareName = "Vivaldi" - -# Using the exact software name here is recommended to avoid -# uninstalling unintended software. -$softwareNameLike = "*$softwareName*" - -# Some uninstallers require additional flags to run silently. -# Each uninstaller might use a different argument (usually it's "/S" or "/s") -$uninstallArgs = "--vivaldi-silent --force-uninstall" - -$uninstallCommand = "" -$exitCode = 0 - -try { - -$userKey = ` - 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -[array]$uninstallKeys = Get-ChildItem ` - -Path @($userKey) ` - -ErrorAction SilentlyContinue | - ForEach-Object { Get-ItemProperty $_.PSPath } - -$foundUninstaller = $false -foreach ($key in $uninstallKeys) { - # If needed, add -notlike to the comparison to exclude certain similar - # software - if ($key.DisplayName -like $softwareNameLike) { - $foundUninstaller = $true - # Get the uninstall command. Some uninstallers do not include - # 'QuietUninstallString' and require a flag to run silently. - $uninstallCommand = if ($key.QuietUninstallString) { - $key.QuietUninstallString - } else { - $key.UninstallString - } - - # The uninstall command may contain command and args, like: - # "C:\Program Files\Software\uninstall.exe" --uninstall --silent - # Split the command and args - $splitArgs = $uninstallCommand.Split('"') - if ($splitArgs.Length -gt 1) { - if ($splitArgs.Length -eq 3) { - $uninstallArgs = "$( $splitArgs[2] ) $uninstallArgs".Trim() - } elseif ($splitArgs.Length -gt 3) { - Throw ` - "Uninstall command contains multiple quoted strings. " + - "Please update the uninstall script.`n" + - "Uninstall command: $uninstallCommand" - } - $uninstallCommand = $splitArgs[1] - } - Write-Host "Uninstall command: $uninstallCommand" - Write-Host "Uninstall args: $uninstallArgs" - - $processOptions = @{ - FilePath = $uninstallCommand - PassThru = $true - Wait = $true - } - if ($uninstallArgs -ne '') { - $processOptions.ArgumentList = "$uninstallArgs" - } - - # Start the process and track the exit code - $process = Start-Process @processOptions - $exitCode = $process.ExitCode - - # Prints the exit code - Write-Host "Uninstall exit code: $exitCode" - # Exit the loop once the software is found and uninstalled. - break - } +# Uninstall Vivaldi (user-scoped Chromium-based browser) + +$displayName = "Vivaldi" + +$paths = @( + 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +) + +$uninstall = $null +foreach ($p in $paths) { + $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -and $_.DisplayName -eq $displayName + } + if ($items) { $uninstall = $items | Select-Object -First 1; break } } -if (-not $foundUninstaller) { - Write-Host "Uninstaller for '$softwareName' not found." - $exitCode = 1 +if (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) { + Write-Host "Uninstall entry not found for '$displayName'" + Exit 1 } -} catch { - Write-Host "Error: $_" - $exitCode = 1 -} +# Kill any running Vivaldi processes before uninstalling +Stop-Process -Name "vivaldi" -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 2 -# Treat acceptable exit codes as success -if ($ExpectedExitCodes -contains $exitCode) { - Exit 0 +$uninstallCommand = if ($uninstall.QuietUninstallString) { + $uninstall.QuietUninstallString } else { - Exit $exitCode + $uninstall.UninstallString } -'@ - -$exitCode = 0 - -# Create a script in a public folder so that it can be accessed by all users. -$uninstallScriptPath = "${env:PUBLIC}/uninstall-$softwareName.ps1" -$taskName = "fleet-uninstall-$softwareName" -try { - Set-Content -Path $uninstallScriptPath -Value $userScript -Force - - # Task properties. The task will be started by the logged in user - $action = New-ScheduledTaskAction -Execute "PowerShell.exe" ` - -Argument "$uninstallScriptPath" - $trigger = New-ScheduledTaskTrigger -AtLogOn - $userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User - $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries - - # Create a task object with the properties defined above - $task = New-ScheduledTask -Action $action -Trigger $trigger ` - -Settings $settings - - # Register the task - Register-ScheduledTask "$taskName" -InputObject $task -User "$userName" - - # keep track of the start time to cancel if taking too long to start - $startDate = Get-Date - # Start the task now that it is ready - Start-ScheduledTask -TaskName "$taskName" -TaskPath "\" +# Parse quoted executable from any trailing args in the registry string +$splitArgs = $uninstallCommand.Split('"') +$exe = $splitArgs[1] +$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" } - # Wait for the task to be running - $state = (Get-ScheduledTask -TaskName "$taskName").State - Write-Host "ScheduledTask is '$state'" +# Chromium-based uninstaller flags +$uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim() - while ($state -ne "Running") { - Write-Host "ScheduledTask is '$state'. Waiting to uninstall..." +Write-Host "Uninstall command: $exe" +Write-Host "Uninstall args: $uninstallArgs" - $endDate = Get-Date - $elapsedTime = New-Timespan -Start $startDate -End $endDate - if ($elapsedTime.TotalSeconds -gt 120) { - Throw "Timed-out waiting for scheduled task state." - } - - Start-Sleep -Seconds 1 - $state = (Get-ScheduledTask -TaskName "$taskName").State - } - - # Wait for the task to be done - $state = (Get-ScheduledTask -TaskName "$taskName").State - while ($state -eq "Running") { - Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..." - - $endDate = Get-Date - $elapsedTime = New-Timespan -Start $startDate -End $endDate - if ($elapsedTime.TotalSeconds -gt 120) { - Throw "Timed-out waiting for scheduled task state." - } - - Start-Sleep -Seconds 10 - $state = (Get-ScheduledTask -TaskName "$taskName").State - } +try { + $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait + $exitCode = $process.ExitCode + Write-Host "Uninstall exit code: $exitCode" + # Chromium uninstallers return 19 on success + if ($exitCode -eq 0 -or $exitCode -eq 19) { + Exit 0 + } + Exit $exitCode } catch { - Write-Host "Error: $_" - $exitCode = 1 -} finally { - # Remove task - Write-Host "Removing ScheduledTask: $taskName." - Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false - - # Remove user script - Remove-Item -Path $uninstallScriptPath -Force + Write-Host "Error running uninstaller: $_" + Exit 1 } - -Exit $exitCode diff --git a/ee/maintained-apps/outputs/vivaldi/windows.json b/ee/maintained-apps/outputs/vivaldi/windows.json index eeeed9b6eba..fbccf42e33b 100644 --- a/ee/maintained-apps/outputs/vivaldi/windows.json +++ b/ee/maintained-apps/outputs/vivaldi/windows.json @@ -1,21 +1,21 @@ { "versions": [ { - "version": "7.8.3925.76", + "version": "7.8.3925.81", "queries": { "exists": "SELECT 1 FROM programs WHERE name = 'Vivaldi' AND publisher = 'Vivaldi Technologies AS.';" }, - "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.76.x64.exe", + "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.81.x64.exe", "install_script_ref": "ab030635", - "uninstall_script_ref": "532c3250", - "sha256": "b47bad7839492127bde8c7409e8f7773ca551eb812684d851c3534957e424802", + "uninstall_script_ref": "dd74a7d3", + "sha256": "4d9ad6cffc65e644fbdd3896634b27be6d2fd6062a7adb4895f0fe4c0f260b5f", "default_categories": [ "Browsers" ] } ], "refs": { - "532c3250": "$softwareName = \"Vivaldi\"\n\n# Script to uninstall software as the current logged-in user.\n$userScript = @'\n\n# Define acceptable/expected exit codes\n$ExpectedExitCodes = @(0, 19)\n\n$softwareName = \"Vivaldi\"\n\n# Using the exact software name here is recommended to avoid\n# uninstalling unintended software.\n$softwareNameLike = \"*$softwareName*\"\n\n# Some uninstallers require additional flags to run silently.\n# Each uninstaller might use a different argument (usually it's \"/S\" or \"/s\")\n$uninstallArgs = \"--vivaldi-silent --force-uninstall\"\n\n$uninstallCommand = \"\"\n$exitCode = 0\n\ntry {\n\n$userKey = `\n 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n[array]$uninstallKeys = Get-ChildItem `\n -Path @($userKey) `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" --uninstall --silent\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $uninstallArgs = \"$( $splitArgs[2] ) $uninstallArgs\".Trim()\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = \"$uninstallArgs\"\n }\n\n # Start the process and track the exit code\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n\n # Prints the exit code\n Write-Host \"Uninstall exit code: $exitCode\"\n # Exit the loop once the software is found and uninstalled.\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstaller for '$softwareName' not found.\"\n $exitCode = 1\n}\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n}\n\n# Treat acceptable exit codes as success\nif ($ExpectedExitCodes -contains $exitCode) {\n Exit 0\n} else {\n Exit $exitCode\n}\n'@\n\n$exitCode = 0\n\n# Create a script in a public folder so that it can be accessed by all users.\n$uninstallScriptPath = \"${env:PUBLIC}/uninstall-$softwareName.ps1\"\n$taskName = \"fleet-uninstall-$softwareName\"\ntry {\n Set-Content -Path $uninstallScriptPath -Value $userScript -Force\n\n # Task properties. The task will be started by the logged in user\n $action = New-ScheduledTaskAction -Execute \"PowerShell.exe\" `\n -Argument \"$uninstallScriptPath\"\n $trigger = New-ScheduledTaskTrigger -AtLogOn\n $userName = (Get-CimInstance Win32_Process -Filter 'name = \"explorer.exe\"' | Invoke-CimMethod -MethodName getowner).User\n $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n\n # Create a task object with the properties defined above\n $task = New-ScheduledTask -Action $action -Trigger $trigger `\n -Settings $settings\n\n # Register the task\n Register-ScheduledTask \"$taskName\" -InputObject $task -User \"$userName\"\n\n # keep track of the start time to cancel if taking too long to start\n $startDate = Get-Date\n\n # Start the task now that it is ready\n Start-ScheduledTask -TaskName \"$taskName\" -TaskPath \"\\\"\n\n # Wait for the task to be running\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n Write-Host \"ScheduledTask is '$state'\"\n\n while ($state -ne \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting to uninstall...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 1\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n }\n\n # Wait for the task to be done\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n while ($state -eq \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting for .exe to complete...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 10\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n }\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n} finally {\n # Remove task\n Write-Host \"Removing ScheduledTask: $taskName.\"\n Unregister-ScheduledTask -TaskName \"$taskName\" -Confirm:$false\n\n # Remove user script\n Remove-Item -Path $uninstallScriptPath -Force\n}\n\nExit $exitCode\n", - "ab030635": "$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n$exitCode = 0\n\ntry {\n\n# Copy the installer to a public folder so that all can access it\n# users\n$exeFilename = Split-Path $exeFilePath -leaf\nCopy-Item -Path $exeFilePath -Destination \"${env:PUBLIC}\" -Force\n$exeFilePath = \"${env:PUBLIC}\\$exeFilename\"\n\n# Task properties. The task will be started by the logged in user\n$action = New-ScheduledTaskAction -Execute \"$exeFilePath\" `\n -Argument \"--vivaldi-silent --do-not-launch-chrome\"\n$trigger = New-ScheduledTaskTrigger -AtLogOn\n$userName = (Get-CimInstance Win32_Process -Filter 'name = \"explorer.exe\"' | Invoke-CimMethod -MethodName getowner).User\n$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n\n# Create a task object with the properties defined above\n$task = New-ScheduledTask -Action $action -Trigger $trigger `\n -Settings $settings\n\n# Register the task\n$taskName = \"fleet-install-$exeFilename\"\nRegister-ScheduledTask \"$taskName\" -InputObject $task -User \"$userName\"\n\n# keep track of the start time to cancel if taking too long to start\n$startDate = Get-Date\n\n# Start the task now that it is ready\nStart-ScheduledTask -TaskName \"$taskName\" -TaskPath \"\\\"\n\n# Wait for the task to be running\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nWrite-Host \"ScheduledTask is '$state'\"\n\nwhile ($state -ne \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting to run .exe...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 1\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Wait for the task to be done\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nwhile ($state -eq \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting for .exe to complete...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 10\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Remove task\nWrite-Host \"Removing ScheduledTask: $taskName.\"\nUnregister-ScheduledTask -TaskName \"$taskName\" -Confirm:$false\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n} finally {\n # Remove installer\n Remove-Item -Path $exeFilePath -Force\n}\n\nExit $exitCode\n" + "ab030635": "$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n$exitCode = 0\n\ntry {\n\n# Copy the installer to a public folder so that all can access it\n# users\n$exeFilename = Split-Path $exeFilePath -leaf\nCopy-Item -Path $exeFilePath -Destination \"${env:PUBLIC}\" -Force\n$exeFilePath = \"${env:PUBLIC}\\$exeFilename\"\n\n# Task properties. The task will be started by the logged in user\n$action = New-ScheduledTaskAction -Execute \"$exeFilePath\" `\n -Argument \"--vivaldi-silent --do-not-launch-chrome\"\n$trigger = New-ScheduledTaskTrigger -AtLogOn\n$userName = (Get-CimInstance Win32_Process -Filter 'name = \"explorer.exe\"' | Invoke-CimMethod -MethodName getowner).User\n$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n\n# Create a task object with the properties defined above\n$task = New-ScheduledTask -Action $action -Trigger $trigger `\n -Settings $settings\n\n# Register the task\n$taskName = \"fleet-install-$exeFilename\"\nRegister-ScheduledTask \"$taskName\" -InputObject $task -User \"$userName\"\n\n# keep track of the start time to cancel if taking too long to start\n$startDate = Get-Date\n\n# Start the task now that it is ready\nStart-ScheduledTask -TaskName \"$taskName\" -TaskPath \"\\\"\n\n# Wait for the task to be running\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nWrite-Host \"ScheduledTask is '$state'\"\n\nwhile ($state -ne \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting to run .exe...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 1\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Wait for the task to be done\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nwhile ($state -eq \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting for .exe to complete...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 10\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Remove task\nWrite-Host \"Removing ScheduledTask: $taskName.\"\nUnregister-ScheduledTask -TaskName \"$taskName\" -Confirm:$false\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n} finally {\n # Remove installer\n Remove-Item -Path $exeFilePath -Force\n}\n\nExit $exitCode\n", + "dd74a7d3": "# Uninstall Vivaldi (user-scoped Chromium-based browser)\n\n$displayName = \"Vivaldi\"\n\n$paths = @(\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -and $_.DisplayName -eq $displayName\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for '$displayName'\"\n Exit 1\n}\n\n# Kill any running Vivaldi processes before uninstalling\nStop-Process -Name \"vivaldi\" -Force -ErrorAction SilentlyContinue\nStart-Sleep -Seconds 2\n\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# Parse quoted executable from any trailing args in the registry string\n$splitArgs = $uninstallCommand.Split('\"')\n$exe = $splitArgs[1]\n$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { \"\" }\n\n# Chromium-based uninstaller flags\n$uninstallArgs = \"$existingArgs --uninstall --force-uninstall\".Trim()\n\nWrite-Host \"Uninstall command: $exe\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\ntry {\n $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n\n # Chromium uninstallers return 19 on success\n if ($exitCode -eq 0 -or $exitCode -eq 19) {\n Exit 0\n }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n" } } From 2b0669d72a79fd1bc64666f766e3e769f0c3a4cc Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:43:11 -0400 Subject: [PATCH 4/8] Simplify Vivaldi Windows install script: remove scheduled task boilerplate --- .../inputs/winget/scripts/vivaldi_install.ps1 | 83 ++----------------- .../outputs/vivaldi/windows.json | 4 +- 2 files changed, 7 insertions(+), 80 deletions(-) diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 index 55cae284998..6c0ecf578f8 100644 --- a/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 @@ -1,78 +1,5 @@ -$exeFilePath = "${env:INSTALLER_PATH}" - -$exitCode = 0 - -try { - -# Copy the installer to a public folder so that all can access it -# users -$exeFilename = Split-Path $exeFilePath -leaf -Copy-Item -Path $exeFilePath -Destination "${env:PUBLIC}" -Force -$exeFilePath = "${env:PUBLIC}\$exeFilename" - -# Task properties. The task will be started by the logged in user -$action = New-ScheduledTaskAction -Execute "$exeFilePath" ` - -Argument "--vivaldi-silent --do-not-launch-chrome" -$trigger = New-ScheduledTaskTrigger -AtLogOn -$userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User -$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries - -# Create a task object with the properties defined above -$task = New-ScheduledTask -Action $action -Trigger $trigger ` - -Settings $settings - -# Register the task -$taskName = "fleet-install-$exeFilename" -Register-ScheduledTask "$taskName" -InputObject $task -User "$userName" - -# keep track of the start time to cancel if taking too long to start -$startDate = Get-Date - -# Start the task now that it is ready -Start-ScheduledTask -TaskName "$taskName" -TaskPath "\" - -# Wait for the task to be running -$state = (Get-ScheduledTask -TaskName "$taskName").State -Write-Host "ScheduledTask is '$state'" - -while ($state -ne "Running") { - Write-Host "ScheduledTask is '$state'. Waiting to run .exe..." - - $endDate = Get-Date - $elapsedTime = New-Timespan -Start $startDate -End $endDate - if ($elapsedTime.TotalSeconds -gt 120) { - Throw "Timed-out waiting for scheduled task state." - } - - Start-Sleep -Seconds 1 - $state = (Get-ScheduledTask -TaskName "$taskName").State -} - -# Wait for the task to be done -$state = (Get-ScheduledTask -TaskName "$taskName").State -while ($state -eq "Running") { - Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..." - - $endDate = Get-Date - $elapsedTime = New-Timespan -Start $startDate -End $endDate - if ($elapsedTime.TotalSeconds -gt 120) { - Throw "Timed-out waiting for scheduled task state." - } - - Start-Sleep -Seconds 10 - $state = (Get-ScheduledTask -TaskName "$taskName").State -} - -# Remove task -Write-Host "Removing ScheduledTask: $taskName." -Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false - -} catch { - Write-Host "Error: $_" - $exitCode = 1 -} finally { - # Remove installer - Remove-Item -Path $exeFilePath -Force -} - -Exit $exitCode +# Install Vivaldi silently (user-scoped Chromium-based browser) +$process = Start-Process -FilePath $env:INSTALLER_PATH ` + -ArgumentList "--vivaldi-silent --do-not-launch-chrome" ` + -NoNewWindow -PassThru -Wait +Exit $process.ExitCode diff --git a/ee/maintained-apps/outputs/vivaldi/windows.json b/ee/maintained-apps/outputs/vivaldi/windows.json index fbccf42e33b..a10b79d9af2 100644 --- a/ee/maintained-apps/outputs/vivaldi/windows.json +++ b/ee/maintained-apps/outputs/vivaldi/windows.json @@ -6,7 +6,7 @@ "exists": "SELECT 1 FROM programs WHERE name = 'Vivaldi' AND publisher = 'Vivaldi Technologies AS.';" }, "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.81.x64.exe", - "install_script_ref": "ab030635", + "install_script_ref": "81b0529a", "uninstall_script_ref": "dd74a7d3", "sha256": "4d9ad6cffc65e644fbdd3896634b27be6d2fd6062a7adb4895f0fe4c0f260b5f", "default_categories": [ @@ -15,7 +15,7 @@ } ], "refs": { - "ab030635": "$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n$exitCode = 0\n\ntry {\n\n# Copy the installer to a public folder so that all can access it\n# users\n$exeFilename = Split-Path $exeFilePath -leaf\nCopy-Item -Path $exeFilePath -Destination \"${env:PUBLIC}\" -Force\n$exeFilePath = \"${env:PUBLIC}\\$exeFilename\"\n\n# Task properties. The task will be started by the logged in user\n$action = New-ScheduledTaskAction -Execute \"$exeFilePath\" `\n -Argument \"--vivaldi-silent --do-not-launch-chrome\"\n$trigger = New-ScheduledTaskTrigger -AtLogOn\n$userName = (Get-CimInstance Win32_Process -Filter 'name = \"explorer.exe\"' | Invoke-CimMethod -MethodName getowner).User\n$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries\n\n# Create a task object with the properties defined above\n$task = New-ScheduledTask -Action $action -Trigger $trigger `\n -Settings $settings\n\n# Register the task\n$taskName = \"fleet-install-$exeFilename\"\nRegister-ScheduledTask \"$taskName\" -InputObject $task -User \"$userName\"\n\n# keep track of the start time to cancel if taking too long to start\n$startDate = Get-Date\n\n# Start the task now that it is ready\nStart-ScheduledTask -TaskName \"$taskName\" -TaskPath \"\\\"\n\n# Wait for the task to be running\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nWrite-Host \"ScheduledTask is '$state'\"\n\nwhile ($state -ne \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting to run .exe...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 1\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Wait for the task to be done\n$state = (Get-ScheduledTask -TaskName \"$taskName\").State\nwhile ($state -eq \"Running\") {\n Write-Host \"ScheduledTask is '$state'. Waiting for .exe to complete...\"\n\n $endDate = Get-Date\n $elapsedTime = New-Timespan -Start $startDate -End $endDate\n if ($elapsedTime.TotalSeconds -gt 120) {\n Throw \"Timed-out waiting for scheduled task state.\"\n }\n\n Start-Sleep -Seconds 10\n $state = (Get-ScheduledTask -TaskName \"$taskName\").State\n}\n\n# Remove task\nWrite-Host \"Removing ScheduledTask: $taskName.\"\nUnregister-ScheduledTask -TaskName \"$taskName\" -Confirm:$false\n\n} catch {\n Write-Host \"Error: $_\"\n $exitCode = 1\n} finally {\n # Remove installer\n Remove-Item -Path $exeFilePath -Force\n}\n\nExit $exitCode\n", + "81b0529a": "# Install Vivaldi silently (user-scoped Chromium-based browser)\n$process = Start-Process -FilePath $env:INSTALLER_PATH `\n -ArgumentList \"--vivaldi-silent --do-not-launch-chrome\" `\n -NoNewWindow -PassThru -Wait\nExit $process.ExitCode\n", "dd74a7d3": "# Uninstall Vivaldi (user-scoped Chromium-based browser)\n\n$displayName = \"Vivaldi\"\n\n$paths = @(\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -and $_.DisplayName -eq $displayName\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for '$displayName'\"\n Exit 1\n}\n\n# Kill any running Vivaldi processes before uninstalling\nStop-Process -Name \"vivaldi\" -Force -ErrorAction SilentlyContinue\nStart-Sleep -Seconds 2\n\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# Parse quoted executable from any trailing args in the registry string\n$splitArgs = $uninstallCommand.Split('\"')\n$exe = $splitArgs[1]\n$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { \"\" }\n\n# Chromium-based uninstaller flags\n$uninstallArgs = \"$existingArgs --uninstall --force-uninstall\".Trim()\n\nWrite-Host \"Uninstall command: $exe\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\ntry {\n $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n\n # Chromium uninstallers return 19 on success\n if ($exitCode -eq 0 -or $exitCode -eq 19) {\n Exit 0\n }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n" } } From 8855672b8ee9b1e5f9e2c9479f65bbff8208f243 Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:49:59 -0400 Subject: [PATCH 5/8] Restore apps.json entries to match main (undo ingester re-encoding) --- ee/maintained-apps/outputs/apps.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 3279a0b41f2..ab4c51839ec 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -83,7 +83,7 @@ "slug": "adobe-creative-cloud/darwin", "platform": "darwin", "unique_identifier": "com.adobe.acc.AdobeCreativeCloud", - "description": "Adobe Creative Cloud is a platform for creative software management and installation, including access to Adobe's suite of tools like Photoshop, Illustrator, and Premiere Pro." + "description": "Adobe Creative Cloud is a platform for creative software management and installation, including access to Adobe’s suite of tools like Photoshop, Illustrator, and Premiere Pro." }, { "name": "Adobe Digital Editions", @@ -118,14 +118,14 @@ "slug": "airtame/darwin", "platform": "darwin", "unique_identifier": "com.airtame.airtame-application", - "description": "Airtame is an app that makes shared screens easy to use - for screen sharing, conferencing, digital signage, and alerts." + "description": "Airtame is an app that makes shared screens easy to use — for screen sharing, conferencing, digital signage, and alerts." }, { "name": "Airtame", "slug": "airtame/windows", "platform": "windows", "unique_identifier": "Airtame 4.15.0", - "description": "Airtame is an app that makes shared screens easy to use - for screen sharing, conferencing, digital signage, and alerts." + "description": "Airtame is an app that makes shared screens easy to use — for screen sharing, conferencing, digital signage, and alerts." }, { "name": "Amazon Chime", @@ -475,14 +475,14 @@ "slug": "cursor/darwin", "platform": "darwin", "unique_identifier": "com.todesktop.230313mzl4w4u92", - "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors - all inside your editor." + "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors — all inside your editor." }, { "name": "Cursor", "slug": "cursor/windows", "platform": "windows", "unique_identifier": "Cursor", - "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors - all inside your editor." + "description": "Cursor AI boosts your coding productivity by giving smart, context-aware code suggestions, quick explanations, and easy refactors — all inside your editor." }, { "name": "Cyberduck", From 698fec422f63a299104c022491de7970dfb1300a Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:07:51 -0400 Subject: [PATCH 6/8] Regenerate Vivaldi FMA outputs; install machine-wide on Windows - Rebase onto current main and regenerate darwin/windows outputs via the ingester at version 8.0.4033.46 (refs content-addressed, not hand-edited). - Windows: add --system-level to the install script and set installer_scope to machine. Fleet installs as SYSTEM, so without --system-level the Chromium-based installer landed in the SYSTEM profile and was invisible to the real user. The exists query (HKLM) and uninstall script already assumed a machine install. Flag verified against Chrome's FMA script and Vivaldi docs. - Fill catalog descriptions for both platforms. --- .../inputs/winget/scripts/vivaldi_install.ps1 | 7 +++++-- .../inputs/winget/scripts/vivaldi_uninstall.ps1 | 4 +++- ee/maintained-apps/inputs/winget/vivaldi.json | 2 +- ee/maintained-apps/outputs/apps.json | 14 ++++++++++++++ ee/maintained-apps/outputs/vivaldi/darwin.json | 15 ++++++++------- ee/maintained-apps/outputs/vivaldi/windows.json | 17 +++++++++-------- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 index 6c0ecf578f8..6205f13747e 100644 --- a/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 @@ -1,5 +1,8 @@ -# Install Vivaldi silently (user-scoped Chromium-based browser) +# Install Vivaldi silently, machine-wide (Chromium-based browser). +# Fleet runs installs as SYSTEM, so --system-level is required to install for +# all users under %ProgramFiles% (and register under HKLM). Without it the +# installer lands in the SYSTEM profile and is invisible to the real user. $process = Start-Process -FilePath $env:INSTALLER_PATH ` - -ArgumentList "--vivaldi-silent --do-not-launch-chrome" ` + -ArgumentList "--vivaldi-silent --do-not-launch-chrome --system-level" ` -NoNewWindow -PassThru -Wait Exit $process.ExitCode diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 index c353a0dbc5e..d4bd6c1b11d 100644 --- a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 @@ -1,4 +1,6 @@ -# Uninstall Vivaldi (user-scoped Chromium-based browser) +# Uninstall Vivaldi (machine-wide Chromium-based browser). +# Looks up the uninstall entry under HKLM (machine install) with an HKCU +# fallback, then runs the Chromium uninstaller with --force-uninstall. $displayName = "Vivaldi" diff --git a/ee/maintained-apps/inputs/winget/vivaldi.json b/ee/maintained-apps/inputs/winget/vivaldi.json index 7228f380902..17aa4d0aefe 100644 --- a/ee/maintained-apps/inputs/winget/vivaldi.json +++ b/ee/maintained-apps/inputs/winget/vivaldi.json @@ -7,6 +7,6 @@ "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1", "installer_arch": "x64", "installer_type": "exe", - "installer_scope": "user", + "installer_scope": "machine", "default_categories": ["Browsers"] } diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index 9ee37e279ea..64b23f23451 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -6000,6 +6000,20 @@ "unique_identifier": "Microsoft Visual Studio Code", "description": "Microsoft Visual Studio Code (VS Code) is an open-source, lightweight, and powerful code editor." }, + { + "name": "Vivaldi", + "slug": "vivaldi/darwin", + "platform": "darwin", + "unique_identifier": "com.vivaldi.Vivaldi", + "description": "Vivaldi is a customizable, privacy-focused web browser built on Chromium." + }, + { + "name": "Vivaldi", + "slug": "vivaldi/windows", + "platform": "windows", + "unique_identifier": "Vivaldi", + "description": "Vivaldi is a customizable, privacy-focused web browser built on Chromium." + }, { "name": "Vivid", "slug": "vivid-app/darwin", diff --git a/ee/maintained-apps/outputs/vivaldi/darwin.json b/ee/maintained-apps/outputs/vivaldi/darwin.json index aaee409614c..7a919d43ed2 100644 --- a/ee/maintained-apps/outputs/vivaldi/darwin.json +++ b/ee/maintained-apps/outputs/vivaldi/darwin.json @@ -1,13 +1,14 @@ { "versions": [ { - "version": "7.8.3925.76", + "version": "8.0.4033.46", "queries": { - "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.vivaldi.Vivaldi';" + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'com.vivaldi.Vivaldi';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM apps WHERE bundle_identifier = 'com.vivaldi.Vivaldi' AND version_compare(bundle_short_version, '8.0.4033.46') < 0);" }, - "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.76.universal.dmg", - "install_script_ref": "d8e37a25", - "uninstall_script_ref": "83339974", + "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.8.0.4033.46.universal.dmg", + "install_script_ref": "6a60dca7", + "uninstall_script_ref": "7c1876ca", "sha256": "no_check", "default_categories": [ "Browsers" @@ -15,7 +16,7 @@ } ], "refs": { - "83339974": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nquit_application 'com.vivaldi.Vivaldi'\nsudo rm -rf \"$APPDIR/Vivaldi.app\"\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/Vivaldi Helper (Renderer)_*.diag'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/Vivaldi_*.diag'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/Vivaldi_*.plist'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.vivaldi.Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Caches/Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.vivaldi.Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.vivaldi.Vivaldi.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.vivaldi.Vivaldi.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.vivaldi.Vivaldi'\n", - "d8e37a25": "#!/bin/sh\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath $INSTALLER_PATH)\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'com.vivaldi.Vivaldi'\nif [ -d \"$APPDIR/Vivaldi.app\" ]; then\n\tsudo mv \"$APPDIR/Vivaldi.app\" \"$TMPDIR/Vivaldi.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Vivaldi.app\" \"$APPDIR\"\nrelaunch_application 'com.vivaldi.Vivaldi'\n" + "6a60dca7": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'com.vivaldi.Vivaldi'\nif [ -d \"$APPDIR/Vivaldi.app\" ]; then\n\tsudo mv \"$APPDIR/Vivaldi.app\" \"$TMPDIR/Vivaldi.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Vivaldi.app\" \"$APPDIR\"\nrelaunch_application 'com.vivaldi.Vivaldi'\n", + "7c1876ca": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nquit_application 'com.vivaldi.Vivaldi'\nsudo rm -rf \"$APPDIR/Vivaldi.app\"\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/Vivaldi Helper (Renderer)_*.diag'\ntrash $LOGGED_IN_USER '/Library/Logs/DiagnosticReports/Vivaldi_*.diag'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashReporter/Vivaldi_*.plist'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.vivaldi.Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Caches/Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.vivaldi.Vivaldi'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.vivaldi.Vivaldi.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.vivaldi.Vivaldi.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.vivaldi.Vivaldi'\n" } } diff --git a/ee/maintained-apps/outputs/vivaldi/windows.json b/ee/maintained-apps/outputs/vivaldi/windows.json index a10b79d9af2..46422aa56fd 100644 --- a/ee/maintained-apps/outputs/vivaldi/windows.json +++ b/ee/maintained-apps/outputs/vivaldi/windows.json @@ -1,21 +1,22 @@ { "versions": [ { - "version": "7.8.3925.81", + "version": "8.0.4033.46", "queries": { - "exists": "SELECT 1 FROM programs WHERE name = 'Vivaldi' AND publisher = 'Vivaldi Technologies AS.';" + "exists": "SELECT 1 FROM programs WHERE name = 'Vivaldi' AND publisher = 'Vivaldi Technologies AS.';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Vivaldi' AND publisher = 'Vivaldi Technologies AS.' AND version_compare(version, '8.0.4033.46') < 0);" }, - "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.7.8.3925.81.x64.exe", - "install_script_ref": "81b0529a", - "uninstall_script_ref": "dd74a7d3", - "sha256": "4d9ad6cffc65e644fbdd3896634b27be6d2fd6062a7adb4895f0fe4c0f260b5f", + "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.8.0.4033.46.x64.exe", + "install_script_ref": "d5d49757", + "uninstall_script_ref": "741609fc", + "sha256": "35ab32e990973e1896df8587f68b480fb4d89512030e9228d0ec39b3cda1b958", "default_categories": [ "Browsers" ] } ], "refs": { - "81b0529a": "# Install Vivaldi silently (user-scoped Chromium-based browser)\n$process = Start-Process -FilePath $env:INSTALLER_PATH `\n -ArgumentList \"--vivaldi-silent --do-not-launch-chrome\" `\n -NoNewWindow -PassThru -Wait\nExit $process.ExitCode\n", - "dd74a7d3": "# Uninstall Vivaldi (user-scoped Chromium-based browser)\n\n$displayName = \"Vivaldi\"\n\n$paths = @(\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -and $_.DisplayName -eq $displayName\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for '$displayName'\"\n Exit 1\n}\n\n# Kill any running Vivaldi processes before uninstalling\nStop-Process -Name \"vivaldi\" -Force -ErrorAction SilentlyContinue\nStart-Sleep -Seconds 2\n\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# Parse quoted executable from any trailing args in the registry string\n$splitArgs = $uninstallCommand.Split('\"')\n$exe = $splitArgs[1]\n$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { \"\" }\n\n# Chromium-based uninstaller flags\n$uninstallArgs = \"$existingArgs --uninstall --force-uninstall\".Trim()\n\nWrite-Host \"Uninstall command: $exe\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\ntry {\n $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n\n # Chromium uninstallers return 19 on success\n if ($exitCode -eq 0 -or $exitCode -eq 19) {\n Exit 0\n }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n" + "741609fc": "# Uninstall Vivaldi (machine-wide Chromium-based browser).\n# Looks up the uninstall entry under HKLM (machine install) with an HKCU\n# fallback, then runs the Chromium uninstaller with --force-uninstall.\n\n$displayName = \"Vivaldi\"\n\n$paths = @(\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -and $_.DisplayName -eq $displayName\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for '$displayName'\"\n Exit 1\n}\n\n# Kill any running Vivaldi processes before uninstalling\nStop-Process -Name \"vivaldi\" -Force -ErrorAction SilentlyContinue\nStart-Sleep -Seconds 2\n\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# Parse quoted executable from any trailing args in the registry string\n$splitArgs = $uninstallCommand.Split('\"')\n$exe = $splitArgs[1]\n$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { \"\" }\n\n# Chromium-based uninstaller flags\n$uninstallArgs = \"$existingArgs --uninstall --force-uninstall\".Trim()\n\nWrite-Host \"Uninstall command: $exe\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\ntry {\n $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n\n # Chromium uninstallers return 19 on success\n if ($exitCode -eq 0 -or $exitCode -eq 19) {\n Exit 0\n }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n", + "d5d49757": "# Install Vivaldi silently, machine-wide (Chromium-based browser).\n# Fleet runs installs as SYSTEM, so --system-level is required to install for\n# all users under %ProgramFiles% (and register under HKLM). Without it the\n# installer lands in the SYSTEM profile and is invisible to the real user.\n$process = Start-Process -FilePath $env:INSTALLER_PATH `\n -ArgumentList \"--vivaldi-silent --do-not-launch-chrome --system-level\" `\n -NoNewWindow -PassThru -Wait\nExit $process.ExitCode\n" } } From b313ee504ce1fe77be717116f4c0b0d41badcf1b Mon Sep 17 00:00:00 2001 From: Mitch Francese <2227948+tux234@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:25:28 -0400 Subject: [PATCH 7/8] Address CodeRabbit: HKLM-first uninstall lookup + robust UninstallString parsing - Reorder the Windows uninstall registry search to HKLM-first (machine-wide --system-level install registers under HKLM), with HKCU as a stale-entry fallback, and require Publisher 'Vivaldi Technologies AS.' in addition to DisplayName to avoid matching the wrong entry. - Replace the brittle UninstallString .Split('"') parsing with the canonical three-case defensive regex (quoted / unquoted-with-spaces / bare token), so unquoted registry commands no longer break Start-Process. - Regenerate vivaldi/windows.json (content-addressed uninstall ref). --- .../winget/scripts/vivaldi_uninstall.ps1 | 28 ++++++++++++++----- .../outputs/vivaldi/windows.json | 4 +-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 index d4bd6c1b11d..c9f10b1a0c4 100644 --- a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 @@ -3,17 +3,20 @@ # fallback, then runs the Chromium uninstaller with --force-uninstall. $displayName = "Vivaldi" +$publisher = "Vivaldi Technologies AS." +# Install is machine-wide (--system-level), which registers under HKLM, so look +# there first. HKCU is only a fallback for a stale user-level install. $paths = @( - 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', - 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' ) $uninstall = $null foreach ($p in $paths) { $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object { - $_.DisplayName -and $_.DisplayName -eq $displayName + $_.DisplayName -eq $displayName -and $_.Publisher -eq $publisher } if ($items) { $uninstall = $items | Select-Object -First 1; break } } @@ -33,10 +36,21 @@ $uninstallCommand = if ($uninstall.QuietUninstallString) { $uninstall.UninstallString } -# Parse quoted executable from any trailing args in the registry string -$splitArgs = $uninstallCommand.Split('"') -$exe = $splitArgs[1] -$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" } +# Parse the executable + trailing args, handling the three registry shapes: +# quoted, unquoted-with-spaces (capture through .exe), and a bare token. +if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') { + $exe = $Matches[1] + $existingArgs = $Matches[2].Trim() +} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') { + $exe = $Matches[1] + $existingArgs = $Matches[2].Trim() +} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') { + $exe = $Matches[1] + $existingArgs = $Matches[2].Trim() +} else { + Write-Host "Unable to parse uninstall command: $uninstallCommand" + Exit 1 +} # Chromium-based uninstaller flags $uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim() diff --git a/ee/maintained-apps/outputs/vivaldi/windows.json b/ee/maintained-apps/outputs/vivaldi/windows.json index 46422aa56fd..b24a0b1bc7f 100644 --- a/ee/maintained-apps/outputs/vivaldi/windows.json +++ b/ee/maintained-apps/outputs/vivaldi/windows.json @@ -8,7 +8,7 @@ }, "installer_url": "https://downloads.vivaldi.com/stable/Vivaldi.8.0.4033.46.x64.exe", "install_script_ref": "d5d49757", - "uninstall_script_ref": "741609fc", + "uninstall_script_ref": "7b14fec4", "sha256": "35ab32e990973e1896df8587f68b480fb4d89512030e9228d0ec39b3cda1b958", "default_categories": [ "Browsers" @@ -16,7 +16,7 @@ } ], "refs": { - "741609fc": "# Uninstall Vivaldi (machine-wide Chromium-based browser).\n# Looks up the uninstall entry under HKLM (machine install) with an HKCU\n# fallback, then runs the Chromium uninstaller with --force-uninstall.\n\n$displayName = \"Vivaldi\"\n\n$paths = @(\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -and $_.DisplayName -eq $displayName\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for '$displayName'\"\n Exit 1\n}\n\n# Kill any running Vivaldi processes before uninstalling\nStop-Process -Name \"vivaldi\" -Force -ErrorAction SilentlyContinue\nStart-Sleep -Seconds 2\n\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# Parse quoted executable from any trailing args in the registry string\n$splitArgs = $uninstallCommand.Split('\"')\n$exe = $splitArgs[1]\n$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { \"\" }\n\n# Chromium-based uninstaller flags\n$uninstallArgs = \"$existingArgs --uninstall --force-uninstall\".Trim()\n\nWrite-Host \"Uninstall command: $exe\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\ntry {\n $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n\n # Chromium uninstallers return 19 on success\n if ($exitCode -eq 0 -or $exitCode -eq 19) {\n Exit 0\n }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n", + "7b14fec4": "# Uninstall Vivaldi (machine-wide Chromium-based browser).\n# Looks up the uninstall entry under HKLM (machine install) with an HKCU\n# fallback, then runs the Chromium uninstaller with --force-uninstall.\n\n$displayName = \"Vivaldi\"\n$publisher = \"Vivaldi Technologies AS.\"\n\n# Install is machine-wide (--system-level), which registers under HKLM, so look\n# there first. HKCU is only a fallback for a stale user-level install.\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -eq $displayName -and $_.Publisher -eq $publisher\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for '$displayName'\"\n Exit 1\n}\n\n# Kill any running Vivaldi processes before uninstalling\nStop-Process -Name \"vivaldi\" -Force -ErrorAction SilentlyContinue\nStart-Sleep -Seconds 2\n\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# Parse the executable + trailing args, handling the three registry shapes:\n# quoted, unquoted-with-spaces (capture through .exe), and a bare token.\nif ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $exe = $Matches[1]\n $existingArgs = $Matches[2].Trim()\n} elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $exe = $Matches[1]\n $existingArgs = $Matches[2].Trim()\n} elseif ($uninstallCommand -match '^\\s*(\\S+)\\s*(.*)$') {\n $exe = $Matches[1]\n $existingArgs = $Matches[2].Trim()\n} else {\n Write-Host \"Unable to parse uninstall command: $uninstallCommand\"\n Exit 1\n}\n\n# Chromium-based uninstaller flags\n$uninstallArgs = \"$existingArgs --uninstall --force-uninstall\".Trim()\n\nWrite-Host \"Uninstall command: $exe\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\ntry {\n $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n\n # Chromium uninstallers return 19 on success\n if ($exitCode -eq 0 -or $exitCode -eq 19) {\n Exit 0\n }\n Exit $exitCode\n} catch {\n Write-Host \"Error running uninstaller: $_\"\n Exit 1\n}\n", "d5d49757": "# Install Vivaldi silently, machine-wide (Chromium-based browser).\n# Fleet runs installs as SYSTEM, so --system-level is required to install for\n# all users under %ProgramFiles% (and register under HKLM). Without it the\n# installer lands in the SYSTEM profile and is invisible to the real user.\n$process = Start-Process -FilePath $env:INSTALLER_PATH `\n -ArgumentList \"--vivaldi-silent --do-not-launch-chrome --system-level\" `\n -NoNewWindow -PassThru -Wait\nExit $process.ExitCode\n" } } From 559931c51e73bd39103c114eb181fc94183047fd Mon Sep 17 00:00:00 2001 From: Allen Houchins <32207388+allenhouchins@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:14:24 -0500 Subject: [PATCH 8/8] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 index c9f10b1a0c4..58ef2671b66 100644 --- a/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 @@ -23,7 +23,7 @@ foreach ($p in $paths) { if (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) { Write-Host "Uninstall entry not found for '$displayName'" - Exit 1 + Exit 0 } # Kill any running Vivaldi processes before uninstalling