Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Deploying GPUpdate to Hundreds of Servers in Under 60 Seconds


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 gpupdate to 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:

  1. Launch PsExec with the -d flag to avoid waiting
  2. Use PowerShell's Start-Process to fire off commands rapidly
  3. Track deployment status (not execution results) in a CSV
  4. 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.

Previous Post Next Post

نموذج الاتصال