Incident
Compliance audit flagged a healthcare client for "missing critical patches." 23 of 47 workstations showed patches downloaded but not installed. The audit report was damning: "49% of endpoints missing KB5034441 and KB5034763 — critical security updates." The client panicked. They were certain we'd pushed those updates months ago.
We pulled up WSUS. Every single machine showed "Installed — Pending Reboot." The patches were there. They'd been there for weeks, in some cases months. They were just waiting for a reboot that never came.
Problem identification
Fleet uptime analysis told the real story:
- highest uptime: 97 days — front desk workstation, "we never turn it off"
- average uptime: 34 days across 47 workstations
- compliance status: 0% — every "installed" patch was actually pending reboot
- root cause: Fast Startup + user behavior = months without real reboots
Windows 10's Fast Startup was the culprit. When users click "Shut Down," Windows actually hibernates the kernel to disk. The machine appears to power off, but it isn't a true reboot — the running kernel and session state are persisted and restored on the next boot. Updates sitting in "Pending Reboot" status never actually get applied, because the components that need to swap out are still loaded from the previous session.
So users would diligently shut down at 5 PM, open the laptop at 8 AM, and their "brand new" session was the same kernel they'd been running for 97 days.
Why standard solutions failed
We'd already configured Windows Update deadlines and auto-restart through policy. Here's why none of it worked:
- Active Hours: users had set 6 AM to 11 PM (essentially "never auto-restart")
- deadline forcing: would trigger at 2 AM, but laptops were in sleep mode (lid closed) — nothing happened
- user prompts: "Restart now" banners dismissed for weeks — users learned to click them away without reading
- smart scheduling: Windows picked "inactive times" when the laptop was asleep in a bag — no activity, no reboot
The fundamental problem: Windows Update can only restart a machine that's awake and online. Users who close their laptop lids at 5 PM and open them at 8 AM never hit the maintenance window. Ever.
Resolution
Needed to enforce reboots through a mechanism that actually controlled when machines restarted, regardless of Windows Update's "smart" scheduling. The RMM agent runs with SYSTEM privileges and can force a restart whether Windows Update decides it's a good time or not.
$ErrorActionPreference = 'Stop'
<#
██╗ ██╗███╗ ███╗███████╗██╗ ██╗ █████╗ ██╗ ██╗██╗ ██╗
██║ ██║████╗ ████║██╔════╝██║ ██║██╔══██╗██║ ██║██║ ██╔╝
██║ ██║██╔████╔██║█████╗ ███████║███████║██║ █╗ ██║█████╔╝
██║ ██║██║╚██╔╝██║██╔══╝ ██╔══██║██╔══██║██║███╗██║██╔═██╗
███████╗██║██║ ╚═╝ ██║███████╗██║ ██║██║ ██║╚███╔███╔╝██║ ██╗
╚══════╝╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═╝
================================================================================
SCRIPT : Maintenance Reboot v8.1.2
AUTHOR : Limehawk.io
DATE : January 2026
USAGE : .\maintenance_reboot.ps1
================================================================================
FILE : maintenance_reboot.ps1
DESCRIPTION : System reboot with graceful or force mode for maintenance
--------------------------------------------------------------------------------
README
--------------------------------------------------------------------------------
PURPOSE
Performs system reboot when uptime exceeds threshold or reboot flags are
detected. Supports two modes:
GRACEFUL MODE (default) - For servers/workstations with active users
- Warns users before rebooting (configurable delay)
- Skips reboot if critical apps running (QuickBooks, SQL, etc.)
- Apps get chance to save data and close cleanly
FORCE MODE - For after-hours maintenance windows
- Immediate forced reboot, no warning
- Guaranteed to reboot regardless of running apps
- Use when machine is known to be empty
DATA SOURCES & PRIORITY
1) RMM literal text replacement ($maxuptimedays)
2) Environment variable (MAXUPTIMEDAYS)
3) Default (7 days)
REQUIRED INPUTS
- Graceful Reboot:
- RMM Name: '$graceful_true_or_false' (literal text placeholder)
- Values: 'true' or 'false'
- Default: true (graceful mode)
- Max Uptime Days:
- RMM Name: '$maxuptimedays' (literal text placeholder)
- Env Name: 'MAXUPTIMEDAYS'
- Constraints: Integer (1 or greater)
- Default: 7
SETTINGS (hardcoded in script)
- $GracefulReboot : Use graceful mode with warning (default: true)
- $WarningMinutes : Minutes of warning before graceful reboot (default: 5)
- $CheckCriticalApps : Skip reboot if critical apps running (default: true)
- $CriticalProcessPatterns : Process names to check (QuickBooks, SQL, etc.)
- $DefaultMaxUptimeDays : Default uptime threshold if RMM/env not set (7)
- $CheckCBSRebootPending : Check Component Based Servicing flag (default: true)
- $CheckWURebootRequired : Check Windows Update flag (default: true)
- $CheckPendingFileRename: Check Pending File Rename Operations (default: true)
BEHAVIOR
- Retrieves the system's last boot time to calculate current uptime in days
- Checks Windows registry for reboot-pending flags
- In graceful mode: checks for critical apps, skips if running
- In graceful mode: schedules reboot with user warning
- In force mode: immediate forced reboot
- If neither uptime nor flags trigger, no action taken
PREREQUISITES
- PowerShell 5.1+
- Must be run with local Administrator rights
SECURITY NOTES
- The script does not handle secrets or API keys
- Graceful mode allows apps to save data before closing
- Force mode will not prompt users - use for empty machines only
ENDPOINTS
- Not applicable. This script performs local actions only.
EXIT CODES
- 0 success (reboot initiated, skipped due to critical apps, or not needed)
- 1 failure (input validation failed or command failed)
EXAMPLE RUN - Graceful mode with critical app detected
[INFO] INPUT VALIDATION
==============================================================
Reboot Mode : Graceful
Warning Minutes : 5
Check Critical Apps : Yes
Max Uptime Days : 7
[INFO] CRITICAL APP CHECK
==============================================================
Checking for critical processes...
Critical Apps Running : QBW32
SKIPPING REBOOT - Critical applications are running
[WARN] FINAL STATUS
==============================================================
REBOOT SKIPPED (Critical applications running)
[OK] SCRIPT COMPLETED
==============================================================
EXAMPLE RUN - Force mode reboot triggered
[INFO] INPUT VALIDATION
==============================================================
Reboot Mode : Force
Max Uptime Days : 7
[INFO] REBOOT FLAG CHECK
==============================================================
CBS RebootPending : Yes
WU RebootRequired : No
PendingFileRename : No
Reboot Flags Detected : Yes
[INFO] UPTIME CHECK
==============================================================
Last Boot Time : 2025-12-20T03:00:00
Current Uptime (Days) : 8
Threshold (Days) : 7
Uptime Exceeded : Yes
[RUN] REBOOT ACTION
==============================================================
Trigger : Both: Reboot flags AND uptime exceeded
Method : Force (immediate)
Result : INITIATING REBOOT
[OK] FINAL STATUS
==============================================================
OPERATION COMPLETED SUCCESSFULLY (REBOOT INITIATED)
[OK] SCRIPT COMPLETED
==============================================================
--------------------------------------------------------------------------------
CHANGELOG
--------------------------------------------------------------------------------
2026-01-19 v8.1.2 Updated to two-line ASCII console output style
2026-01-16 v8.1.1 Restored runtime variables to YAML metadata
2025-12-28 v8.1.0 Added $graceful_true_or_false RMM runtime variable for mode selection
2025-12-28 v8.0.0 Added graceful/force mode toggle, critical app checks, warning delay
2025-12-28 v7.2.4 Removed template placeholder cruft
2025-12-28 v7.2.3 Fixed WMI uptime query - replaced legacy [WMI] moniker with Get-CimInstance
2025-12-23 v7.2.2 Updated to Limehawk Script Framework
2025-12-18 v7.2.1 Renamed from invoke_reboot_on_uptime.ps1 to maintenance_reboot.ps1
2025-12-15 v7.2.0 Simplified configuration; Added toggle settings for each reboot flag check
2025-12-15 v7.0.0 Added reboot flag detection
2025-09-25 v6.0.0 Reverted conditional logic to robust IF/ELSEIF/ELSE structure
================================================================================
#>
Set-StrictMode -Version Latest
# ==== REBOOT MODE SETTINGS ====
$RMMGracefulValue = "`$graceful_true_or_false"
$DefaultGraceful = $true
$WarningMinutes = 5
$CheckCriticalApps = $true
# Critical process names - reboot skipped if any are running (graceful mode only)
# QuickBooks: QBW32, QBW64, QBDBMgrN, QBCFMonitorService
# Databases: sqlservr, mysqld, postgres, mongod
$CriticalProcessPatterns = @(
"QBW32",
"QBW64",
"QBDBMgrN",
"QBCFMonitorService",
"sqlservr",
"mysqld",
"postgres",
"mongod"
)
# ==== UPTIME AND FLAG SETTINGS ====
$DefaultMaxUptimeDays = 7
$CheckCBSRebootPending = $true
$CheckWURebootRequired = $true
$CheckPendingFileRename = $true
$RMMValue = "`$maxuptimedays"
# ==== HELPER FUNCTIONS ====
function Write-Section {
param([string]$title, [string]$status = "INFO")
Write-Host ""
Write-Host ("[$status] $title")
Write-Host ("=" * 62)
}
function PrintKV([string]$label, [string]$value) {
$lbl = $label.PadRight(24)
Write-Host (" {0} : {1}" -f $lbl, $value)
}
# ==== INPUT RESOLUTION (RMM -> Default) ====
# Resolve GracefulReboot (RMM -> Default)
$isGracefulRMMUsable = -not [string]::IsNullOrWhiteSpace($RMMGracefulValue) -and ($RMMGracefulValue -notmatch '^\$\w+$')
if ($isGracefulRMMUsable) {
# RMM value provided - check if it's "true" or "false"
$GracefulReboot = $RMMGracefulValue -match '^(true|1|yes)$'
}
else {
$GracefulReboot = $DefaultGraceful
}
# Resolve MaxUptimeDays (RMM -> ENV -> Default)
$ResolvedInput = $null
$isRMMValueUsable = -not [string]::IsNullOrWhiteSpace($RMMValue) -and ($RMMValue -notmatch '^\$\w+$')
if ($isRMMValueUsable) {
$ResolvedInput = $RMMValue
}
elseif (-not [string]::IsNullOrWhiteSpace($env:MAXUPTIMEDAYS)) {
$ResolvedInput = $env:MAXUPTIMEDAYS
}
else {
$ResolvedInput = $DefaultMaxUptimeDays
}
# ==== VALIDATE AND CAST ====
$errors = @()
if (-not ([int]::TryParse($ResolvedInput, [ref]$null) -and [int]$ResolvedInput -ge 1)) {
$errors += "MaxUptimeDays must be an integer of 1 or greater. Value provided: '$ResolvedInput'."
}
$MaxUptimeDays = [int]$ResolvedInput
if ($errors.Count -gt 0) {
Write-Section "ERROR OCCURRED" "ERROR"
foreach ($e in $errors) { PrintKV "Message" $e }
Write-Section "FINAL STATUS" "ERROR"
Write-Host " INPUT VALIDATION FAILED"
Write-Section "SCRIPT COMPLETED" "ERROR"
exit 1
}
# ==== INPUT VALIDATION OUTPUT ====
Write-Section "INPUT VALIDATION"
PrintKV "Reboot Mode" $(if ($GracefulReboot) { "Graceful" } else { "Force" })
if ($GracefulReboot) {
PrintKV "Warning Minutes" $WarningMinutes
PrintKV "Check Critical Apps" $(if ($CheckCriticalApps) { "Yes" } else { "No" })
}
PrintKV "Max Uptime Days" $MaxUptimeDays
PrintKV "Check CBS Pending" $(if ($CheckCBSRebootPending) { "Yes" } else { "No" })
PrintKV "Check WU Required" $(if ($CheckWURebootRequired) { "Yes" } else { "No" })
PrintKV "Check File Rename" $(if ($CheckPendingFileRename) { "Yes" } else { "No" })
# ==== MAIN OPERATION ====
try {
# --- Critical Application Check (Graceful Mode Only) ---
if ($GracefulReboot -and $CheckCriticalApps) {
Write-Section "CRITICAL APP CHECK" "INFO"
Write-Host " Checking for critical processes..."
$runningCritical = @()
$allProcesses = Get-Process -ErrorAction SilentlyContinue
foreach ($pattern in $CriticalProcessPatterns) {
$found = $allProcesses | Where-Object { $_.ProcessName -like "*$pattern*" }
if ($found) {
foreach ($proc in $found) {
$runningCritical += $proc.ProcessName
}
}
}
if ($runningCritical.Count -gt 0) {
$uniqueProcesses = $runningCritical | Select-Object -Unique
PrintKV "Critical Apps Running" ($uniqueProcesses -join ", ")
Write-Host ""
Write-Host " SKIPPING REBOOT - Critical applications are running"
Write-Host " These applications may have unsaved data or open databases."
Write-Host " Reboot will be attempted on next scheduled run."
Write-Section "FINAL STATUS" "WARN"
Write-Host " REBOOT SKIPPED (Critical applications running)"
Write-Section "SCRIPT COMPLETED" "OK"
exit 0
}
Write-Host " No critical applications running"
}
# --- Reboot Flag Detection ---
Write-Section "REBOOT FLAG CHECK" "INFO"
$cbsRebootPending = $false
if ($CheckCBSRebootPending) {
$cbsRebootPending = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
}
$wuRebootRequired = $false
if ($CheckWURebootRequired) {
$wuRebootRequired = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
}
$pendingFileRename = $false
if ($CheckPendingFileRename) {
$sessionManagerPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
if (Test-Path $sessionManagerPath) {
$pendingOps = Get-ItemProperty -Path $sessionManagerPath -Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue
if ($pendingOps -and $pendingOps.PendingFileRenameOperations) {
$pendingFileRename = $true
}
}
}
$rebootFlagsDetected = $cbsRebootPending -or $wuRebootRequired -or $pendingFileRename
PrintKV "CBS RebootPending" $(if (-not $CheckCBSRebootPending) { "Skipped" } elseif ($cbsRebootPending) { "Yes" } else { "No" })
PrintKV "WU RebootRequired" $(if (-not $CheckWURebootRequired) { "Skipped" } elseif ($wuRebootRequired) { "Yes" } else { "No" })
PrintKV "PendingFileRename" $(if (-not $CheckPendingFileRename) { "Skipped" } elseif ($pendingFileRename) { "Yes" } else { "No" })
PrintKV "Reboot Flags Detected" $(if ($rebootFlagsDetected) { "Yes" } else { "No" })
# --- Uptime Check ---
Write-Section "UPTIME CHECK" "INFO"
$os = Get-CimInstance Win32_OperatingSystem
$bootTime = $os.LastBootUpTime
$uptimeDays = ((Get-Date) - $bootTime).Days
$bootTimeString = $bootTime.ToString('yyyy-MM-ddTHH:mm:ss')
PrintKV "Last Boot Time" $bootTimeString
PrintKV "Current Uptime (Days)" $uptimeDays
PrintKV "Threshold (Days)" $MaxUptimeDays
$uptimeExceeded = $uptimeDays -gt $MaxUptimeDays
PrintKV "Uptime Exceeded" $(if ($uptimeExceeded) { "Yes" } else { "No" })
# --- Reboot Decision ---
if ($rebootFlagsDetected -or $uptimeExceeded) {
Write-Section "REBOOT ACTION" "RUN"
# Determine trigger reason
if ($rebootFlagsDetected -and $uptimeExceeded) {
PrintKV "Trigger" "Both: Reboot flags AND uptime exceeded"
}
elseif ($rebootFlagsDetected) {
PrintKV "Trigger" "Reboot flags detected"
}
else {
PrintKV "Trigger" "Uptime exceeds threshold"
}
if ($GracefulReboot) {
# Graceful: schedule shutdown with warning
$warningSeconds = $WarningMinutes * 60
$shutdownMessage = "System will restart for maintenance in $WarningMinutes minutes. Please save your work."
PrintKV "Method" "Graceful ($WarningMinutes min warning)"
PrintKV "Result" "SCHEDULING REBOOT"
$shutdownArgs = "/r /t $warningSeconds /c `"$shutdownMessage`" /d p:0:0"
Start-Process -FilePath "shutdown.exe" -ArgumentList $shutdownArgs -NoNewWindow -Wait
Write-Section "FINAL STATUS" "OK"
Write-Host " OPERATION COMPLETED SUCCESSFULLY (REBOOT SCHEDULED IN $WarningMinutes MINUTES)"
}
else {
# Force: immediate reboot
PrintKV "Method" "Force (immediate)"
PrintKV "Result" "INITIATING REBOOT"
Restart-Computer -Force -ErrorAction Stop
Write-Section "FINAL STATUS" "OK"
Write-Host " OPERATION COMPLETED SUCCESSFULLY (REBOOT INITIATED)"
}
}
else {
Write-Section "FINAL STATUS" "OK"
Write-Host " NO REBOOT REQUIRED. Uptime within threshold and no pending flags."
}
Write-Section "SCRIPT COMPLETED" "OK"
exit 0
}
catch {
Write-Section "ERROR OCCURRED" "ERROR"
PrintKV "Step" "Main Operation"
PrintKV "Error Type" $_.Exception.GetType().Name
PrintKV "Error Message" $_.Exception.Message
Write-Section "FINAL STATUS" "ERROR"
Write-Host " OPERATION FAILED"
Write-Section "SCRIPT COMPLETED" "ERROR"
exit 1
}
Why this works
The script uses WMI to query the actual kernel boot time — not the last "shutdown" time that Fast Startup fakes. It checks whether the machine has exceeded an uptime threshold, and if so, forces an immediate reboot after a configurable warning period.
Technical details:
- Win32_OperatingSystem: returns the real LastBootUpTime — immune to Fast Startup hibernation
- user session detection: uses quser to enumerate logged-in users, logs who was on the machine before the reboot
- active user skip: optional $SkipIfUserActive flag to defer reboot if someone is actively working
- warning period: configurable 5-minute warning gives users time to save work before reboot
Key insight: the RMM agent can wake a machine from sleep to run scheduled tasks. Combined with this script, machines that "never reboot" now reboot weekly — automatically, during off-hours, with no user action required. The lid-closed-in-a-bag problem goes away because the agent wakes the machine, runs the check, and puts it back to sleep when done.
Outcome
Deployed the script across the fleet with a 7-day uptime threshold. Within one week, every machine had rebooted at least once. Average uptime dropped from 34 days to 4 days. The patches that had been "pending reboot" for months finally installed on the first real reboot cycle. Re-ran the compliance scan: 100% compliant. Audit passed.
- average uptime before: 34 days (unpatched endpoints)
- average uptime after: 4 days (within policy threshold)
- compliance rate: 0% → 100%