Script to Force User Logoff After-Hours #posh #ws2012 #windowsserver #wintel #IntelIT #corevpro #sysctr #configmgr

After-hours computer patch and software update management can be tricky, particularly if you have users in your environment who never logoff from their PCs! Below, I've included a PowerShell script that I've used in the past to forcibly logoff and restart PC's when outside of the allowed logon hours defined in Active Directory for users.  Of course, you may also have the reverse situation with some users - users who do power-down their PCs at the end of the workday and you need to remotely boot them after-hours to apply patches and deploy software updates.  For remote boot-up capabilities of powered-down machines, I'd recommend investigating Intel vPro processor technology combined with System Center Configuration Manager - together they can provide secure, enterprise-grade out-of-band management regardless of PC power state.

To get started with using the AutoLogOff script below, copy and paste the script into notepad and customize the following variables to match your environment: 

  • ComputerContainer - set to your Active Directory Container node at which search for Computers subjected to forced shutdown starts (ie., "ou=Computers") - Ideally, this OU should contain only the computer objects you'd like to restart
  • ExceptionAccounts - List of usernames exempt from forced shutdown (ie., "domain\user1,domain\admin1, ...") - be sure to include any admin or service accounts that you'd like to exclude
  • ExceptionMachines - List of computers exempt from forced shutdowns only if no one is logged in (ie., "PC001, PC002, ...") -  be sure to include any admin workstations that you'd like to exclude
  • LogPath - Path for existing shared folder path to store shutdown logfile, overwritten weekly (ie., "\\SERVER\SHARED\LOGS" ) - You'll need to create and share this folder on your network for write access by the account that runs this script.
  • NonDomainHours - Logon Hours byte array representing permitted logon hours for non-domain (typically local) accounts ( M:7-M:22 Tu:7-Tu:22 W:7-W:22 Th:7-Th:22 F:7-F:22 Sa:7-Sa:16 EST )
  • cmdReboot - General command line to run to force restart of each PC after-hours (ie. "Shutdown /r /f /m \\") - change /r switch to /s if you'd rather shutdown PC's to save power after-hours

Next, make sure PowerShell is configured on your Admin workstation to properly run unsigned scripts locally.  Launch PowerShell using the "Run as Administrator" option and enter the following command:

set-executionpolicy RemoteSigned

After saving this script into a local path on your admin workstation, such as c:\scripts\autologoff.ps1, you can run this script after hours prior to your patch/software update schedule to make sure that PC's are freshly restarted and waiting for updates to be applied by adding a scheduled task on your administrative workstation that runs the following command line: 

powershell -command "& 'c:\scripts\autologoff.ps1' "

Just place this above command into a .cmd batch file and schedule to run as a task in Windows Task Scheduler running under a user account that has local Admin rights on each workstation.  That's it!





# Change these variables to match your environment ...
$ComputerContainer = "ou=Computers,"
$ExceptionAccounts = "domain\user1,domain\admin1,domain\admin2,domain\user2,domain\user3"
$ExceptionMachines = "PC001,PC002,PC003"
[byte[]]$NonDomainHours = @(0,0,0,0,240,255,7,240,255,7,240,255,7,240,255,7,240,255,7,240,31)
$cmdReboot = "Shutdown /r /f /m \\"

