2018-10-15

[Powershell] How to annoy your users to restart their computers more often

A few days ago someone on /r/sysadmin asked how others are making their users restart the computer more often.

Since I wrote a little Powershell script for exactly this purpose back in march I thought I'd share it here.


The script is triggered through a scheduled task deployed via group policy. For that purpose I've defined three triggers in the scheduled task:
- At log on
- On connection to user session
- On workstation unlock

So the script will be executed no matter whether the users logs into their machine, RDPs into it or simply unlocks it.

The script will display a message to the logged in user if one of two conditions is met:
- It has been more than 14 days since the last reboot
- It has been more than 7 days since the user logged in

If both conditions are met both messages are displayed.

To make sure the users are not mistaking the window for ransomware or a virus I've included a base64 encoded image of the company logo and a small note about calling the company's IT hotline for questions.

The window will stay on top for 30 seconds and then close automatically. To make the script more annoying there is a counter per message in the registry that will count how often each message has been displayed already. The base display duration of 30 seconds of the message will then be extended by X seconds (sum of both counters plus "some"). So the more often the people ignore the message the longer the message will be displayed.

If the script detects that the criteria to show the windows (<14 days of uptime, or <7 days of login) are not met the counters in the registry will be reset to 0.


Now for the script itself. Please read the comments (especially those marked with "ADJUST") in the code if you want to use the script yourself. (Please ignore the shitty formatting ... Blogger is quite useless when it comes to posting code.)


# ADJUST: These are the variables defining the number of days after which the messages will be displayed
$global:boot_limit = 14
$global:logon_limit = 7
# ADJUST: This variable is used to store the message counters in a company-named subfolder in the registry
$global:company = "Company"
function ShowGUI {
   
param($boot_days, $logon_days)

   
Add-Type -AssemblyName System.Windows.Forms
   
$Form = New-Object system.Windows.Forms.Form

   
$Font = New-Object System.Drawing.Font("Verdana", 12, [System.Drawing.FontStyle]::Bold)
   
$Font2 = New-Object System.Drawing.Font("Verdana", 8, [System.Drawing.FontStyle]::Bold)
   
$Label = New-Object System.Windows.Forms.Label
   
$Label2 = New-Object System.Windows.Forms.Label

   
[System.Windows.Forms.Application]::EnableVisualStyles()

   
$Form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
   
$Form.TopLevel = $true
   
$Form.Topmost = $true
   
$Form.AutoSize = $true
   
$Form.AutoSizeMode = "GrowAndShrink"
   
$Form.ControlBox = $false
   
$Form.ShowInTaskbar = $false
   
$Form.FormBorderStyle = "None"

   
$Label.AutoSize = $True
   
$Label.Location = New-Object System.Drawing.Size(10, 70)
   
$Label.Font = $Font
   
$Label.ForeColor = "0x00EF4122"
   
$Label.Text = ""

   
$cnt = 0
   
if ($boot_days -ge $global:boot_limit) {
       
Set-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Boot_Count" -Value ((Get-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Boot_Count").Boot_Count + 1) -Force -ErrorAction SilentlyContinue | Out-Null
       
# ADJUST: This is the message to be displayed if the computer has been running for longer than X days
       
$Label.Text += "The computer was last started more than $boot_days days ago.`n`nTo make sure the computers are performing correctly it is necessary`n`nto reboot the computers at least every two weeks.`n`n`n"
       
$cnt += 1
   
}
   
if ($logon_days -ge $global:logon_limit) {
       
Set-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Logon_Count" -Value ((Get-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Logon_Count").Logon_Count + 1) -Force -ErrorAction SilentlyContinue | Out-Null
       
# ADJUST: This is the message to be displayed if the user has been logged in for longer than X days
       
$Label.Text += "You are logged in for more than $logon_days days already.`n`nTo make sure the computers are performing correctly it is necessary`n`nto log out and back in at least once a week."
       
$cnt += 1
   
}

   
$Label2.AutoSize = $True
   
$Label2.Location = New-Object System.Drawing.Size(10, (50 + ($cnt * 140)))
   
$Label2.Font = $Font2
   
$Label2.ForeColor = "0x00000000"
   
# ADJUST: This is the additional note about calling the IT hotline for any questions
   
$Label2.Text = "For any questions call your IT hotline"


   
# ADJUST: This is the base64 encoded image with the company logo. Run the following two lines to encode an image in $path to base64
   
# Param([String]$path)
   
# [convert]::ToBase64String((get-content $path -encoding byte))
   
$img = "..."

   
$imgbytes = [Convert]::FromBase64String($img)
   
$ms = New-Object IO.MemoryStream($imgbytes, 0, $imgbytes.Length)
   
$ms.Write($imgbytes, 0, $imgbytes.Length)
   
$image = [System.Drawing.Image]::FromStream($ms, $true)

   
$picture = New-Object Windows.Forms.PictureBox
   
$picture.Width = $image.Size.Width
   
$picture.Height = $image.Size.Height
   
$picture.Image = $image

    $Form.Controls.Add($Label1)

   
$Form.Controls.Add($Label2)
   
$Form.Controls.Add($picture)
   
$Form.Visible=$True

   
# This makes sure the window stays open longer each time it is displayed, based on the number of times it has been displayed before
   
$multiplier = ((Get-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Boot_Count").Boot_Count + (Get-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Logon_Count").Logon_Count)
   
