В заметках "Скрипт обслуживания сервера 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).