# Function to evaluate whether current time is an authorized login time
Function Check-Hours ( [byte[]]$HoursArray ) {

 for ($i=0; $i -le 167; $i++) {
  $LogonArray[$i] = ([int] ([math]::pow(2,($i % 8))) -band ([int]($HoursArray[([math]::truncate($i/8))])))
  if ($LogonArray[$i] -ne 0) {$LogonArray[$i] = 1}

 if ($LogonArray[(($dayhash.item(($today.ToUniversalTime().DayofWeek).tostring())*24)+$today.ToUniversalTime().hour)] -eq 0) {
 } else {

$userDomain = New-Object System.DirectoryServices.DirectoryEntry
$Domain = $userDomain.distinguishedName.tostring()
$DomLength = $Domain.IndexOf(",") - 3

for ($i=0; $i -le 167; $i++) {$LogonArray += @(0)}
$DayHash = @{}

$today = get-date
$RptFileName = $LogPath+"ForcedLogoffs"+$today.DayofWeek.ToString().SubString(0,3)+".txt"
$ReportFile = New-Item -type file -force $RptFileName

" " | Out-File $ReportFile -encoding ASCII -append
"Nightly Forced Log Offs " + $today.DateTime | Out-File $ReportFile -encoding ASCII -append

# Forced Shutdowns only performed M-F, after 10:00pm and before 6:00am, Sat after 4:00pm or anytime Sun
# Since Logon Hours attribute is checked this is not really necessary but adds extra protection against
# accidental shutdowns during testing
if ( $today.hour -ge 22 -or $today.hour -lt 6 -or $today.DayofWeek -eq "Sunday" -or ($today.DayofWeek -eq "Saturday" -and $today.hour -ge 16) ) {
 $AfterHours = $True
} else {
 $AfterHours = $False
if ($AfterHours -eq $False) {" - Run during allowed hours **** Reporting Only ****" | Out-File $ReportFile -encoding ASCII -append}

" " | Out-File $ReportFile -encoding ASCII -append
" " | Out-File $ReportFile -encoding ASCII -append

$ObjFiler = "(objectCategory=Computer)"
$objSearch = New-Object System.DirectoryServices.DirectorySearcher
$objSearch.SearchRoot = "LDAP://"+$ComputerContainer+$Domain"
$ObjProp = "name"
$objSearch.Filter = $ObjFiler
$AllObj = $objSearch.FindAll()

foreach ($Obj in $AllObj) {

 $objItem = $Obj.Properties
 [string]$compname = $

 $ping = gwmi Win32_PingStatus -filter "Address='$compname'"

 if ($ping.StatusCode -eq 0) {
  $UserName = (gwmi -computer $compname Win32_ComputerSystem).Username
  if (([int]$UserName.Length) -gt 0) {
   if ($ExceptionAccounts.Contains( $UserName.ToLower() ) -eq $False) {

    $strUserName = $UserName.SubString( ($DomLength+1) )

    $userSearcher = New-Object System.DirectoryServices.DirectorySearcher
    $strFilter = "(&(objectCategory=User)(samAccountName=" + $strUserName + "))"
    $userSearcher.SearchRoot = $userDomain
    $userSearcher.PageSize = 1000
    $userSearcher.Filter = $strFilter
    $userSearcher.SearchScope = "Subtree"
    $colResults = $userSearcher.FindAll()

    $user = [ADSI]$colResults[0].Path
    $LogonHours = $user.logonHours
    $UserPermitted = $True

    if ([int]$colResults[0].Path.Length -eq 0) {
     $LogonHours += @($Null)
    if ($LogonHours[0] -eq $Null) {
     if ($UserName.Substring(0,($DomLength+1)) -eq ($Domain.substring(3,$DomLength)+"\")) {
      $UserPermitted = $True
     } else {
      $UserPermitted = Check-Hours $NonDomainHours
    } else {
     $UserPermitted = Check-Hours $LogonHours[0]

    if ($UserPermitted) {
     $compname + ":" + $UserName + " permitted to be logged in at this time" | Out-File $ReportFile -encoding ASCII -append
    } else {
     $cmd = $cmdReboot + $compname
     $cmd + " (" + $UserName + ")" | Out-File $ReportFile -encoding ASCII -append
     if ($AfterHours) {Invoke-Expression $cmd}
   } else {
    "Shutdown Exception made: " + $compname + ":" + $UserName | Out-File $ReportFile -encoding ASCII -append
  } else {
   if ($ExceptionMachines.Contains( $compname.ToUpper() ) -eq $False) {
    $cmd = $cmdReboot + $compname
    $compname + ": turned on but logged out - " + $cmd | Out-File $ReportFile -encoding ASCII -append
    if ($AfterHours) {Invoke-Expression $cmd}
   } else {
    $compname + ": turned on but logged out - Shutdown Exception made" | Out-File $ReportFile -encoding ASCII -append