Install and schedule Windows updates with PowerShell

The PowerShell script described here enables you to install Windows updates more flexibly than with Windows Server Update Services (WSUS) or Group Policy. To this end, it allows you to determine precisely when to install certain updates on different types of computers in your network.

WSUS and Group Policy work well for updating Windows if it doesn't really matter much when to install updates. If you look at the standard Group Policy settings below for Automatic Updates, you realize the schedule options are very limited. All you can determine is the day on which to install updates.

Default updates schedule

Default updates schedule

However, I needed to create a more precise schedule for my update tasks. For instance, I wanted to install updates every second Thursday of the month on my pre-production systems, every third Monday on my test computers, and every third Thursday on my production system. The standard tools from Microsoft don't provide these options.

The PowerShell script below uses the Windows Update Agent API that you can access through the Microsoft.Update.Searcher COM object.

First, I need to determine the criteria that I have to supply to the Microsoft.Update.Searcher object. There are actually a lot of choices for the criteria. For example, I could get the updates already installed, or get updates marked “hidden,” or get a particular update by its ID. For my purpose, I only need software updates not yet installed. I exclude all other updates, such as driver updates.

I create the Searcher object with the New-Object cmdlet using its search method with the criteria I established in the previous step. Then I create the Microsoft.Update.Session object because I'd like to make sure to download the updates selected for installation.

Next, I use the CreateUpdateDownloader method of the Session object to create the UpdateDowloader interface. In the next step, with the help of the $SearchResult value, I determine the updates to download. I then call the Download() method, which initiates the actual download of the updates.

The last four lines are for installing the updates. First, I create the Installer object, and then I assign the value of $SearchResult to the object's $Installer.Updates property to ensure installation of only the updates that meet my criteria.

Finally, the Install() method installs the updates. I'm saving the result of the installation into the $Result variable, so I can check whether to require a reboot after the installation. The last line of the script accomplishes this.

There is only one significant caveat of this script: It must run locally. Some Windows Update Agent tasks can run remotely, but tasks for downloading and installing updates have to run locally. To solve this problem, I created a scheduled task with the desired run time on my test computer and exported this task to an XML file. Then I wrote a short PowerShell script to import this task to all machines on which to install updates:

The script executes the "schtasks" command remotely. It uses the Invoke-Expression cmdlet and the imported XML file that contains the scheduled task on a list of computers imported from a CSV file.

If you want to learn more about how to work with scheduled tasks in Windows, you can read the article I wrote some time ago.

Join the 4sysops PowerShell group!

Your question was not answered? Ask in the forum!

