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:
kitzy
2026-08-05 14:04:49 -05:00
committed by GitHub
co-authored by Claude Opus 5 Allen Houchins
parent 92aaf1de81
commit ecdf1ff003
6 changed files with 168 additions and 0 deletions
@@ -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"
]
}
+7
View File
@@ -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