Silent patching and controlled reboots

Patching silently and installing applications without any user interaction… This is really hard to do and it’s really hard to get compliant machines if you do not force a reboot every now and then.
Then, if you force the user to reboot with a short timeout they will not be to happy.
So… to solve this I took some inspiration from a couple of blogposts some scripts and then some of my knowledge and put together a recipe that I think work’s really nice. 🙂

All in all, this results in a nice popup (only when a reboot is needed) that gives the user a good amount of time to decide when to reboot.
With this in place you can push out updates and applications silently and then just sit back and wait for the users to reboot when they want to.

So, this is what you need to do…

Copy the following files to %ProgramFiles%\RebootIfNeeded

Create a Scheduled Task that runs once or twice every day (I have it set at 08:00 and 13:00 every weekday), and on that task create an with the following configuration:
Program: %ProgramFiles%\RebootIfNeeded\hstart64.exe
Arguments:

/NOCONSOLE /WAIT ""%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -File "%ProgramFiles%\RebootIfNeeded\RebootIfNeeded.ps1""

(Tip: If using GPP, set item level targeting to check that the PS1-file exist before creating the task)

The script will check for pending reboots and if the computer havn’t rebooted for XX days.
You can add support for more languages, just extend the hashtable $restartDescriptions

[CmdletBinding()]
PARAM (
    $maxBootAgeDays = 35,
    $restartTimeOut = (9 * 60), # 9 hours
    $restartMaxPostpone = (48 * 60), # 48 hours
    $restartDescriptions = @{
        "en-US" = "Your computer needs to restart to receive the latest updates.";
        "sv-SE" = "Din dator behöver startas om för att få de senaste uppdateringarna.";
    },
    $defaultLanguage = "en-US"
)


Function Get-PendingReboot {
	# Local HKLM
	$HKLM = [UInt32] "0x80000002"
	$wmiRegistry = [WMIClass] "\\.\root\default:StdRegProv"

	#Default
    $PendingReboot = $false

	# CBS - Reboot Required ?
	$RegSubKeysCBS = $wmiRegistry.EnumKey($HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\")
	if ($RegSubKeysCBS.sNames -contains "RebootPending") {
        Write-Verbose "Component Based Servicing have a reboot pending"
		$PendingReboot = $true
    }
							
	# Windows Update - Reboot Required?
	$RegistryWUAU = $wmiRegistry.EnumKey($HKLM,"SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\")
	if ($RegistryWUAU.sNames -contains "RebootRequired") {
        Write-Verbose "Windows Update have a reboot required"
		$PendingReboot = $true
    }

	## Pending FileRenameOperations ?
	$RegSubKeySM = $wmiRegistry.GetMultiStringValue($HKLM,"SYSTEM\CurrentControlSet\Control\Session Manager\","PendingFileRenameOperations")
	If ($RegSubKeySM.sValue) {
        $RegSubKeySM.sValue | ForEach-Object { 
        	If ($_.Trim() -ne '') {
        		Write-Verbose "Pending FileRename operation: $($_)"
        	}
        }
		$PendingReboot = $true
    }

	# ConfigMgr - Pending reboot ?
	TRY {
	    $CCMClientSDK = Invoke-WmiMethod -NameSpace "ROOT\ccm\ClientSDK" -Class "CCM_ClientUtilities" -Name "DetermineIfRebootPending" -ErrorAction Stop

		If ($CCMClientSDK.IsHardRebootPending -or $CCMClientSDK.RebootPending) {
            Write-Verbose "ConfigMgr have reboot pending"
            $PendingReboot = $true
        }
	} CATCH {
        Write-Verbose "Cant talk to ConfigMgr Agent"
    }

    Write-Verbose "Pending reboot: $($PendingReboot)"
    Return $PendingReboot
}

Function Check-OldBootAge {
    PARAM (
        $maxAgeDays = 35
    )

    $BootTime = Get-WmiObject  Win32_Operatingsystem
    [Int]$days = (New-TimeSpan -Start $boottime.ConvertToDateTime($boottime.LastBootUpTime) -End (Get-Date)).TotalDays

    if ($days -ge $maxAgeDays) {
        Write-Verbose "Boot age is $($days) days (more than $($maxBootAgeDays)), reboot required"
        Return $true
    } else {
        Write-Verbose "Boot age is $($days) days (less than $($maxBootAgeDays))"
        Return $false
    }

    Return $days
}

Function Get-UserLanguage {
    Return [String] ([System.Threading.Thread]::CurrentThread).CurrentUICulture.Name
}

# ------------------------------------------------------------------------------------------------------------
# Main script

if ( (Get-WmiObject -Query "SELECT ProductType FROM Win32_OperatingSystem").ProductType -eq 1) {

    If ( (Get-Process "ShutdownTool" -ErrorAction SilentlyContinue) ) {
        Write-Host "Already running ShutdownTool"
    } else {
        If ((Check-OldBootAge -maxAgeDays $maxBootAgeDays) -or (Get-PendingReboot)) {
            Write-Host "Reboot is required, calling restart utility"

            $userLanguage = Get-UserLanguage
            Write-Verbose "Language: $($userLanguage)"

            # Find description
            $Description = $restartDescriptions[$userLanguage]
            if ($Description -eq $null) {
                $Description = $restartDescriptions[$defaultLanguage]
            }

            $timeOutSeconds = ($restartTimeOut*60) - 1

            Write-Verbose "Restart timeout: $($timeOutSeconds) seconds"
            Write-Verbose "Max postpone: $($restartMaxPostpone) minutes"
            Write-Verbose "Description: $($Description)"


            If ((Test-Path ".\ShutdownTool.exe") -eq $false) {
                Throw "Cant find ShutdownTool.exe"
            } else {
                Write-Verbose "Calling restart with ShutdownTool"
                .\ShutdownTool.exe /g:$userLanguage /d:"$Description" /t:$timeOutSeconds /m:$restartMaxPostpone /r /c
            }

            # /g - Language
            # /d - description
            # /t - countdown in sec
            # /m - max postpone in min
            # /r - reboot instead of shutdown
            # /c - force & remove abort-btn
        }
    }

} else {
    Write-Verbose "Not a client OS"
}
# Done!

5 comments

  1. James Cramer

    Changed (“”) quote marks in the Arguments: to plain ASCII quote characters — but still can’t seem to get HStart64 to launch the .PS1 script — nothing happens.
    However, the ShutdownToll woindow does appear if I run the .PS1 file alone. Not sure why it won’t run if called from HStart64.exe.
    Any comments or suggestions would be welcome. Thank you.

  2. riro

    Should be double quote’s before %SystemRoot and then single after powershell.exe and before %ProgramFiles then ending with a double after .ps1

  3. James Cramer

    Thank you for the code block, I am making some progress.
    When I CD into the C:\Program Files\RebootIfNeeded folder and then run from a command prompt:

    hstart64 /NOCONSOLE /WAIT “”%SystemRoot%\system3
    2\WindowsPowerShell\v1.0\powershell.exe” -NoLogo -NoProfile -NonInteractive -Fil
    e “%ProgramFiles%\RebootIfNeeded\RebootIfNeeded.ps1″”

    The ShutdownTool window appears, however, when I set it up as a scheduled task, it does not run
    Still trying to get it works as a scheduled task. Many thanks for your assistance.