Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Checking (and Nudging) Google Chrome Updates with PowerShell

Find out which Chrome version a machine is really running, whether an update is stuck waiting on a restart, and kick Google's updater into checking now.

AT A GLANCEInvoke-ChromeUpdateCheck.ps1
What it does
Reports the installed Chrome version, any staged update waiting on a browser restart, and the Google Update policy on the machine. With -TriggerUpdate it wakes whichever Google updater is installed so it checks right away.
Requires
  • Windows PowerShell 5.1 or PowerShell 7+
  • Google Chrome installed with Google's updater (GoogleUpdater or the legacy GoogleUpdate.exe)
  • Internet access for -CompareToLatest
Permissions
Reporting works as a standard user. Triggering the system-wide updater needs an elevated session.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4

Part 7 of the thread Spring cleaning for Windows PCs

The old version of this post downloaded Chrome's online installer and ran it silently. It worked, mostly, but it's the wrong tool. Run it without elevation and you can end up with a second, per-user copy of Chrome in AppData. Run it on a machine where updates are managed by policy and you're fighting your own settings. And Chrome already ships with an updater that knows how to do this properly.

What you usually want to know is simpler: is this machine actually on the current version, and if not, why? Very often the answer is that the update already downloaded and is sitting there waiting for someone to close the browser, which they haven't done since March.

This script answers that. It reads the installed version, spots a staged update, reads the Google Update policy, and can compare against the current Stable release. Add -TriggerUpdate and it tells Google's own updater to check now instead of waiting for its next scheduled run. It handles both the newer GoogleUpdater and the legacy GoogleUpdate.exe, since you'll still find both out there.

Invoke-ChromeUpdateCheck.ps1Download
<#
.SYNOPSIS
    Reports the installed Google Chrome version and update policy, and can tell Google's
    own updater to check for an update right now.
.DESCRIPTION
    Finds system-wide and per-user Chrome installs, reads their versions, notices a staged
    update that's waiting on a browser restart, and reads the Google Update policy keys so
    you can see if updates have been switched off. With -TriggerUpdate it wakes whichever
    updater is present: the newer GoogleUpdater (updater.exe --wake) or the legacy
    GoogleUpdate.exe (/ua /installsource scheduler). Supports -WhatIf.
.PARAMETER TriggerUpdate
    Ask the updater to check for and install updates now, instead of waiting for its schedule.
.PARAMETER CompareToLatest
    Look up the current Stable version for Windows from Google's VersionHistory API and flag
    whether this machine is behind. Needs internet access.
.EXAMPLE
    .\Invoke-ChromeUpdateCheck.ps1 -CompareToLatest
.EXAMPLE
    .\Invoke-ChromeUpdateCheck.ps1 -TriggerUpdate -Verbose
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [switch]$TriggerUpdate,
    [switch]$CompareToLatest
)

$programDirs = @($env:ProgramFiles, ${env:ProgramFiles(x86)}) | Where-Object { $_ } | Select-Object -Unique
$chromeGuid = '{8A69D345-D564-463C-AFF1-A69D9E530F96}'

# What does policy say? 0 = disabled, 1 = always allow, 2 = manual only, 3 = automatic only.
$policy = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Google\Update' -ErrorAction SilentlyContinue
$policyValue = if ($policy -and $null -ne $policy."Update$chromeGuid") { $policy."Update$chromeGuid" }
               elseif ($policy -and $null -ne $policy.UpdateDefault) { $policy.UpdateDefault }
$policyNames = @{
    0 = 'Updates disabled by policy'
    1 = 'Always allow (policy)'
    2 = 'Manual updates only (policy)'
    3 = 'Automatic updates only (policy)'
}
$policyText = if ($null -ne $policyValue -and $policyNames.ContainsKey([int]$policyValue)) { $policyNames[[int]$policyValue] } else { 'Not configured' }

$latest = $null
if ($CompareToLatest) {
    try {
        $uri = 'https://versionhistory.googleapis.com/v1/chrome/platforms/win64/channels/stable/versions'
        $latest = [version](Invoke-RestMethod -Uri $uri -ErrorAction Stop).versions[0].version
        Write-Verbose "Current Stable for win64: $latest"
    }
    catch {
        Write-Warning "Couldn't reach the VersionHistory API: $($_.Exception.Message)"
    }
}