7+
avataravatar
Share
26 Comments
  1. Lars Panzerbjørn 3 years ago

    Even though this must run locally, I feel like it should be possible to control centrally via DSC...

    0

  2. Tom Hughes 3 years ago

    Hi!

    this looks amazing and exactly what i'm looking for! however i'm receiving this error:

    Exception calling "Download" with "0" argument(s): "Exception from HRESULT: 0x80240044"
    At C:\installupdates.ps1:10 char:1
    + $Downloader.Download()
    + ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ComMethodTargetInvocation

    Exception calling "Install" with "0" argument(s): "Exception from HRESULT: 0x80240044"
    At C:\installupdates.ps1:14 char:1
    + $Result = $Installer.Install()
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ComMethodTargetInvocation

    not sure what i'm doing wrong but have tested it on a computer thats ready for search and download updates, i've also tested it on one that already has updates ready to install and i get the same error.

    0

    • Author

      Check if you are running your PowerShell session on behalf of administrator (Run as administrator). The error you are getting might suggest that the user running this script doesn't have enough permissions.

      0

  3. James 3 years ago

    Thanks for the insight - it is very helpful.

    Just so you know, the reason the download function doesn't work in a remote session is because of the 'double-hop' restriction in the default configuration of powershell endpoints.

    If you properly configure a Just-Enough-Administration (JEA) endpoint, you can do the whole thing without resorting to scheduled tasks. The JEA endpoint needs to be configured to RunAsVirtualAccount and would need either the "FullLanguage" LanguageMode, or have the commands wrapped in a module/visible functions.

     

    2+
    avataravatar
  4. Brian 3 years ago

    Thanks for the script, working well for the most part.

    I am having an issue running it on Server 2016. The script works fine as long as it is run as administrator on my 2008/2012 servers but still get the permission error Tom Hughes referenced on 2016. Has anyone seen this behavior?

    I'm running scripts as administrator on all server versions and my logon ID is a member of the local admins group.

    0

    • Author

      Could you paste the text of the error you're getting here?

      1+

      • Brian 3 years ago

        PS C:\Windows\system32> C:\Support\WSUS\wsus_install_updates.ps1
        Exception from HRESULT: 0x80240024
        At C:\Support\WSUS\wsus_install_updates.ps1:15 char:1
        + $Downloader.Download()
        + ~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : OperationStopped: (:) [], COMException
        + FullyQualifiedErrorId : System.Runtime.InteropServices.COMException

        Exception from HRESULT: 0x80240024
        At C:\Support\WSUS\wsus_install_updates.ps1:19 char:1
        + $Result = $Installer.Install()
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : OperationStopped: (:) [], COMException
        + FullyQualifiedErrorId : System.Runtime.InteropServices.COMException

        Unless there is something easily identifiable I may just rebuild the server since it isn't in production. I've tested on another 2016 server and it worked fine there.

        0

        • Author

          I just ran it on the Win 2016 server and it worked.

          And as you said it worked on on another server for you as well.

          So, its probably that particular server issue. I've seen similar behavior once, except that in my case I haven't been able to install updates at all. It was Windows 2016 RTM fresh install. So I ended up with downloading and installing manually following update:

          http://www.catalog.update.microsoft.com/Search.aspx?q=kb4015217

          After that it started working.

          I can't say if it is your case or not, though.

          1+

        • John 3 years ago

          I got this on several of my Windows 2016 servers. For me it turned out to be that I had a proxy set. Once I removed that it worked fine.

          So in general I think this error is saying that it is unable to talk to the specified WSUS server. In my case it was a local WSUS server but I think you would get the same error if you were trying to use Microsoft but did not have internet access.

          0

        • vmxnet4 1 year ago

          This is because there were no updates available for that system when the script was ran.

           

          WU_E_NO_UPDATE

          0x80240024

          There are no updates.

          https://docs.microsoft.com/en-us/windows/desktop/wua_sdk/wua-success-and-error-codes-

          0

  5. senthil 3 years ago

    Hi ,

    Thank you for your wonderful work.

    I need to save the downloaded updates into local disk the i will trigger the updates whenever i require could you please tell me how to do it in powershell. i am very new for this..

    Thanking in advance

    0

  6. John Thayer Jensen 3 years ago

    Alex - I am trying to work out how to force the searcher to access Microsoft directly even though the GPO for our machines tells them to use SCCM. It looks like the ServerSelection property is the one to set but I don't understand how to use it.

    Any thoughts?

    John Thayer Jensen
    Applications Specialist
    University of Auckland Business School

    0

  7. John Thayer Jensen 3 years ago

    And - sorry, I should have tried this first! - it looks like I just set the value of ServerSelection to 2, which appears to mean the Windows Update service.  I'll try that.

    jj

    0

  8. John 3 years ago

    Great scrip. I expanded it a little to give a bit more feedback and control

    4+

  9. Tommy Becker 3 years ago

    You need to add a hook to run the *.AcceptEula() method on updates that require EULA.

    0

  10. senthil 3 years ago

    could you please help me to modify this code using patch id to download only particular patch and save it in to local disk.

    0

  11. david puckett 3 years ago

    modified for my purpose

    added: date filter (patch tuesday, same patches for everyone)

    added: scan / install

    added: reboot force
    ###################
    ### example: run script -> powershell.exe -file {scriptname} 2017-OCT-09 Action=Install ForceReboot=Yes
    ### example: run script -> powershell.exe -file {scriptname} 2050-JAN-01 Action=ScanOnly
    ### fwiw: really should add fuctionions instead of long script
    #$args.count
    $DateIn=$args[0]
    $ActionIn=$args[1]
    $bScanOnly=$true
    $bForceReboot=$false

    if ($args.Length -gt 2) {$ForceRebootIn=$args[2]} else { $ForceRebootIn="no" }

    #needed: Add acceptance for EULA

    #If no date passed to the script, quit
    if ( $DateIn -eq $null ){write-host "Date parameter missing, script exiting."; Exit}
    #If no action passed to the script, quit
    if ( $ActionIn -eq $null ){write-host "Action parameter missing, script exiting."; Exit}

    #If "Action=Install" value passed, scan only and exit.
    if ( $ActionIn.ToLower() -eq "action=install" ){ $bScanOnly=$false }

    #If "ForceReboot=True" value passed, reboot at end of install if required.
    if ( $ForceRebootIn.ToLower() -eq "forcereboot=yes" ){ $bForceReboot=$true }

    ###added for limiting installs by before date###
    $enus = "en-US" -as [Globalization.CultureInfo]
    $LastDeploymentChangeTime = $DateIn + " 12:00:00 AM"
    $BeforeCheckDate = [datetime]::ParseExact($LastDeploymentChangeTime,"yyyy-MMM-dd hh:mm:ss tt", $enus)
    #$BeforeCheckDate

    #Initialize global variables
    $INSTALLFAILED = $false
    $REBOOTREQUIRED = $false

    #Initialize objects that are needed for the script
    $Searcher = New-Object -ComObject Microsoft.Update.Searcher
    $Session = New-Object -ComObject Microsoft.Update.Session
    $Installer = New-Object -ComObject Microsoft.Update.Installer
    $CheckInfo = $Session.CreateUpdateDownloader()
    $Downloader = $Session.CreateUpdateDownloader()
    $UpdatesToCheckInfo = New-Object -ComObject Microsoft.Update.UpdateColl
    $UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
    $UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl

    #Clear-Host

    #This set the criteria used for searching for new updates
    $Criteria = "IsInstalled=0 and Type='Software'"

    #This searches for new needed updates and stores them as a list in $SearchResult
    Write-Host "Searching for updates. Please wait... (Date criteria: before $BeforeCheckDate)"
    $SearchResult = $Searcher.Search($Criteria).Updates | Where-Object { $_.LastDeploymentChangeTime -lt $BeforeCheckDate }

    #This gets the number of updates that are currently needed by the given server
    $NumberOfUpdates = $SearchResult.Count

    #If $NumberOfUpdates is zero then there are no needed updates so we just exit
    #Otherwise inform the user of how many updates are required then proceed
    if ($NumberOfUpdates -gt 0) {
    Write-Host "There are "$SearchResult.Count" updates to install"
    Write-Host
    }
    else {
    #Write-Host "There are no updates to install released before $BeforeCheckDate"
    Write-Host "There are no updates released before $BeforeCheckDate to install."
    Exit
    }

    ######################################################################################################################
    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"

    #Show the updates
    Write-Host "Applying date criteria: before $BeforeCheckDate"
    #$SearchResult | Select-Object -Property @{N="Patch Release Date";E={$_.LastDeploymentChangeTime}}, `
    $SearchResult | Select-Object -Property @{N="PatchReleaseDate";E={$_.LastDeploymentChangeTime}}, `
    @{N="Severity";E={$_.MsrcSeverity}}, Title | `
    Where-Object { $_.PatchReleaseDate -lt $BeforeCheckDate } | Format-Table -AutoSize

    #Exit if not "Action=Install"
    if ($bScanOnly) { exit }

    ######################################################################################################################

    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"

    #This steps through each of the discovered updates and downloads them one at a time
    #These are done individually to provide more feedback on the overall progress of the script
    ForEach ($Update in $SearchResult) {
    #This adds the current Update to the collection and sends the return value to null to mask bogus output
    $null=$UpdatesToDownload.Add($Update)

    #This creates a temporary download task
    $Downloader.Updates = $UpdatesToDownload

    if ($Downloader.Updates.Item(0).LastDeploymentChangeTime -lt $BeforeCheckDate){
    #Update the end user with which patch is being downloaded
    $DownloadSize = "{0:N1}" -f ((($Downloader.Updates.Item(0).MaxDownloadSize)/1024)/1000)
    Write-Host "Downloading ->"$Downloader.Updates.Item(0).Title" ---Please wait..."
    Write-Host "Estimated Size: "$DownloadSize"MB"

    #This checks to see if the current update has already been downloaded at some point
    #If it already exists then the download is skipped. Otherwise it starts the download
    if ($Downloader.Updates.Item(0).IsDownloaded) {
    Write-Host "Update has already been downloaded -> Skipped"
    }else{
    #Process the actual download
    $null=$Downloader.Download()
    }

    #This verifies that the update was successfully downloaded. If not it alerts the user and aborts the script
    if (-not ($Downloader.Updates.Item(0).IsDownloaded)) {
    Write-Host "Download failed for unknown reason. Script abort."
    Exit
    }
    }else{
    #Write-Host "skipping update -> "$Downloader.Updates.Item(0).Title " Released:" $Downloader.Updates.Item(0).LastDeploymentChangeTime
    }

    #This clears the update collection to prepare for the next update
    $UpdatesToDownload.Clear()
    Write-Host

    }

    Write-Host "All updates have been downloaded. Proceeding with installation steps..."
    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"

    #This copies the list of updates from above to a list that should be installed on the computer
    #$Installer.Updates = $SearchResult

    #This steps through each of the downloaded updates and installs them one at a time
    #These are done individually to provide more feedback on the overall progress of the script
    ForEach ($Update in $SearchResult) {
    #This adds the current Update to the collection and sends the return value to null to mask bogus output
    $null=$UpdatesToInstall.Add($Update)

    #This creates a temporary install task
    $Installer.Updates = $UpdatesToInstall

    if ($Installer.Updates.Item(0).LastDeploymentChangeTime -lt $BeforeCheckDate){

    #Update the end user with which patch is being installed
    Write-Host "Installing ->"$Installer.Updates.Item(0).Title" ---Please wait..."

    #This checks to see if the current update has already been installed at some point
    #If it is already installed then the install is skipped. Otherwise it starts the install
    if ($Installer.Updates.Item(0).IsInstalled) {
    Write-Host "Update has already been installed -> Skipped"
    }else{
    #Process the actual install
    $result=$Installer.Install()
    }

    #This verifies that the update was successfully installed. If not it alerts the user then continues with the next update
    if (-not ($result.ResultCode -eq 2)) {
    Write-Host "WARNING!!! - Installation failed for unknown reason. Continuing to next update."
    $INSTALLFAILED = $true
    }
    }else{
    #Write-Host "skipping install -> "$Installer.Updates.Item(0).Title " Released:" $Downloader.Updates.Item(0).LastDeploymentChangeTime
    }

    #This clears the update collection to prepare for the next update
    $UpdatesToInstall.Clear()
    Write-Host

    #This checks to see if the current update requires a reboot
    if ($result.rebootRequired) {
    Write-Host "INFO!!! - Installation requires reboot."
    $REBOOTREQUIRED = $true
    }
    }

    Write-Host
    Write-Host
    Write-Host "-----------------------------------------------------------------------------------------"
    Write-Host "Update Results:"
    Write-Host

    #Alert the user about installation success
    if ($INSTALLFAILED) {
    Write-Host "WARNING!!! - At least one update failed to install."
    }
    else {
    Write-Host "All updates have been installed."
    }

    #Alert the user if reboot is required
    if ($REBOOTREQUIRED) {
    if ($bForceReboot){
    Write-host "Rebooting. ForceReboot=$bForceReboot"
    Start-Sleep -s 10
    Restart-Computer -Force
    }else{
    Write-Host "INFO!!! - At least one update requires a reboot. Please reboot now."
    }
    }
    else {
    Write-Host "Script Finished"
    }

    Exit

     

    0

  12. david puckett 3 years ago

    want to call it from a patch file?

    assumes patching.ps1 is the file name

    powershell.exe -file "%~dp0patching.ps1" 2017-oct-17 Action=Install

    2+

    • David Puckett 3 years ago

      I spent some time trying to think around the double hop issue and not being able to download patches when invoking the script remotely.
      Imo, the workaround to force patch installs on a remote computer is launch an RDP session with a PatchingAccount that runs a command at login. (disable the account when patching is completed until the next patch tuesday)
      Another option is to remotely schedule a task and have it launch in ~x minutes.

      0

      • Steve Mahoney 3 years ago

        Or maybe use Invoke-Command with the -Credential parameter?

        0

  13. sebus 2 years ago

    Running the script on machine with Settings/Windows Update window open does not make it show real time that anything is happening.

    If we were to run C:\Windows\System32\USOClient.exe StartInstall you can see the real time output

    0

  14. sebus 2 years ago

    I am trying to run it on remote system either by invoke-command OR Enter-PSSession

    Either gives me Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))

    It works fine on local machine if PS is run as administrator

    Any idea how to make it work?

    0

    • J 2 years ago

      From the article:

      There is only one significant caveat of this script: It must run locally. Some Windows Update Agent tasks can run remotely, but tasks for downloading and installing updates have to run locally. To solve this problem, I created a scheduled task with the desired run time on my test computer and exported this task to an XML file. Then I wrote a short PowerShell script to import this task to all machines on which to install updates:

      Import-Csv -Path "C:\Temp\servers.csv" | % {
      if (Test-Connection $_.Name -Quiet){
      $srv = $_.Name
      Write-Host "Working on $srv" -ForegroundColor Green
      $Expr = "schtasks /Create /S " + $_.Name + " /XML C:\Temp\Install_Updates.xml" + " /TN " + "InstallUpdates"
      Invoke-Expression -Command $Expr
      }
      }

      1
      2
      3
      4
      5
      6
      7
      8

      Import-Csv -Path "C:\Temp\servers.csv" | % {
      if (Test-Connection $_.Name -Quiet){
         $srv = $_.Name
         Write-Host "Working on $srv" -ForegroundColor Green
         $Expr = "schtasks /Create /S " + $_.Name + " /XML C:\Temp\Install_Updates.xml" + " /TN " + "InstallUpdates"
         Invoke-Expression -Command $Expr
        }
      }

      The script executes the "schtasks" command remotely. It uses the Invoke-Expression cmdlet and the imported XML file that contains the scheduled task on a list of computers imported from a CSV file.

      If you want to learn more about how to work with scheduled tasks in Windows, you can read the article I wrote some time ago.

      0

  15. Sandor 1 year ago

    You must keep in mind though....sometimes...the third Thursday may come before the third Monday.

    These months begin on Tuesday, Wednesday, Thursday.

    So following your schedule....2019 has 4 months where the Production computers get the updates before the Test computers.

    An adjustment to your script could include the 1st Monday after the 2nd Tuesday and then the 1st Thursday after the 2nd Tuesday.

    Cheers!

    0

Leave a reply

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2020

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account