Скрипт обслуживания сервера RDS (версия 2)

17.02.2017

В заметках "Скрипт обслуживания сервера RDS" и "PowerShell - Удаление профилей с сервера" я уже рассказывал о скрипте для очистки профилей и очереди печати на терминальных серверах. В этой публикации я размещаю обновленный текст кода Powershell, который я оптимизировал для обслуживания терминальных серверов Windows Server 2012.

# --- Description ---
# This script is for maintainance of Windows 2012 RDS server in roaming profile mode.
# - remove users from access to RDP
# - force users logoff from the server
# - delete broken profile folders (folders must be removed after user logoff)
# - delete broken profile registry section (registry sections must be removed after user logoff)
# - delete profile oprimization scheduled tasks
# - stop print spooler, delete all print jobs, run DeleteInactivePortSilently.exe, start print spooler
# - restore users access to RDP
# - reboot the server on Monday

# --- Declare variables ---

$ExcludedProfilePaths = New-Object System.Collections.ArrayList
$ExcludedRegistrySections = New-Object System.Collections.ArrayList

# --- Input data ---

# !!!!!   DON'T FORGET TO EXCLUDE SERVICE ACCOUNTS. LOCAL ACCOUNTS WILL BE EXCLUDED AUTOMATICALLY   !!!!!

$ProfileFolder = "C:\Users"
$ProfileRegistrySection = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
$DomainRDSGroup = "RDS_Application_Users"
$LocalRDSGroup = "Remote Desktop Users"
$ZipArhiverPath = "C:\Scripts\7za.exe"
[void] $ExcludedProfilePaths.AddRange( ("C:\Users\All Users", "C:\Users\Default", "C:\Users\Default User", `
 "C:\Users\Public") )
[void] $ExcludedRegistrySections.AddRange( ("S-1-5-18", "S-1-5-19", "S-1-5-20") )

# --- Functions ---

Function Write-ScriptLog {
 Param(
   [CmdletBinding()]
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
   [String]$Message,
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
   [String]$LogFile
 )
 Process {
   $LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
   $LogMessage += "`t"
   $LogMessage += $Message
   $LogMessage | Out-File -FilePath $LogFile -Append
 }
}#End Function

Function Get-ComputerSessions {
 Param(
   [CmdletBinding()]
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
   [String]$Computer
 )
 Process {
   $Report = quser /server:$Computer | Select-Object -Skip 1 | ForEach-Object {
     $CurrentLine = $_.Trim() -Replace '\s+',' ' -Split '\s'
     $HashProps = @{
       UserName = $CurrentLine[0]
       ComputerName = $Computer
     }
     # If session is disconnected, different fields will be selected
     If ($CurrentLine[2] -eq 'Disc') {
       $HashProps.SessionName = $null
       $HashProps.Id = $CurrentLine[1]
       $HashProps.State = $CurrentLine[2]
       $HashProps.IdleTime = $CurrentLine[3]
       $HashProps.LogonTime = $CurrentLine[4..6] -join ' '
     } Else {
       $HashProps.SessionName = $CurrentLine[1]
       $HashProps.Id = $CurrentLine[2]
       $HashProps.State = $CurrentLine[3]
       $HashProps.IdleTime = $CurrentLine[4]
       $HashProps.LogonTime = $CurrentLine[5..7] -join ' '
     }
     New-Object -TypeName PSCustomObject -Property $HashProps |
       Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleTime,LogonTime
   }
   Return $Report
 }
}#End Function

