Add Lenovo System Update as a Windows FMA (#50339)
**Related issue:** Resolves #50324 Adds Lenovo System Update as a Windows Fleet-maintained app, from winget `Lenovo.SystemUpdate` (5.08.03.59, Inno Setup, machine scope, x86-only). Found in a customer's ManageEngine ServiceDesk Plus Windows deployment catalog with no Fleet equivalent. Distinct from `lenovo-dock-manager/windows`, which we already ship. ## Verification - Installer SHA confirmed against a local download of `system_update_5.08.03.59.exe` (`e66794dc…53e0d`), served from `download.lenovo.com` — a pinned vendor URL, so none of the SourceForge mirror trouble from #50322 applies. - Registry `DisplayName` determined offline as a bare `Lenovo System Update`: `innoextract --info` reports `AppVerName` when set and falls back to `AppName`, and Inno writes that same value to `DisplayName`. This installer reports no version suffix, unlike CrystalDiskMark in #50322 which reports `"CrystalDiskMark 9.0.3"`. That is why the exists query here is an exact match rather than a prefix. - Icon extracted from the installer's own `Tvsukernel.exe` resource, not sourced from the web. - The uninstall script targets the Inno registry key directly via the manifest's `ProductCode` (`TVSU_is1` — a key name, not a GUID) using the `$PACKAGE_ID` substitution, rather than string-matching `DisplayName`. ## Two things reviewers should weigh in on **1. The exists query deliberately omits the publisher.** House style usually pins `publisher = '...'`, but the registry `Publisher` is not determinable offline for Inno, and the validator's log prints only the name and version — so I could not confirm it. A wrong publisher makes the exists query silently never match while the validator still passes, which is the exact failure mode called out in the FMA docs. `name = 'Lenovo System Update'` is unambiguous on its own. Happy to add the publisher clause if someone can confirm the registry value on a real Lenovo host. **2. This may not be validatable on the CI runner.** Lenovo System Update is a vendor tool for Lenovo hardware, and the runner is a generic Azure VM. If the installer refuses to run on non-Lenovo hardware this will fail the way Dell Display and Peripheral Manager did in #50020 (which was dropped for exactly this reason, and is being retried on a client-OS runner in #50313). Leaving this in draft until the validator reports. # Checklist for submitter - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Lenovo System Update to the maintained Windows software catalog. * Added support for silent installation and uninstallation, including status verification and reboot-success handling. * Added Lenovo System Update metadata, download information, categorization, and application icon. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Lenovo System Update",
|
||||
"slug": "lenovo-system-update/windows",
|
||||
"package_identifier": "Lenovo.SystemUpdate",
|
||||
"unique_identifier": "Lenovo System Update",
|
||||
"exists_query": "SELECT 1 FROM programs WHERE name = 'Lenovo System Update';",
|
||||
"install_script_path": "ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_install.ps1",
|
||||
"uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_uninstall.ps1",
|
||||
"installer_arch": "x86",
|
||||
"installer_type": "exe",
|
||||
"installer_scope": "machine",
|
||||
"default_categories": ["Productivity"]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# Learn more about .exe install scripts:
|
||||
# http://fleetdm.com/learn-more-about/exe-install-scripts
|
||||
|
||||
$exeFilePath = "${env:INSTALLER_PATH}"
|
||||
|
||||
$installTimeoutSeconds = 420
|
||||
$registrationTimeoutSeconds = 120
|
||||
|
||||
# The installer is x86, so on 64-bit Windows it registers under Wow6432Node.
|
||||
$machineKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
$machineKey32on64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
|
||||
function Get-LenovoSystemUpdateEntry {
|
||||
Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |
|
||||
Where-Object { $_.DisplayName -eq "Lenovo System Update" } |
|
||||
Select-Object -First 1
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
# -Wait also waits on descendants, so wait on the installer process alone.
|
||||
$process = Start-Process -FilePath "$exeFilePath" `
|
||||
-ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" `
|
||||
-PassThru
|
||||
# Keeps .ExitCode readable after the process ends.
|
||||
$null = $process.Handle
|
||||
|
||||
$killed = $false
|
||||
if (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {
|
||||
Write-Host "Installer process did not exit within ${installTimeoutSeconds}s, stopping it."
|
||||
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
||||
$null = $process.WaitForExit(30 * 1000)
|
||||
$killed = $true
|
||||
}
|
||||
|
||||
$exitCode = $null
|
||||
if ($process.HasExited) {
|
||||
$exitCode = $process.ExitCode
|
||||
Write-Host "Install exit code: $exitCode"
|
||||
}
|
||||
|
||||
# The installer can return before the ARP entry is written.
|
||||
$elapsed = 0
|
||||
while (-not (Get-LenovoSystemUpdateEntry) -and ($elapsed -lt $registrationTimeoutSeconds)) {
|
||||
Start-Sleep -Seconds 5
|
||||
$elapsed += 5
|
||||
Write-Host "Waiting for Lenovo System Update to register... ($elapsed seconds)"
|
||||
}
|
||||
|
||||
$entry = Get-LenovoSystemUpdateEntry
|
||||
if (-not $entry) {
|
||||
Write-Host "Lenovo System Update did not register in Add/Remove Programs."
|
||||
Exit 1
|
||||
}
|
||||
Write-Host "Registered '$($entry.DisplayName)' by '$($entry.Publisher)', version $($entry.DisplayVersion)."
|
||||
|
||||
# Registration above is the success signal; a killed process's code means nothing.
|
||||
if ($killed -or $null -eq $exitCode) { Exit 0 }
|
||||
|
||||
# 3010 (reboot required) and 1641 (reboot initiated) are successful installs.
|
||||
if ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }
|
||||
|
||||
Exit $exitCode
|
||||
|
||||
} catch {
|
||||
Write-Host "Error: $_"
|
||||
Exit 1
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Fleet substitutes the winget ProductCode, which for this Inno installer is the
|
||||
# uninstall registry key name rather than a GUID.
|
||||
$packageId = $PACKAGE_ID
|
||||
$uninstallArgs = "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART"
|
||||
|
||||
# The installer is x86, so on 64-bit Windows it registers under Wow6432Node.
|
||||
$paths = @(
|
||||
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$packageId",
|
||||
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$packageId"
|
||||
)
|
||||
$exitCode = 0
|
||||
|
||||
try {
|
||||
$key = $paths |
|
||||
ForEach-Object { Get-ItemProperty -Path $_ -ErrorAction SilentlyContinue } |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $key) { Write-Host "Uninstall entry not found for '$packageId'."; Exit 0 }
|
||||
|
||||
$uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
|
||||
if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
|
||||
$uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
|
||||
} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
|
||||
$uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
|
||||
} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
|
||||
$uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = "$($Matches[2]) $uninstallArgs".Trim() }
|
||||
}
|
||||
|
||||
Write-Host "Uninstall command: $uninstallCommand"; Write-Host "Uninstall args: $uninstallArgs"
|
||||
$processOptions = @{ FilePath = $uninstallCommand; PassThru = $true; Wait = $true }
|
||||
if ($uninstallArgs -ne '') { $processOptions.ArgumentList = $uninstallArgs }
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode; Write-Host "Uninstall exit code: $exitCode"
|
||||
} catch { Write-Host "Error: $_"; Exit 1 }
|
||||
|
||||
Exit $exitCode
|
||||
@@ -4859,6 +4859,13 @@
|
||||
"unique_identifier": "Lenovo Dock Manager",
|
||||
"description": "Lenovo Dock Manager is an application for deploying and managing firmware updates for Lenovo docks."
|
||||
},
|
||||
{
|
||||
"name": "Lenovo System Update",
|
||||
"slug": "lenovo-system-update/windows",
|
||||
"platform": "windows",
|
||||
"unique_identifier": "Lenovo System Update",
|
||||
"description": "Lenovo System Update installs and updates Lenovo drivers, BIOS, and applications on Lenovo computers."
|
||||
},
|
||||
{
|
||||
"name": "Lens",
|
||||
"slug": "lens/darwin",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "5.08.03.59",
|
||||
"queries": {
|
||||
"exists": "SELECT 1 FROM programs WHERE name = 'Lenovo System Update';",
|
||||
"patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Lenovo System Update' AND version_compare(version, '5.08.03.59') < 0);"
|
||||
},
|
||||
"installer_url": "https://download.lenovo.com/pccbbs/thinkvantage_en/system_update_5.08.03.59.exe",
|
||||
"install_script_ref": "04c67509",
|
||||
"uninstall_script_ref": "aca82ee7",
|
||||
"sha256": "e66794dc561a3e58e3dc68556eb053ee32b674ac9d99638e473ef7322f353e0d",
|
||||
"default_categories": [
|
||||
"Productivity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"refs": {
|
||||
"04c67509": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n$installTimeoutSeconds = 420\n$registrationTimeoutSeconds = 120\n\n# The installer is x86, so on 64-bit Windows it registers under Wow6432Node.\n$machineKey = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n$machineKey32on64 = 'HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*'\n\nfunction Get-LenovoSystemUpdateEntry {\n Get-ChildItem -Path @($machineKey, $machineKey32on64) -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } |\n Where-Object { $_.DisplayName -eq \"Lenovo System Update\" } |\n Select-Object -First 1\n}\n\ntry {\n\n# -Wait also waits on descendants, so wait on the installer process alone.\n$process = Start-Process -FilePath \"$exeFilePath\" `\n -ArgumentList \"/VERYSILENT /SUPPRESSMSGBOXES /NORESTART\" `\n -PassThru\n# Keeps .ExitCode readable after the process ends.\n$null = $process.Handle\n\n$killed = $false\nif (-not $process.WaitForExit($installTimeoutSeconds * 1000)) {\n Write-Host \"Installer process did not exit within ${installTimeoutSeconds}s, stopping it.\"\n Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue\n $null = $process.WaitForExit(30 * 1000)\n $killed = $true\n}\n\n$exitCode = $null\nif ($process.HasExited) {\n $exitCode = $process.ExitCode\n Write-Host \"Install exit code: $exitCode\"\n}\n\n# The installer can return before the ARP entry is written.\n$elapsed = 0\nwhile (-not (Get-LenovoSystemUpdateEntry) -and ($elapsed -lt $registrationTimeoutSeconds)) {\n Start-Sleep -Seconds 5\n $elapsed += 5\n Write-Host \"Waiting for Lenovo System Update to register... ($elapsed seconds)\"\n}\n\n$entry = Get-LenovoSystemUpdateEntry\nif (-not $entry) {\n Write-Host \"Lenovo System Update did not register in Add/Remove Programs.\"\n Exit 1\n}\nWrite-Host \"Registered '$($entry.DisplayName)' by '$($entry.Publisher)', version $($entry.DisplayVersion).\"\n\n# Registration above is the success signal; a killed process's code means nothing.\nif ($killed -or $null -eq $exitCode) { Exit 0 }\n\n# 3010 (reboot required) and 1641 (reboot initiated) are successful installs.\nif ($exitCode -eq 3010 -or $exitCode -eq 1641) { Exit 0 }\n\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n",
|
||||
"aca82ee7": "# Fleet substitutes the winget ProductCode, which for this Inno installer is the\n# uninstall registry key name rather than a GUID.\n$packageId = 'TVSU_is1'\n$uninstallArgs = \"/VERYSILENT /SUPPRESSMSGBOXES /NORESTART\"\n\n# The installer is x86, so on 64-bit Windows it registers under Wow6432Node.\n$paths = @(\n \"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$packageId\",\n \"HKLM:\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\$packageId\"\n)\n$exitCode = 0\n\ntry {\n $key = $paths |\n ForEach-Object { Get-ItemProperty -Path $_ -ErrorAction SilentlyContinue } |\n Select-Object -First 1\n\n if (-not $key) { Write-Host \"Uninstall entry not found for '$packageId'.\"; Exit 0 }\n\n $uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }\n if ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n } elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n } elseif ($uninstallCommand -match '^\\s*(\\S+)\\s*(.*)$') {\n $uninstallCommand = $Matches[1]; if ($Matches[2]) { $uninstallArgs = \"$($Matches[2]) $uninstallArgs\".Trim() }\n }\n\n Write-Host \"Uninstall command: $uninstallCommand\"; Write-Host \"Uninstall args: $uninstallArgs\"\n $processOptions = @{ FilePath = $uninstallCommand; PassThru = $true; Wait = $true }\n if ($uninstallArgs -ne '') { $processOptions.ArgumentList = $uninstallArgs }\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode; Write-Host \"Uninstall exit code: $exitCode\"\n} catch { Write-Host \"Error: $_\"; Exit 1 }\n\nExit $exitCode\n"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -571,6 +571,7 @@ import LastWindowQuits from "./LastWindowQuits";
|
||||
import Latest from "./Latest";
|
||||
import Launchbar from "./Launchbar";
|
||||
import LenovoDockManager from "./LenovoDockManager";
|
||||
import LenovoSystemUpdate from "./LenovoSystemUpdate";
|
||||
import Lens from "./Lens";
|
||||
import LibreOffice from "./LibreOffice";
|
||||
import Lightburn from "./Lightburn";
|
||||
@@ -1723,6 +1724,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = {
|
||||
latest: Latest,
|
||||
launchbar: Launchbar,
|
||||
"lenovo dock manager": LenovoDockManager,
|
||||
"lenovo system update": LenovoSystemUpdate,
|
||||
lens: Lens,
|
||||
libreoffice: LibreOffice,
|
||||
lightburn: Lightburn,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
Reference in New Issue
Block a user