# System-wide installs first, then the per-user one for whoever is running this.
$candidates = @($programDirs | ForEach-Object { Join-Path $_ 'Google\Chrome\Application' })
if ($env:LOCALAPPDATA) { $candidates += Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application' }

foreach ($appDir in $candidates) {
    $exe = Join-Path $appDir 'chrome.exe'
    if (-not (Test-Path -LiteralPath $exe)) { continue }

    $installed = [version](Get-Item -LiteralPath $exe).VersionInfo.ProductVersion
    # When Chrome is running during an update, the new build is parked as new_chrome.exe
    # and swapped in on the next launch.
    $staged = Join-Path $appDir 'new_chrome.exe'
    $pending = if (Test-Path -LiteralPath $staged) { [version](Get-Item -LiteralPath $staged).VersionInfo.ProductVersion }
    $effective = if ($pending) { $pending } else { $installed }

    [pscustomobject]@{
        Scope            = if ($env:LOCALAPPDATA -and $appDir -like "$env:LOCALAPPDATA*") { 'PerUser' } else { 'System' }
        InstalledVersion = $installed
        PendingRestart   = $pending
        LatestStable     = $latest
        UpToDate         = if ($latest) { $effective -ge $latest } else { $null }
        UpdatePolicy     = $policyText
        Path             = $exe
    }
}

if (-not $TriggerUpdate) { return }

if ($policyValue -eq 0 -or $policyValue -eq 2) {
    Write-Warning "Policy blocks automatic updates ($policyText). The updater will likely do nothing."
}

# Newer machines: GoogleUpdater, one folder per updater version. Use the newest.
$updater = $programDirs |
    ForEach-Object { Get-ChildItem -Path (Join-Path $_ 'Google\GoogleUpdater\*\updater.exe') -ErrorAction SilentlyContinue } |
    Sort-Object { try { [version]$_.Directory.Name } catch { [version]'0.0' } } -Descending |
    Select-Object -First 1

if ($updater) {
    if ($PSCmdlet.ShouldProcess($updater.FullName, 'Wake GoogleUpdater (--wake --system)')) {
        Start-Process -FilePath $updater.FullName -ArgumentList '--wake', '--system' -WindowStyle Hidden
        Write-Verbose 'GoogleUpdater woken. It works in the background; check again in a few minutes.'
    }
    return
}

# Older machines: the legacy Omaha updater.
$legacy = $programDirs | ForEach-Object { Join-Path $_ 'Google\Update\GoogleUpdate.exe' } |
    Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1

if ($legacy) {
    if ($PSCmdlet.ShouldProcess($legacy, 'Run GoogleUpdate.exe /ua /installsource scheduler')) {
        Start-Process -FilePath $legacy -ArgumentList '/ua', '/installsource', 'scheduler' -WindowStyle Hidden
        Write-Verbose 'GoogleUpdate.exe started. It works in the background; check again in a few minutes.'
    }
    return
}

Write-Warning 'No Google updater found. If Chrome came from an MSI with updates stripped out, update it the way it was deployed.'

Parameters

ParameterTypeDefaultWhat it's for
-TriggerUpdateswitch—Wake the Google updater so it checks for and installs updates now. Without this switch the script only reports.
-CompareToLatestswitch—Ask Google's VersionHistory API for the current Stable version on Windows and set UpToDate accordingly.

Run it

What's installed, and is it current?

.\Invoke-ChromeUpdateCheck.ps1 -CompareToLatest

Kick off an update check from an elevated prompt.

.\Invoke-ChromeUpdateCheck.ps1 -TriggerUpdate -Verbose

Check a batch of machines over remoting.

Invoke-Command -ComputerName PC-0142, PC-0143 -FilePath .\Invoke-ChromeUpdateCheck.ps1 | Select-Object PSComputerName, InstalledVersion, PendingRestart, UpdatePolicy

Find machines where updates are switched off by policy.

.\Invoke-ChromeUpdateCheck.ps1 | Where-Object UpdatePolicy -like '*disabled*'

What you'll see

Example outputvalues are illustrative
Scope            : System
InstalledVersion : 139.0.7258.155
PendingRestart   : 140.0.7339.128
LatestStable     : 140.0.7339.128
UpToDate         : True
UpdatePolicy     : Not configured
Path             : C:\Program Files\Google\Chrome\Application\chrome.exe

How it works

  1. Read the policy first. Google Update policies live under HKLM\SOFTWARE\Policies\Google\Update. The Chrome-specific value (Update{8A69D345-D564-463C-AFF1-A69D9E530F96}) wins over UpdateDefault. 0 means disabled, 2 means manual only, and either one explains a lot of "why is this machine so far behind?"
  2. Find every Chrome. It checks both Program Files folders for a system install and %LOCALAPPDATA% for a per-user one, and reads the version straight from chrome.exe.
  3. Look for a staged update. A new_chrome.exe sitting next to chrome.exe means the update is done and waiting on a restart.
  4. Optionally compare to Stable. -CompareToLatest asks versionhistory.googleapis.com for the current Windows Stable version.
  5. Optionally wake the updater. Newer machines have GoogleUpdater, with one folder per updater version under Google\GoogleUpdater; the script runs the newest updater.exe --wake --system, which is what its own scheduled task does. Older machines have Google\Update\GoogleUpdate.exe, which gets /ua /installsource scheduler, again the same thing its scheduled task runs.

Managing it properly with policy

For more than a handful of machines, let policy do the work. Google publishes ADMX templates for both Chrome (chrome.admx) and Google Update (GoogleUpdate.admx) in the Chrome Enterprise bundle. The settings I'd look at first:

  • Update policy override (Google Update): keep automatic updates on. If you need to pin a version for testing, pair it with Target version prefix override rather than turning updates off.
  • Auto-update check period override (Google Update): how often the updater checks. The default is fine for most places.
  • RelaunchNotification and RelaunchNotificationPeriod (Chrome): nag users to restart, or force it after a grace period. This fixes the "downloaded but never applied" problem better than any script.

In Intune you can import the same ADMX files, or use the Chrome settings in the settings catalog.

Take it further

  • Report across the fleet. Run it with Invoke-Command or as a detection-only script in Intune, collect PendingRestart and UpToDate, and you'll know who needs a relaunch nudge.
  • Watch for per-user installs. A PerUser row on a managed machine usually means somebody installed their own copy, and it won't follow your machine policies.

Things that'll trip you up

  • Updated is not the same as running. If Chrome is open during an update, the new build is parked as new_chrome.exe and only swapped in when the browser restarts. The script counts that as up to date, but the user is still running the old code until they relaunch. The RelaunchNotification policy exists for exactly this.
  • The updater works in the background. -TriggerUpdate starts the updater and returns straight away. Give it a few minutes, then run the script again to see the new version.
  • "Latest" depends on the rollout. Google stages Stable releases, so a machine can be a build or two behind the newest version for a day or so without anything being wrong.
  • MSI installs can go either way. The enterprise MSI installs the updater too, but some teams strip it out or disable it and push new MSIs instead. If no updater is found, update Chrome the same way it was deployed.