Function Process-DeleteProfileFolders {
 Param(
   [CmdletBinding()]
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
   [String]$ProfileFolder,
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
   [String[]]$ExcludedProfiles,
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
   [String]$LogFile = $Nothing
 )
 Process {
   $Report = $Nothing
   # Получение объектов из папки профилей
   $SubDirs= Get-ChildItem $ProfileFolder -Force
   # Обработка объектов из папки профилей
   ForEach ($Dir in $SubDirs) {
     $LogMessage = $Nothing
     # Проверка, что объект существуюет и это папка
     If (Test-Path $Dir.FullName -PathType Container) {
       # Проверка, что профиль - это не служебная папка
       $NotDeleteFlag = $False
       ForEach ($ExcludedProfile in $ExcludedProfiles) {
         If ($Dir.FullName -eq $ExcludedProfile) {
           $NotDeleteFlag = $True
         }
       }
       # Removing the profile
       If ($NotDeleteFlag -eq $False) {
         $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
         $LogMessage += "`t"
         $LogMessage += "Delete " + $Dir.FullName + "."
         # Executing removing process
         $CmdLine = "CMD /c RD /S /Q """ + $Dir.FullName + """"
         Invoke-Expression -command $CmdLine
       } Else {
         $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
         $LogMessage += "`t"
         $LogMessage += "Skip " + $Dir.FullName + " - service folder."
       }
     } Else {
       $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
       $LogMessage += "`t"
       $LogMessage += "Skip " + $Dir.FullName + " - file."
     }
     If ($LogFile.Length -gt 0) {
       $LogMessage | Out-File -FilePath $LogFile -Append
     } else {
       Write-Output $LogMessage
     }
   }
 }
}#End Function

Function Process-DeleteProfileRegistry {
 Param(
   [CmdletBinding()]
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
   [String]$ProfileRegistrySection,
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
   [String[]]$ExcludedRegistrySections,
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
   [String]$LogFile = $Nothing
 )
 Process {
   $Report = $Nothing
   # Convering the registry path to Powershell path
   $ProfileRegistrySection = $ProfileRegistrySection.Replace("HKEY_LOCAL_MACHINE","HKLM:")
   # Requesting profile sections from the registry
   $Sections= Get-ChildItem $ProfileRegistrySection
   ForEach ($Section in $Sections) {
     $LogMessage = $Nothing
     # Filtering exclusions
     $NotDeleteFlag = $False
     ForEach ($ExcludedRegistrySection in $ExcludedRegistrySections) {
       If ($Section.Name.Contains($ExcludedRegistrySection)) {
         $NotDeleteFlag = $True
       }
     }
     # Removing profile sections
     If ($NotDeleteFlag -eq $False) {
       $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
       $LogMessage += "`t"
       $LogMessage += "Delete " + $Section.Name + "."
       # Processing deleting the profile section
       $SectionPath = $Section.Name.Replace("HKEY_LOCAL_MACHINE","HKLM:")
       Remove-Item -Path $SectionPath -Force -Recurse
     } Else {
       $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
       $LogMessage += "`t"
       $LogMessage += "Skip " + $Section.Name + " - service profile."
     }
     If ($LogFile.Length -gt 0) {
       $LogMessage | Out-File -FilePath $LogFile -Append
     } Else {
      # Write-Output $LogMessage
     }
   }
 }
}#End Function

Function Process-ResetPrintSpooler {
 Param(
   [CmdletBinding()]
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
   [String]$LogFile = $Nothing
 )
 Process {
   $LogMessage = $Nothing
   $Error.Clear()
   # Остановка сервиса
   Try {
     Stop-Service -Name "Spooler" -Force
     $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" +
       "Print Spooler service is stopped." + "`r`n"
   } Catch {
     $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error + "`r`n"
   }
   # Очистка заданий
   Try {
       Remove-Item -Path "C:\Windows\System32\spool\PRINTERS\*.*" -Force
       $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + "Print queue is purged." + "`r`n"
   }
   Catch {
       $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error + "`r`n"
   }
   # Очистка неактивных портов принтеров терминального сервера
   $CmdLine = $ScriptTools + "DeleteInactivePortSilently.exe"
   $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" +
     "Delete Inactive TS Ports by DeleteInactivePortSilently.exe command." + "`r`n"
   $CommandOutput = Invoke-Expression -command $CmdLine
   $CommandOutput | ForEach-Object {
     If ($_.Length -gt 0) {
       $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $_ + "`r`n"
     }
   }
   # Запуск сервиса
   Try {
     Start-Service -Name "Spooler"
     $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + "Print Spooler service is started."
   } Catch {
     $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error
   }
   # Запись события в лог-файл
   If ($LogFile.Length -gt 0) {
     $LogMessage | Out-File -FilePath $LogFile -Append
   } Else {
     Write-Output $LogMessage
   }
 }
}#End Function

Function Get-Monday {
 Param(
   [CmdletBinding()]
   [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
   [DateTime]$DateValue
 )
 Process {
   For ($i=0; $i -le 6; $i++) {
     If (($DateValue).adddays(-$i).DayOfWeek -eq "Monday") {
       Return ($DateValue).adddays(-$i)
     }
   }
 }
}#End Function

# --- Start ---

$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length - `
 $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" + `
 $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"
$OSName = (Get-WmiObject -class Win32_OperatingSystem).Caption
$MondayDate = Get-Monday (Get-Date)
$CurrentDate = Get-Date
$DomainRDSGroupObject = [ADSI]"WinNT://glaverbel.com/$DomainRDSGroup,group"
$LocalRDSGroupObject = [ADSI]"WinNT://./$LocalRDSGroup,group"

# Adding local accounts SIDs to exclusions
$LocalAccounts = Get-WmiObject Win32_UserAccount -Filter "Domain='$env:computername'"
ForEach ($LocalAccount in $LocalAccounts) {
 $CurrentSID = $LocalAccount.SID
 If ($CurrentSID -ne $Nothing) {
   [void] $ExcludedRegistrySections.Add( $CurrentSID )
 }
}

# Adding local accounts Profiles paths to exclusions
ForEach ($CurrentSID in $ExcludedRegistrySections) {
 $CurrentPath =  (Get-WmiObject Win32_UserProfile -Filter "SID='$CurrentSID'").LocalPath
 If ($CurrentPath -ne $Nothing) {
   [void] $ExcludedProfilePaths.Add( $CurrentPath )
 }
}

# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
 New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile

# Check OS version
If (-Not $OSName.Contains("2012")) {
 Write-ScriptLog -LogFile $LogFile -Message ("The operating system is not match for this script.")
 Exit
}

# Extracting additional files
$ScriptTools = $ScriptFolder + `
 $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + "\"
$ScriptToolsArchive = $ScriptFolder + `
 $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".zip"
If (Test-Path $ScriptToolsArchive -PathType Leaf) {
 Write-ScriptLog -LogFile $LogFile -Message ("Script tools are found in " + `
   $ScriptToolsArchive + " archive.")
 Write-ScriptLog -LogFile $LogFile -Message ("Extracting the archive into " + $ScriptTools +".")

 New-Item -Path $ScriptTools -ItemType directory -Force
 $Shell = New-Object -com shell.application
 $ArchiveItem = $shell.NameSpace($ScriptToolsArchive)
 ForEach($Item in $ArchiveItem.items()) {
   $Shell.Namespace($ScriptTools).copyhere($Item, 0x14)
 }

 If (Test-Path $ZipArhiverPath -PathType Leaf) {
   [String]$CmdLine = $ZipArhiverPath
   [Array]$CmdLineArg = 'x', """$ScriptToolsArchive""",  "-o""$ScriptTools""", "-y"
   # Оператор & (ampersand) указывает, то необходимо выполнить внешнюю команду, указанную после него
   # подробнее тут https://technet.microsoft.com/en-us/library/ee176880.aspx
   Write-ScriptLog -LogFile $LogFile -Message (&$CmdLine $CmdLineArg)
 } Else {
   Write-ScriptLog -LogFile $LogFile -Message ("C:\Scripts\7za.exe is not found. " + `
     "Script execution can't be continue.")
   Exit
 }
}  


# Forbiging users for loging  - Removing users from Remote Desktop group
Write-ScriptLog -LogFile $LogFile -Message ("---   Closing the server for users   ---")
$LocalRDSGroupObject.Remove($DomainRDSGroupObject.Path)
Write-ScriptLog -LogFile $LogFile -Message ($DomainRDSGroupObject.Name.ToString() + `
 " is removed from " + $LocalRDSGroupObject.Name.ToString() + ".")
Write-ScriptLog -LogFile $LogFile -Message ("The server is closed for users.")

# Logging off active users
$Sessions = Get-ComputerSessions -Computer localhost
ForEach ($Session in $Sessions) {
 logoff $Session.Id /server:localhost
 Write-ScriptLog -LogFile $LogFile -Message ($Session.UserName + " (" + $Session.Id + `
   ") forced to logoff from the server.")
}

# Waiting for 30 seconds
Write-ScriptLog -LogFile $LogFile -Message ("Waiting 30 seconds for users logoff.")
Start-Sleep -s 30

# Check if there are no users sessions
$Sessions = Get-ComputerSessions -Computer localhost
If ($Sessions.Count -gt 0) {

 Write-ScriptLog -LogFile $LogFile -Message ("Currently logged on users number is " + `
   $Sessions.Count + ".")
 $ActiveSessions = "`r`n"
 $ActiveSessions += $Sessions | FT ID, UserName, State, IdleTime, LogonTime
 Write-ScriptLog -LogFile $LogFile -Message ($ActiveSessions)
 Write-ScriptLog -LogFile $LogFile -Message ("The script processing is terminated, " + `
   "please reboot the server to release sessions.")

} Else {

# Executing cleanup
 Write-ScriptLog -LogFile $LogFile -Message ("Currently logged on users number is 0.")
 # Profiles cleanup
 Write-ScriptLog -LogFile $LogFile -Message ("---   Profiles cleanup   ---")
 Process-DeleteProfileFolders -ProfileFolder $ProfileFolder `
    -ExcludedProfiles $ExcludedProfilePaths -LogFile $LogFile
 # Registry cleanup
 Write-ScriptLog -LogFile $LogFile -Message ("---   Registry cleanup   ---")
 Process-DeleteProfileRegistry -ProfileRegistrySection $ProfileRegistrySection `
   -ExcludedRegistrySections $ExcludedRegistrySections -LogFile $LogFile
 # Task scheduler cleanup
 Write-ScriptLog -LogFile $LogFile -Message ("---   Task scheduler cleanup   ---")
 Write-ScriptLog -LogFile $LogFile -Message ("Delete ""Optimize Start Menu Cache Files"" scheduled tasks.")
 Get-ScheduledTask | Where {$_.taskname -like "Optimize Start Menu Cache Files*"} | `
   Unregister-ScheduledTask -Confirm:$false
 # Print prooler cleanup
 Write-ScriptLog -LogFile $LogFile -Message ("---   Print prooler cleanup   ---")
 Process-ResetPrintSpooler -LogFile $LogFile
}

# Allowing users for loging  - Adding users to Remote Desktop group
Write-ScriptLog -LogFile $LogFile -Message ("---   Openning the server for users   ---")
$LocalRDSGroupObject.Add($DomainRDSGroupObject.Path)
Write-ScriptLog -LogFile $LogFile -Message ($DomainRDSGroupObject.Name.ToString() + " is added to " +
 $LocalRDSGroupObject.Name.ToString() + ".")
Write-ScriptLog -LogFile $LogFile -Message ("The server is opened for users.")

# Remove auxiliary files
$CmdLine = "CMD /c RD /S /Q """ + $ScriptTools + """"
Invoke-Expression -command $CmdLine
Write-ScriptLog -LogFile $LogFile -Message ("$ScriptTools auxiliary folder is deleted.")

#Reboot server
If ($CurrentDate.Date -eq $MondayDate.Date) {
 Write-ScriptLog -LogFile $LogFile -Message ("Today is Monday - rebooting the server.")
 Restart-Computer -ComputerName "localhost"
}

Указанный скрипт рекомендуется поставить в планировщик Windows на ежедневное выполнение в ночные часы, когда на серверах нет пользователей. Запуск задания нужно выполнять от имени системы (от имени пользователя SYSTEM).