Sleep -seconds (5 + $multiplier)

   
$Form.Close()
}
# Create registry keys to keep track of number of times the user has been informed
if (-not (Test-Path "HKCU:\Software\$global:company\" -ErrorAction SilentlyContinue)) {
   
New-Item -Path "HKCU:\Software\$global:company\" -Force -ErrorAction SilentlyContinue | Out-Null
   
New-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Boot_Count" -Value 0 -Force -ErrorAction SilentlyContinue | Out-Null
   
New-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Logon_Count" -Value 0 -Force -ErrorAction SilentlyContinue | Out-Null
   
New-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "NoNag" -Value 0 -Force -ErrorAction SilentlyContinue | Out-Null
}
# Determine user's last logon time
# The script reads the output of "query.exe user" and parses the date and time returned by it using a regular expression.
# ADJUST: Make sure to change the regular expression to match your date format.
$query = query.exe user $env:username
($user, $logon, $matches) = ($null, $null, $null)
foreach ($line in $query) {
   
$temp = $line -match "^\>([a-zA-Z0-9-_]+).*((\d{2}\.){2}\d{4}\ \d{2}\:\d{2})$"
}
$user = $matches[1]
$last_logon = $matches[2]
# This calculates the timespan between NOW and the last time the user logged in
# ADJUST: Make sure the date format matches your locale
$last_logon_duration = (New-TimeSpan –Start ([datetime]::ParseExact($last_logon, 'dd.MM.yyyy HH:mm', $null)) -End (Get-Date))
# Determine system's last boot time from CIM
# ATTENTION: Systems with a depleted CMOS battery will likely contain a wrong value here and users might call and ask why their computer thinks it was last booted over 2000 days ago.
# ATTENTION: While the system time gets updated by NTP some time after booting the value in CIM will stay wrong. Replace the CMOS battery to fix the issue.
$last_boot = (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object LastBootUpTime).LastBootUpTime
$last_boot_duration = (New-TimeSpan –Start $last_boot -End (Get-Date))
# Reset message counters in the registry if the thresholds are not met
if ($last_logon_duration.Days -le $global:logon_limit) { Set-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Logon_Count" -Value 0 -Force -ErrorAction SilentlyContinue | Out-Null }
if ($last_boot_duration.Days -le $global:boot_limit)  { Set-ItemProperty -Path "HKCU:\Software\$global:company\" -Name "Boot_Count" -Value 0 -Force -ErrorAction SilentlyContinue | Out-Null }
# Check this key for existence of the "NoNag" value. Don't display the nagscreen if the value is "1"
if (Test-Path "HKCU:\Software\$global:company\" -ErrorAction SilentlyContinue) {
   
if ((Get-ItemProperty "HKCU:\Software\$global:company\" -Name "NoNag" -ErrorAction SilentlyContinue).NoNag -eq 1) {
       
break
   
}
}
# Check whether the time since the last logon or the last boot exceed the defined values. Show the message window if that is the case
if (($last_logon_duration.Days -gt $global:logon_limit) -or ($last_boot_duration.Days -gt $global:boot_limit)) {
   
ShowGUI -boot_days $last_boot_duration.Days -logon_days $last_logon_duration.Days
}

I'm sure there are a lot of things that could be improved upon, but for now the script works as intended.

As for how to roll this script out as a scheduled task using group policies, I'm sure you can figure this out yourself.


[1] https://twitter.com/BeingSysAdmin/status/1051736762454827009
[2] https://www.reddit.com/r/sysadmin/comments/9nl19t/gpo_for_reboot_nag_window/
[3] https://www.reddit.com/r/SysAdminBlogs/comments/9oajrj/powershell_how_to_annoy_your_users_to_restart/

3 comments:

  1. Nice one! I'll be trying to implement this :D

    ReplyDelete
  2. I would love to use this! When I save as .ps1 and run on my machine I get this error:

    Any ideas on how I can fix this or perhaps what I am doing wrong?

    Cannot index into a null array.
    At C:\Users\xx\downloads\test-restart-script.ps1:96 char:1
    + $user = $matches[1]
    + ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    Cannot index into a null array.
    At C:\Users\xx\downloads\test-restart-script.ps1:97 char:1
    + $last_logon = $matches[2]
    + ~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArray

    New-TimeSpan : Cannot bind parameter 'Start'. Cannot convert value "â€Start ([datetime]::ParseExact(, 'dd.MM.yyyy HH:mm', )) -End (Get-Date))
    # Determine system's last boot time from CIM
    # ATTENTION: Systems with a depleted CMOS battery will likely contain a wrong value here and users might call and ask why their computer thinks it was last booted over 2000
    days ago.
    # ATTENTION: While the system time gets updated by NTP some time after booting the value in CIM will stay wrong. Replace the CMOS battery to fix the issue.
    = (Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object LastBootUpTime).LastBootUpTime
    = (New-TimeSpan â€Start" to type "System.DateTime". Error: "The string was not recognized as a valid DateTime. There is an unknown word starting at index 0."
    At C:\Users\xx\downloads\test-restart-script.ps1:100 char:38
    + ... ew-TimeSpan –Start ([datetime]::ParseExact($last_logon, 'dd.MM.yyyy ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [New-TimeSpan], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.NewTimeSpanCommand

    ReplyDelete
  3. Any chance you've updated this? It sounds simple and great, just what a I need.
    Got some errors testing the ps script on my win 11 23h2
    Thank you!

    ReplyDelete