From f082237518b46d5e07defeb8d08d991bcedad5f9 Mon Sep 17 00:00:00 2001 From: kitzy Date: Fri, 31 Jul 2026 21:36:01 -0400 Subject: [PATCH] Add Lenovo System Update as a Windows FMA (#50339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 ## 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. --- .../inputs/winget/lenovo-system-update.json | 13 ++++ .../scripts/lenovo_system_update_install.ps1 | 69 ++++++++++++++++++ .../lenovo_system_update_uninstall.ps1 | 36 +++++++++ ee/maintained-apps/outputs/apps.json | 7 ++ .../outputs/lenovo-system-update/windows.json | 22 ++++++ .../components/icons/LenovoSystemUpdate.tsx | 14 ++++ .../SoftwarePage/components/icons/index.ts | 2 + ...app-icon-lenovo-system-update-60x60@2x.png | Bin 0 -> 8235 bytes 8 files changed, 163 insertions(+) create mode 100644 ee/maintained-apps/inputs/winget/lenovo-system-update.json create mode 100644 ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_install.ps1 create mode 100644 ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_uninstall.ps1 create mode 100644 ee/maintained-apps/outputs/lenovo-system-update/windows.json create mode 100644 frontend/pages/SoftwarePage/components/icons/LenovoSystemUpdate.tsx create mode 100644 website/assets/images/app-icon-lenovo-system-update-60x60@2x.png diff --git a/ee/maintained-apps/inputs/winget/lenovo-system-update.json b/ee/maintained-apps/inputs/winget/lenovo-system-update.json new file mode 100644 index 0000000000..27193818df --- /dev/null +++ b/ee/maintained-apps/inputs/winget/lenovo-system-update.json @@ -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"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_install.ps1 new file mode 100644 index 0000000000..5c4f9410b6 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_install.ps1 @@ -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 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_uninstall.ps1 new file mode 100644 index 0000000000..0b7b922dce --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/lenovo_system_update_uninstall.ps1 @@ -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 diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index ddc87a1c6a..bae1958151 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -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", diff --git a/ee/maintained-apps/outputs/lenovo-system-update/windows.json b/ee/maintained-apps/outputs/lenovo-system-update/windows.json new file mode 100644 index 0000000000..b3e2efb3e8 --- /dev/null +++ b/ee/maintained-apps/outputs/lenovo-system-update/windows.json @@ -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" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/LenovoSystemUpdate.tsx b/frontend/pages/SoftwarePage/components/icons/LenovoSystemUpdate.tsx new file mode 100644 index 0000000000..ff9a916244 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/LenovoSystemUpdate.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const LenovoSystemUpdate = (props: SVGProps) => ( + + + +); +export default LenovoSystemUpdate; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index 442e31e641..bc4b9b593d 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -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, diff --git a/website/assets/images/app-icon-lenovo-system-update-60x60@2x.png b/website/assets/images/app-icon-lenovo-system-update-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b78e4615356aa2ce7e6b35a4cdf3092af6a2d87 GIT binary patch literal 8235 zcmV+`Ak^Q9P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91fS>~a1ONa40RR91fB*mh07#AmcK`q%l}SWFRCodHU3q*|)!BY$CJWj3 z1d;#=YY|z*gf+-!#g9eBjaq&#)mjCyix#a~th++>4{L>fwOF;5YVD_OMG!@HP?88L zYCyKIB#?yc1jsTo-}Bt$jx&?V%-oqtG85+ferN8TyPSLO`#$I0&T@6q1C!Zo)~#5v zV#1j-XTqO+@=4L_^$}NJef2+j_39P7XU`rfEG%^R);T#jGJ5oAKCAtE{Y?Q&CZo2k|c0I%IeRjcoha`G z-u6cxg?uaWGGu6Y^GRed%nT-)ag#^+4DLJLR>mQ0pF>`XJO-K11kg~lZDt)=SJt_2 z-@Y>;tpZ+6XkwZ#E`H;UH&P)|AL!Sw-{G{hw01-$;Rhij^ff-)L8h9s@q0;m{nvq&x&KcwTf}l4c{~Ot# znBEbX2!?+l{{|WMhYg4cgrP%+n&-})yXN}quO|=!!UogIPq(jMy z3PQb!d;;0u8SHNT_%3{>E0M#HRWK0r1V7xzMWBhn?UobB7ZULZqO3_zPp6k=AqGV~ zTl^_xcL^J4*HJzE4VgxxH4x(B;!Gn(j97z!Q_uAnsPBk$BA&WXWqu@x@hEa2i+Zg< ztj7C3i)?Qr00^oy=vhYMyYIf6N__|G-qPx6Dbr%!gRGT+J>`0Wei5>@*Fb47VZwwp zJ9g|y4P;4gsXN75Zf?zFj^gSBdXkjf4NGs9|L9i(nLI&avMSgsI zd@({5=fB*e!(KBXYK3`-@kolUy gD-h6gnmToAzTX;$JHY(u)2HVrCnqc7i@JOV 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