The challenge? Your environment has strict security controls:
- Remote PowerShell/WinRM is disabled for security reasons
- Traditional PsExec runs synchronously, hanging around waiting for each
gpupdateto complete - Running commands sequentially on dozens or hundreds of servers would take forever
- Built-in Group Policy cmdlets require remote management features I couldn't use
Why Traditional Approaches Fail
When you run PsExec normally, it connects to the remote server, executes the command, and waits for it to complete before returning control. For a command like gpupdate /force that can take 30-60 seconds per server, this sequential approach simply doesn't scale.
Running PsExec \\<server> gpupdate /force on 100 servers sequentially could take over an hour - completely defeating the purpose of an "immediate" update!
The Solution: Fire and Forget with PsExec -d
The key insight was using PsExec's -d (detach) flag. This flag tells PsExec to start the remote process and immediately return control without waiting for completion. Combined with PowerShell's ability to launch processes asynchronously, I could effectively "spray" the gpupdate command across all servers in seconds.
Here's the approach:
- Launch PsExec with the
-dflag to avoid waiting - Use PowerShell's
Start-Processto fire off commands rapidly - Track deployment status (not execution results) in a CSV
- Complete the entire operation in under 60 seconds
Important Caveat
This approach gives you deployment confirmation, not execution results. You'll know that the command was successfully dispatched to each server, but you won't get real-time feedback about whether the gpupdate completed successfully. For most scenarios where you need immediate GP refresh, this trade-off is acceptable.
Script : gpupdate-force.ps1
Here's the complete PowerShell script that solved my problem:
# ---- CONFIGURATION ----
$ServerListFile = "servers.txt"
$PsExecPath = "PsExec.exe"
$Command = "gpupdate /force"
$OutputFileName = "GPUpdate_Launch_Report.csv"
$StartTime = Get-Date
# Read the server list
try {
$Servers = Get-Content $ServerListFile | Where-Object { $_.Trim() -ne "" }
}
catch {
Write-Host "[FATAL ERROR] Could not read '$ServerListFile'. Details: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host "`n==================================================" -ForegroundColor DarkCyan
Write-Host " GPUpdate Bulk Launcher (Fast Mode)" -ForegroundColor White
Write-Host " Servers: $($Servers.Count)" -ForegroundColor White
Write-Host " Command: $Command" -ForegroundColor White
Write-Host "==================================================" -ForegroundColor DarkCyan
# Simple approach - just fire and forget
$LaunchResults = @()
$counter = 1
foreach ($Server in $Servers) {
Write-Host "[$counter/$($Servers.Count)] $Server..." -NoNewline
$result = @{
Server = $Server
TimeOfLaunch = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
# Quick ping test
$ping = Test-Connection -ComputerName $Server -Count 1 -Quiet -ErrorAction SilentlyContinue
if (!$ping) {
$result.Status = "UNREACHABLE"
$result.Details = "Cannot ping server"
Write-Host " [UNREACHABLE]" -ForegroundColor Yellow
}
else {
try {
# Use Start-Process with -PassThru to get the process object
$proc = Start-Process -FilePath $PsExecPath `
-ArgumentList "\\$Server", "-accepteula", "-d", "-s", $Command `
-WindowStyle Hidden `
-PassThru `
-ErrorAction Stop
# Give it a moment to start
Start-Sleep -Milliseconds 500
# Check if process started successfully
if ($proc -and !$proc.HasExited) {
$result.Status = "LAUNCHED"
$result.Details = "Command dispatched successfully"
Write-Host " [LAUNCHED]" -ForegroundColor Green
}
else {
$result.Status = "FAILED"
$result.Details = "Process exited immediately"
Write-Host " [FAILED]" -ForegroundColor Red
}
}
catch {
$result.Status = "ERROR"
$result.Details = $_.Exception.Message
Write-Host " [ERROR]" -ForegroundColor Red
}
}
$LaunchResults += [PSCustomObject]$result
$counter++
}
# Export ONLY the columns we want
$LaunchResults | Select-Object Server, Status, Details, TimeOfLaunch | Export-Csv $OutputFileName -NoTypeInformation
# Summary
$Duration = (Get-Date) - $StartTime
$SuccessCount = ($LaunchResults | Where-Object { $_.Status -eq "LAUNCHED" }).Count
$UnreachableCount = ($LaunchResults | Where-Object { $_.Status -eq "UNREACHABLE" }).Count
$FailCount = ($LaunchResults | Where-Object { $_.Status -in @("FAILED", "ERROR") }).Count
Write-Host "`n==================================================" -ForegroundColor DarkCyan
Write-Host " COMPLETED in $($Duration.TotalSeconds.ToString('F1')) seconds" -ForegroundColor DarkGreen
Write-Host " Success: $SuccessCount servers" -ForegroundColor Green
Write-Host " Unreachable: $UnreachableCount servers" -ForegroundColor Yellow
Write-Host " Failed: $FailCount servers" -ForegroundColor Red
Write-Host " Report: $OutputFileName" -ForegroundColor Cyan
Write-Host "==================================================" -ForegroundColor DarkCyan
How It Works
The main processing is done with this section of the code:
$proc = Start-Process -FilePath $PsExecPath `
-ArgumentList "\\$Server", "-accepteula", "-d", "-s", $Command `
-WindowStyle Hidden `
-PassThru `
-ErrorAction Stop
The critical flags are:
-d: Tells PsExec to detach and not wait for the remote process-s: Runs under SYSTEM context for maximum privileges-accepteula: Prevents the EULA popup from blocking execution-WindowStyle Hidden: Suppresses the console window-PassThru: Returns the process object so I can verify it started
When to Use This
This script is perfect for those emergency situations where:
- You've made a critical Group Policy change that needs immediate deployment
- Waiting 60-90 minutes for automatic refresh isn't acceptable
- You need to ensure consistency across your server farm quickly
- Traditional remote management tools aren't available due to security restrictions
Remember, in most cases, the automatic Group Policy refresh cycle is sufficient. But when you absolutely need that policy applied NOW across your entire infrastructure, this script delivers.
Prerequisites
- PsExec.exe from Microsoft Sysinternals
- Administrative rights on target servers
- Network connectivity to target servers (port 445/SMB)
- A text file listing your server names (one per line)
That's it! A simple but effective solution for those times when Group Policy's built-in refresh cycle just isn't fast enough.