<# .SYNOPSIS Pops up a friendly "time to restart" reminder when the PC has been up longer than -Days. .DESCRIPTION Works out how long Windows has been running from the last boot time. If that's under -Days it exits quietly. Otherwise it shows a small window with the uptime and two buttons: Later, which dismisses it, and Restart now, which asks for confirmation and then restarts with a short countdown (shutdown /a cancels it). With -RemindAgainMinutes, choosing Later brings it back after that many minutes instead of ending. Made to run as a scheduled task at logon, in the signed-in user's session. -WhatIf shows the reminder but only reports the restart instead of doing it. Returns an object describing what happened. .PARAMETER Days Show the reminder when uptime is at least this many days. Default: 7. .PARAMETER RemindAgainMinutes After Later, wait this long and ask again. 0 (the default) means Later ends it until next time. .PARAMETER RestartDelaySeconds Countdown between confirming and the restart, so open apps get a moment. Default: 60. .PARAMETER Title Window title. Default: Time for a restart. .PARAMETER AlwaysShow Show the reminder whatever the uptime. Handy for testing how it looks. .EXAMPLE .\Show-UptimeReminder.ps1 -Days 5 .EXAMPLE .\Show-UptimeReminder.ps1 -AlwaysShow -WhatIf #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateRange(1, 365)] [int]$Days = 7, [ValidateRange(0, 1440)] [int]$RemindAgainMinutes = 0, [ValidateRange(0, 3600)] [int]$RestartDelaySeconds = 60, [ValidateNotNullOrEmpty()] [string]$Title = 'Time for a restart', [switch]$AlwaysShow ) $lastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem -Property LastBootUpTime).LastBootUpTime $uptime = (Get-Date) - $lastBoot $result = [pscustomobject]@{ ComputerName = $env:COMPUTERNAME LastBoot = $lastBoot UptimeDays = [math]::Round($uptime.TotalDays, 1) ThresholdDays = $Days Response = 'NotNeeded' } if ($uptime.TotalDays -lt $Days -and -not $AlwaysShow) { return $result } if ([Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') { Write-Warning 'Windows Forms needs a single-threaded apartment. Run this with powershell.exe, or pwsh -STA.' } Add-Type -AssemblyName System.Windows.Forms, System.Drawing -ErrorAction Stop [System.Windows.Forms.Application]::EnableVisualStyles() function Show-Reminder { $upText = if ($uptime.TotalDays -ge 2) { '{0} days' -f [math]::Floor($uptime.TotalDays) } else { '{0} hours' -f [math]::Floor($uptime.TotalHours) } $font = [System.Drawing.Font]::new('Segoe UI', 10) $form = [System.Windows.Forms.Form]@{ Text = $Title ClientSize = [System.Drawing.Size]::new(440, 170) StartPosition = 'CenterScreen' FormBorderStyle = 'FixedDialog' MaximizeBox = $false MinimizeBox = $false TopMost = $true ShowInTaskbar = $true Font = $font } $icon = [System.Windows.Forms.PictureBox]@{ Image = [System.Drawing.SystemIcons]::Information.ToBitmap() SizeMode = 'AutoSize' Location = [System.Drawing.Point]::new(20, 22) } $heading = [System.Windows.Forms.Label]@{ Text = "This PC has been running for $upText." Font = [System.Drawing.Font]::new('Segoe UI', 11, [System.Drawing.FontStyle]::Bold) Location = [System.Drawing.Point]::new(64, 20) AutoSize = $true } $body = [System.Windows.Forms.Label]@{ Text = "A restart clears out memory leaks and finishes installing updates. Save your work and restart when it suits you. (Last started $($lastBoot.ToString('g')).)" Location = [System.Drawing.Point]::new(64, 50) Size = [System.Drawing.Size]::new(356, 60) } $later = [System.Windows.Forms.Button]@{ Text = 'Later' Size = [System.Drawing.Size]::new(110, 32) Location = [System.Drawing.Point]::new(190, 122) DialogResult = 'Cancel' } $restart = [System.Windows.Forms.Button]@{ Text = 'Restart now' Size = [System.Drawing.Size]::new(110, 32) Location = [System.Drawing.Point]::new(310, 122) DialogResult = 'OK' } $form.Controls.AddRange(@($icon, $heading, $body, $later, $restart)) $form.AcceptButton = $later # Enter shouldn't restart anyone by accident $form.CancelButton = $later # Esc and the X both mean Later try { $form.ShowDialog() } finally { $form.Dispose(); $font.Dispose() } } while ($true) { $choice = Show-Reminder if ($choice -eq [System.Windows.Forms.DialogResult]::OK) { $confirm = [System.Windows.Forms.MessageBox]::Show( "Restart in $RestartDelaySeconds seconds? Anything unsaved will be lost.", $Title, 'YesNo', 'Warning', 'Button2') if ($confirm -eq [System.Windows.Forms.DialogResult]::Yes) { if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Restart in $RestartDelaySeconds seconds")) { try { & "$env:SystemRoot\System32\shutdown.exe" /r /t $RestartDelaySeconds /c "Restarting in $RestartDelaySeconds seconds. Run 'shutdown /a' to cancel." /d p:0:0 if ($LASTEXITCODE -ne 0) { throw "shutdown.exe exited with code $LASTEXITCODE" } $result.Response = 'Restart' } catch { $result.Response = "RestartFailed: $($_.Exception.Message)" Write-Warning $result.Response } } else { $result.Response = 'Restart (WhatIf)' } break } continue # They said no to the confirmation; show the reminder again. } $result.Response = 'Later' if ($RemindAgainMinutes -le 0) { break } Start-Sleep -Seconds ($RemindAgainMinutes * 60) $uptime = (Get-Date) - $lastBoot } $result