<#
.SYNOPSIS
Runs the TEMP profile cleanup remotely across the HURCO Citrix VDA farm.
.DESCRIPTION
Uses PowerShell Remoting (WinRM) to connect to each target server in
parallel and remove stale TEMP profile folders under C:\Users - matching
TEMP* (e.g. TEMP.HURCO.055, TEMP.UK.004, TEMP.FR.000) as well as the
legacy "<username>.TEMP" style - plus their orphaned entries under
HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList.
Deletion is permanent (Remove-Item bypasses the Recycle Bin).
.PARAMETER ComputerName
Target server names. Defaults to the HURCO Citrix VDA farm.
.PARAMETER Path
Remote path to scan on each server. Defaults to C:\Users.
.PARAMETER Credential
Credential with local admin rights on the target servers. Prompted for if
not supplied.
.PARAMETER WhatIf
Preview only - reports what would be deleted on each server without
deleting anything.
.EXAMPLE
.\Invoke-RemoteTempProfileCleanup.ps1 -WhatIf
Preview across all 8 default servers.
.EXAMPLE
.\Invoke-RemoteTempProfileCleanup.ps1
Actually deletes across all 8 default servers (prompts for credentials).
.EXAMPLE
.\Invoke-RemoteTempProfileCleanup.ps1 -ComputerName HURCO-CX33 -WhatIf
Preview against a single server.
.NOTES
Requires:
- WinRM enabled on each target (Enable-PSRemoting) and reachable on
TCP 5985 (HTTP) or 5986 (HTTPS) from this machine.
- An account with local Administrator rights on each target server.
- If this machine is NOT domain-joined, the targets must be added to
this machine's WinRM TrustedHosts list (Kerberos won't be available):
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "HURCO-CX*" -Force
#>
[CmdletBinding()]
param(
[string[]]$ComputerName = @(
'HURCO-CX32', 'HURCO-CX33', 'HURCO-CX34', 'HURCO-CX35',
'HURCO-CX42', 'HURCO-CX43', 'HURCO-CX44', 'HURCO-CX45'
),
[string]$Path = 'C:\Users',
[pscredential]$Credential,
[switch]$WhatIf
)
if (-not $Credential) {
$Credential = Get-Credential -Message "Enter an account with local admin rights on the target servers"
}
# This scriptblock runs ON EACH REMOTE SERVER. It mirrors the logic in
# Remove-TempProfiles.ps1, but reports results back as objects instead of
# writing to a local log file, and uses a plain boolean instead of the
# ShouldProcess/-WhatIf machinery (more reliable across a remoting boundary).
$remoteScript = {
param($Path, $PreviewOnly)
$result = [System.Collections.Generic.List[object]]::new()
$profileListPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
$activeSessionUsers = @()
try {
$quserOutput = quser 2>$null
if ($quserOutput) {
$activeSessionUsers = $quserOutput | Select-Object -Skip 1 | ForEach-Object {
($_ -replace '^\s*>', '').Trim() -split '\s+' | Select-Object -First 1
} | Where-Object { $_ }
}
} catch {}
$tempFolders = Get-ChildItem -Path $Path -Directory -ErrorAction SilentlyContinue |
Where-Object {
$_.Name -like 'TEMP*' -or
$_.Name -like '*.TEMP' -or
$_.Name -like '*.TEMP.*'
}
foreach ($folder in $tempFolders) {
$baseName = $folder.Name -replace '\.TEMP(\.\d+)?$', ''
if ($activeSessionUsers -contains $baseName) {
$result.Add([pscustomobject]@{ Folder = $folder.FullName; Status = 'Skipped (active session)' })
continue
}
$inUse = $false
$lockCheckFile = Join-Path $folder.FullName '.lockcheck.tmp'
try {
Set-Content -Path $lockCheckFile -Value 'x' -ErrorAction Stop
Remove-Item -Path $lockCheckFile -ErrorAction SilentlyContinue
} catch {
$inUse = $true
}
if ($inUse) {
$result.Add([pscustomobject]@{ Folder = $folder.FullName; Status = 'Skipped (locked/in use)' })
continue
}
if ($PreviewOnly) {
$result.Add([pscustomobject]@{ Folder = $folder.FullName; Status = 'Would delete (WhatIf)' })
continue
}
try {
Remove-Item -Path $folder.FullName -Recurse -Force -ErrorAction Stop
} catch {
$result.Add([pscustomobject]@{ Folder = $folder.FullName; Status = "Error: $($_.Exception.Message)" })
continue
}
Get-ChildItem $profileListPath -ErrorAction SilentlyContinue | ForEach-Object {
$props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
if ($props.ProfileImagePath -and $props.ProfileImagePath -ieq $folder.FullName) {
try { Remove-Item -Path $_.PSPath -Recurse -Force -ErrorAction Stop } catch {}
}
}
$result.Add([pscustomobject]@{ Folder = $folder.FullName; Status = 'Deleted' })
}
if ($result.Count -eq 0) {
$result.Add([pscustomobject]@{ Folder = '(none found)'; Status = 'N/A' })
}
$result
}
Write-Host "Connecting to $($ComputerName.Count) server(s)$(if ($WhatIf) { ' (WhatIf - preview only)' })..." -ForegroundColor Cyan
$jobs = Invoke-Command -ComputerName $ComputerName -Credential $Credential `
-ScriptBlock $remoteScript -ArgumentList $Path, $WhatIf.IsPresent `
-AsJob -JobName 'TempProfileCleanup'
$jobs | Wait-Job | Out-Null
$report = foreach ($job in $jobs.ChildJobs) {
$computer = $job.Location
try {
Receive-Job -Job $job -ErrorAction Stop | ForEach-Object {
[pscustomobject]@{
Server = $computer
Folder = $_.Folder
Status = $_.Status
}
}
} catch {
[pscustomobject]@{
Server = $computer
Folder = '(connection failed)'
Status = $_.Exception.Message
}
}
}
$stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$csvPath = Join-Path $PWD "TempProfileCleanup_Report_$stamp.csv"
$report = $report | Sort-Object Server, Folder
$report | Format-Table -AutoSize
$report | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "`nReport saved to: $csvPath" -ForegroundColor Green
Get-Job -Name 'TempProfileCleanup' -ErrorAction SilentlyContinue | Remove-Job -Force