Add TeamViewer Host as a Windows Fleet-maintained app (#50553)
**Related issue:** Resolves #50332 Adds **TeamViewer Host** as a Windows Fleet-maintained app. ## The issue's premise was wrong — this needed no new capability #50332 was blocked on "not in winget." It is in winget: `TeamViewer.TeamViewer.Host`, published continuously since **June 2023** (v15.42.8), currently **15.80.4** (merged upstream as microsoft/winget-pkgs#409335, 2026-07-29). I downloaded the live x64 installer and its SHA256 matches that manifest byte-for-byte. It reads as absent because it's nested a level deeper than you'd expect — `manifests/t/TeamViewer/TeamViewer/Host/`, a sibling of the full client's *version* directories rather than of the publisher's package directories. Consequence: this comes off the dependency on #50364, and that FR loses one of its three examples. The other two (#50328, #50329, OLE DB Driver 18/19) still hold — there is no OLE DB package under any winget publisher. ## Identity verified against the installer, not winget metadata Per the `new-fma` golden rule, from the x64 MSI's `Property` and `Registry` tables: | Field | Value | Evidence | |---|---|---| | `unique_identifier` | `TeamViewer Host` | MSI `ProductName`; no `ARPDISPLAYNAME`, empty `Registry` table, so this is the ARP DisplayName | | publisher | `TeamViewer` | MSI `Manufacturer`, equal to the winget locale `Publisher` → no `program_publisher` override | | `installer_scope` | `machine` | `ALLUSERS=1` | | bootstrapper? | No | no `ARPSYSTEMCOMPONENT` | Corroborated independently by [silentinstallhq's PSADT script](https://silentinstallhq.com/teamviewer-host-install-and-uninstall-powershell/), which detects the app with `Get-InstalledApplication -Name 'TeamViewer Host'` and uses `/S` for both install and uninstall. Generated exists query: ```sql SELECT 1 FROM programs WHERE name = 'TeamViewer Host' AND publisher = 'TeamViewer'; ``` No collision with the existing `teamviewer/windows` FMA, which generates an exact `name = 'TeamViewer'`. ## Why exe + `ignore_hash`, matching the full client winget offers Host as an NSIS exe and as a nested `wix` MSI inside a zip. The ingester can't select the zip (`installer.InstallerType` is `zip`, which never normalizes to `msi`), so the exe is the only reachable installer. TeamViewer publishes no version-pinned Host exe — `TeamViewer_Host_Setup_x64_15.80.4.exe` and the x86 equivalent both 404 — so the manifest's URL is the unpinned `TeamViewer_Host_Setup_x64.exe` and `ignore_hash: true` is required. This is vendor asymmetry, not a fixable winget defect: the *full* client does publish pinned exe URLs. Same reason `teamviewer/windows` already sets `ignore_hash`. ## Correcting the coexistence note in #50332 The issue assumed Host and the full client can co-exist. They can't. The Host MSI's `LaunchCondition` table blocks the install outright: > Error 25001: An incompatible TeamViewer package was detected that conflicts with the current MSI package: TeamViewer_Full 64-bit. Please manually uninstall this package. …plus equivalents for Full 32-bit/ARM64, Host ARM64, and the NSIS installs. **Relevant to validation: the validator host must not already have `teamviewer/windows` installed.** ## Icons No `index.ts` change needed — `matchLoosePrefixToKey` treats keys as whole words at the start, so `"teamviewer host"` matches the existing `teamviewer` key and inherits the TeamViewer brand icon. Verified against the real 1,163-key map. The website resolves its icon from the slug (`app-icon-${slug}-60x60@2x.png`) with no such fallback, so `app-icon-teamviewer-host-60x60@2x.png` is added — a copy of the existing TeamViewer brand asset, since Host ships the same logo. ## Two things for review 1. **The PowerShell is unverified.** Authored on macOS with no PowerShell available, so install/uninstall have not been executed. The uninstall script searches ARP by DisplayName instead of a hardcoded key, and uses the three-shape `UninstallString` parser (TeamViewer's is unquoted and contains a space in `C:\Program Files\...`, which the older `.Split('"')` approach in `teamviewer_uninstall.ps1` mishandles). Validator run is the real check. 2. **Category mismatch with the macOS side.** This uses `Communication`, matching the merged `teamviewer/windows`. #47121 adds `teamviewer-host/darwin` with `Productivity`. Worth reconciling — and that PR will want this same website PNG, so expect a trivial conflict. `name` is `TeamViewer Host`, matching #47121 so both platforms group together in the FMA library. ## Testing - [x] `go test ./cmd/maintained-apps/... ./ee/maintained-apps/...` passes - [x] Generator is idempotent — re-running produces no diff and preserves the `apps.json` description - [x] `apps.json` is valid JSON with no empty descriptions - [x] Live installer SHA256 confirmed against the winget manifest - [ ] FMA validator: install → detect → uninstall on a Windows host **(pending — needs a host without the full TeamViewer client)** No shared code changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Allen Houchins <allenhouchins@mac.com>
This commit is contained in:
co-authored by
Claude Opus 5
Allen Houchins
parent
92aaf1de81
commit
ecdf1ff003
@@ -0,0 +1,29 @@
|
||||
# Learn more about .exe install scripts:
|
||||
# http://fleetdm.com/learn-more-about/exe-install-scripts
|
||||
|
||||
$exeFilePath = "${env:INSTALLER_PATH}"
|
||||
|
||||
try {
|
||||
|
||||
# TeamViewer Host ships as an NSIS installer, which takes "/S" for a silent
|
||||
# install. The MSI underneath sets ALLUSERS=1, so this always installs
|
||||
# machine-wide.
|
||||
$processOptions = @{
|
||||
FilePath = "$exeFilePath"
|
||||
ArgumentList = "/S"
|
||||
PassThru = $true
|
||||
Wait = $true
|
||||
}
|
||||
|
||||
# Start process and track exit code
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# Prints the exit code
|
||||
Write-Host "Install exit code: $exitCode"
|
||||
Exit $exitCode
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
Exit 1
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# TeamViewer Host registers itself in Add/Remove Programs with the DisplayName
|
||||
# "TeamViewer Host" (the MSI ProductName; there is no ARPDISPLAYNAME override)
|
||||
# and the Publisher "TeamViewer" (the MSI Manufacturer).
|
||||
#
|
||||
# The trailing wildcard tolerates a version suffix if TeamViewer ever adds one,
|
||||
# and the "Host" in the pattern keeps this from matching the full TeamViewer
|
||||
# client, which registers as plain "TeamViewer" and ships as its own
|
||||
# Fleet-maintained app (teamviewer/windows).
|
||||
$softwareNameLike = "TeamViewer Host*"
|
||||
|
||||
# Matched with a trailing wildcard so this filter stays strictly looser than the
|
||||
# app's exists query (publisher = 'TeamViewer'). Uninstall must never be more
|
||||
# selective than detection, or the app reports installed with no way to remove it.
|
||||
$publisherLike = "TeamViewer*"
|
||||
|
||||
$paths = @(
|
||||
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
|
||||
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
|
||||
)
|
||||
|
||||
try {
|
||||
|
||||
$uninstall = $null
|
||||
foreach ($p in $paths) {
|
||||
$items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.DisplayName -like $softwareNameLike -and $_.Publisher -like $publisherLike
|
||||
}
|
||||
if ($items) { $uninstall = $items | Select-Object -First 1; break }
|
||||
}
|
||||
|
||||
if (-not $uninstall) {
|
||||
Write-Host "Uninstall entry not found for $softwareNameLike"
|
||||
Exit 0
|
||||
}
|
||||
|
||||
if (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString) {
|
||||
Write-Host "Error: uninstall entry for $($uninstall.DisplayName) has no UninstallString or QuietUninstallString"
|
||||
Exit 1
|
||||
}
|
||||
|
||||
# Get the uninstall command. Some uninstallers do not include
|
||||
# 'QuietUninstallString' and require a flag to run silently.
|
||||
$uninstallCommand = if ($uninstall.QuietUninstallString) {
|
||||
$uninstall.QuietUninstallString
|
||||
} else {
|
||||
$uninstall.UninstallString
|
||||
}
|
||||
|
||||
# UninstallString comes in three shapes. TeamViewer's is unquoted and
|
||||
# contains a space ("C:\Program Files\TeamViewer\uninstall.exe"), so
|
||||
# capture through the .exe rather than splitting on the first space.
|
||||
$existingArgs = ''
|
||||
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
|
||||
# Quoted path, optionally followed by args
|
||||
$uninstallCommand = $Matches[1]
|
||||
$existingArgs = $Matches[2].Trim()
|
||||
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
|
||||
# Unquoted path that may contain spaces
|
||||
$uninstallCommand = $Matches[1]
|
||||
$existingArgs = $Matches[2].Trim()
|
||||
} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
|
||||
# Bare token (e.g. MsiExec.exe /X{GUID})
|
||||
$uninstallCommand = $Matches[1]
|
||||
$existingArgs = $Matches[2].Trim()
|
||||
} else {
|
||||
Throw "Could not parse uninstall string: $uninstallCommand"
|
||||
}
|
||||
|
||||
# NSIS installers require /S for a silent uninstall. Append it unless the
|
||||
# registered command already runs silently.
|
||||
if ($existingArgs -notmatch '(?i)(^|\s)/S(\s|$)') {
|
||||
$uninstallArgs = "$existingArgs /S".Trim()
|
||||
} else {
|
||||
$uninstallArgs = $existingArgs
|
||||
}
|
||||
|
||||
Write-Host "Uninstall command: $uninstallCommand"
|
||||
Write-Host "Uninstall args: $uninstallArgs"
|
||||
|
||||
$processOptions = @{
|
||||
FilePath = $uninstallCommand
|
||||
ArgumentList = $uninstallArgs
|
||||
PassThru = $true
|
||||
Wait = $true
|
||||
}
|
||||
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode
|
||||
Write-Host "Uninstall exit code: $exitCode"
|
||||
Exit $exitCode
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
Exit 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "TeamViewer Host",
|
||||
"slug": "teamviewer-host/windows",
|
||||
"package_identifier": "TeamViewer.TeamViewer.Host",
|
||||
"unique_identifier": "TeamViewer Host",
|
||||
"install_script_path": "ee/maintained-apps/inputs/winget/scripts/teamviewer-host_install.ps1",
|
||||
"uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/teamviewer-host_uninstall.ps1",
|
||||
"installer_arch": "x64",
|
||||
"installer_type": "exe",
|
||||
"installer_scope": "machine",
|
||||
"ignore_hash": true,
|
||||
"default_categories": [
|
||||
"Communication"
|
||||
]
|
||||
}
|
||||
@@ -8436,6 +8436,13 @@
|
||||
"unique_identifier": "com.apptorium.TeaCode-dm",
|
||||
"description": "TeaCode is a text expanding app for developers."
|
||||
},
|
||||
{
|
||||
"name": "TeamViewer Host",
|
||||
"slug": "teamviewer-host/windows",
|
||||
"platform": "windows",
|
||||
"unique_identifier": "TeamViewer Host",
|
||||
"description": "TeamViewer Host is a remote access agent for unattended 24/7 access to a device, for uses such as remote device monitoring and server maintenance."
|
||||
},
|
||||
{
|
||||
"name": "TeamViewer",
|
||||
"slug": "teamviewer/darwin",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "15.80.4",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM programs WHERE name = 'TeamViewer Host' AND publisher = 'TeamViewer';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'TeamViewer Host' AND publisher = 'TeamViewer' AND version_compare(version, '15.80.4') < 0);"
|
||||
},
|
||||
"installer_url": "https://download.teamviewer.com/download/version_15x/TeamViewer_Host_Setup_x64.exe",
|
||||
"install_script_ref": "0c887797",
|
||||
"uninstall_script_ref": "6e2af20a",
|
||||
"sha256": "no_check",
|
||||
"default_categories": [
|
||||
"Communication"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"0c887797": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# TeamViewer Host ships as an NSIS installer, which takes \"/S\" for a silent\n# install. The MSI underneath sets ALLUSERS=1, so this always installs\n# machine-wide.\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/S\"\n PassThru = $true\n Wait = $true\n}\n\n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n",
|
||||
"6e2af20a": "# TeamViewer Host registers itself in Add/Remove Programs with the DisplayName\n# \"TeamViewer Host\" (the MSI ProductName; there is no ARPDISPLAYNAME override)\n# and the Publisher \"TeamViewer\" (the MSI Manufacturer).\n#\n# The trailing wildcard tolerates a version suffix if TeamViewer ever adds one,\n# and the \"Host\" in the pattern keeps this from matching the full TeamViewer\n# client, which registers as plain \"TeamViewer\" and ships as its own\n# Fleet-maintained app (teamviewer/windows).\n$softwareNameLike = \"TeamViewer Host*\"\n\n# Matched with a trailing wildcard so this filter stays strictly looser than the\n# app's exists query (publisher = 'TeamViewer'). Uninstall must never be more\n# selective than detection, or the app reports installed with no way to remove it.\n$publisherLike = \"TeamViewer*\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\ntry {\n\n$uninstall = $null\nforeach ($p in $paths) {\n $items = Get-ItemProperty \"$p\\*\" -ErrorAction SilentlyContinue | Where-Object {\n $_.DisplayName -like $softwareNameLike -and $_.Publisher -like $publisherLike\n }\n if ($items) { $uninstall = $items | Select-Object -First 1; break }\n}\n\nif (-not $uninstall) {\n Write-Host \"Uninstall entry not found for $softwareNameLike\"\n Exit 0\n}\n\nif (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString) {\n Write-Host \"Error: uninstall entry for $($uninstall.DisplayName) has no UninstallString or QuietUninstallString\"\n Exit 1\n}\n\n# Get the uninstall command. Some uninstallers do not include\n# 'QuietUninstallString' and require a flag to run silently.\n$uninstallCommand = if ($uninstall.QuietUninstallString) {\n $uninstall.QuietUninstallString\n} else {\n $uninstall.UninstallString\n}\n\n# UninstallString comes in three shapes. TeamViewer's is unquoted and\n# contains a space (\"C:\\Program Files\\TeamViewer\\uninstall.exe\"), so\n# capture through the .exe rather than splitting on the first space.\n$existingArgs = ''\nif ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n # Quoted path, optionally followed by args\n $uninstallCommand = $Matches[1]\n $existingArgs = $Matches[2].Trim()\n} elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n # Unquoted path that may contain spaces\n $uninstallCommand = $Matches[1]\n $existingArgs = $Matches[2].Trim()\n} elseif ($uninstallCommand -match '^\\s*(\\S+)\\s*(.*)$') {\n # Bare token (e.g. MsiExec.exe /X{GUID})\n $uninstallCommand = $Matches[1]\n $existingArgs = $Matches[2].Trim()\n} else {\n Throw \"Could not parse uninstall string: $uninstallCommand\"\n}\n\n# NSIS installers require /S for a silent uninstall. Append it unless the\n# registered command already runs silently.\nif ($existingArgs -notmatch '(?i)(^|\\s)/S(\\s|$)') {\n $uninstallArgs = \"$existingArgs /S\".Trim()\n} else {\n $uninstallArgs = $existingArgs\n}\n\nWrite-Host \"Uninstall command: $uninstallCommand\"\nWrite-Host \"Uninstall args: $uninstallArgs\"\n\n$processOptions = @{\n FilePath = $uninstallCommand\n ArgumentList = $uninstallArgs\n PassThru = $true\n Wait = $true\n}\n\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\nWrite-Host \"Uninstall exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Reference in New Issue
Block a user