Public/ntp/Sync-NTPTime.ps1

#Requires -Version 5.1

function Sync-NTPTime {
    <#
    .SYNOPSIS
        Forces NTP time resynchronization on Windows machines
 
    .DESCRIPTION
        Forces a resynchronization of the Windows Time Service (w32tm) on one or more
        local or remote machines. Optionally restarts the Windows Time service before
        resyncing to ensure a clean state. Parses the output of w32tm /resync to report
        success or failure per machine, supporting both English and French OS locales.
 
        Uses Invoke-Command for uniform local and remote execution, enabling consistent
        behavior and straightforward mocking in tests. Each machine is processed
        independently with per-machine error isolation -- a failure on one target does
        not prevent processing of subsequent targets.
 
        Supports -WhatIf and -Confirm via SupportsShouldProcess. Since ConfirmImpact is
        Medium and the default ConfirmPreference is High, the function does not prompt by
        default. Pass -Confirm to force a prompt, or -Confirm:$false to suppress it
        explicitly in automation contexts.
 
    .PARAMETER ComputerName
        One or more computer names to resynchronize. Accepts pipeline input.
        Defaults to the local machine ($env:COMPUTERNAME). Each value must be
        a non-empty string.
 
    .PARAMETER RestartService
        When specified, restarts the Windows Time service (w32time) on each target
        machine before running w32tm /resync. This can help recover from a stale
        service state. The restart action also goes through ShouldProcess confirmation.
 
    .EXAMPLE
        Sync-NTPTime
 
        Forces an NTP resync on the local machine using default parameters.
 
    .EXAMPLE
        Sync-NTPTime -ComputerName 'SRV-DC01'
 
        Forces an NTP resync on a single remote machine.
 
    .EXAMPLE
        Sync-NTPTime -ComputerName 'SRV-DC01', 'SRV-DC02' -RestartService -Verbose
 
        Restarts the w32time service then forces an NTP resync on two remote machines,
        with verbose logging enabled.
 
    .EXAMPLE
        Get-Content -Path 'C:\\Admin\\servers.txt' | Sync-NTPTime -RestartService
 
        Reads a list of server names from a file and pipelines them into Sync-NTPTime,
        restarting the w32time service on each before resyncing.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-03-12
        Requires: PowerShell 5.1+ / Windows only
        Permissions: Requires admin rights (local and remote) to restart services
                     and run w32tm /resync. Remote targets require PSRemoting enabled.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [switch]$RestartService
    )

    begin {
        Set-StrictMode -Version Latest
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting - PowerShell $($PSVersionTable.PSVersion)"

        # Scriptblock: force NTP resynchronization via w32tm
        $resyncScriptBlock = {
            $w32tmPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\\w32tm.exe'
            if (-not (Test-Path -Path $w32tmPath)) {
                throw "[ERROR] w32tm.exe not found at '$w32tmPath'"
            }
            $rawOutput = & w32tm.exe /resync 2>&1

            [PSCustomObject]@{
                Output   = ($rawOutput | Out-String).Trim()
                ExitCode = $LASTEXITCODE
            }
        }

        # Scriptblock: restart the Windows Time service
        $restartScriptBlock = {
            Restart-Service -Name 'w32time' -Force -ErrorAction Stop
            Start-Sleep -Seconds 2
        }

        # Locale-agnostic success pattern (EN + FR)
        $successPattern = '(The command completed successfully|Sending resync command to local computer|La commande s.est correctement|Envoi de la commande de resynchronisation)'
    }

    process {
        foreach ($targetComputer in $ComputerName) {
            try {
                Write-Verbose "[$($MyInvocation.MyCommand)] Processing: $targetComputer"
                $isLocal = ($targetComputer -eq $env:COMPUTERNAME)
                $serviceRestarted = $false

                # Optionally restart w32time service before resync
                if ($RestartService) {
                    if ($PSCmdlet.ShouldProcess($targetComputer, 'Restart Windows Time service (w32time)')) {
                        Write-Verbose "[$($MyInvocation.MyCommand)] Restarting w32time on '$targetComputer'..."
                        if ($isLocal) {
                            $null = Invoke-Command -ScriptBlock $restartScriptBlock
                        } else {
                            $null = Invoke-Command -ComputerName $targetComputer -ScriptBlock $restartScriptBlock
                        }
                        $serviceRestarted = $true
                        Write-Verbose "[$($MyInvocation.MyCommand)] w32time restarted on '$targetComputer'"
                    }
                }

                # Force NTP resynchronization
                if ($PSCmdlet.ShouldProcess($targetComputer, 'Force NTP resynchronization')) {
                    Write-Verbose "[$($MyInvocation.MyCommand)] Running w32tm /resync on '$targetComputer'..."
                    if ($isLocal) {
                        $resyncResult = Invoke-Command -ScriptBlock $resyncScriptBlock
                    } else {
                        $resyncResult = Invoke-Command -ComputerName $targetComputer -ScriptBlock $resyncScriptBlock
                    }

                    $outputText = [string]$resyncResult.Output
                    $exitCode = [int]$resyncResult.ExitCode
                    $isSuccess = ($exitCode -eq 0) -and ($outputText -match $successPattern)

                    if ($isSuccess) {
                        Write-Verbose "[$($MyInvocation.MyCommand)] [OK] Resync succeeded on '$targetComputer'"
                    } else {
                        Write-Warning "[$($MyInvocation.MyCommand)] Resync may have failed on '$targetComputer': $outputText"
                    }

                    [PSCustomObject]@{
                        PSTypeName       = 'PSWinOps.NtpResyncResult'
                        ComputerName     = $targetComputer
                        Success          = $isSuccess
                        ServiceRestarted = $serviceRestarted
                        Message          = $outputText
                        Timestamp        = Get-Date -Format 'o'
                    }
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed to sync NTP on '$targetComputer': $_"
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed"
    }
}