mehdix
mehdix

Reputation: 5164

How to automate synchronizing Windows 10 guest's time with the Linux host?

On Arch Linux I have a Windows 10 Guest on top of libvirt, kvm and virsh (still having some trouble to connect all these dots mentally together). Every time I suspend the laptop and a day is gone the Windows 10 host goes out of sync. I learned that with the following command I can force a time sync in the host:

➜  ~ virsh qemu-agent-command win10 '{"execute":"guest-set-time"}'
{"return":{}}

In order to make this work I modifed the clock XML block and added a kvm clock entry. This is how the block looks like now:

<clock offset="localtime">
  <timer name="tsc" tickpolicy="delay"/>
  <timer name="kvmclock"/>
  <timer name="rtc" tickpolicy="delay" track="wall"/>
  <timer name="pit" tickpolicy="delay"/>
  <timer name="hpet" present="yes"/>
</clock>

I would like to know whether I can automate this step or trigger an update everytime I wake up the machine or log-in.

Thanks in advance

Upvotes: 2

Views: 1853

Answers (1)

FSCKur
FSCKur

Reputation: 1070

I was not able to get anywhere specifically using virsh. Here is how I fixed this issue in a Windows 11 guest on MacOS in UTM 3.6.4 and 4.1.5.


At first I tried many workarounds using w32tm - but this was always flaky.


This helped slightly:

  • disable "use local time for base clock" (otherwise you can't add a manual -rtc argument if using UTM)
  • add -rtc base=localtime,driftfix=slew

This wasn't great, because it won't recover a significant delta.


This is the solution I settled on (run in the Windows guest). It creates a scheduled task that runs every 5 minutes, gets the time from NTP, converts it to local time, measures the drift, and if the drift is >30 seconds in either direction it updates the system clock.

function Get-NtpTime
{
    [OutputType([datetime])]
    [CmdletBinding()]
    param
    (
        [string]$Server = "time.nist.gov",

        [int]$Port = 13
    )

    if (-not $PSBoundParameters.ContainsKey('ErrorAction'))
    {
        $ErrorActionPreference = 'Stop'
    }

    $Client = [Net.Sockets.TcpClient]::new($Server, $Port)
    $Reader = [IO.StreamReader]::new($Client.GetStream())
    try
    {
        $Response  = $Reader.ReadToEnd()
        $UtcString = $Response.Substring(7, 17)
        $LocalTime = [datetime]::ParseExact(
            $UtcString,
            "yy-MM-dd HH:mm:ss",
            [cultureinfo]::InvariantCulture,
            [Globalization.DateTimeStyles]::AssumeUniversal
        )
    }
    finally
    {
        $Reader.Dispose()
        $Client.Dispose()
    }
    $LocalTime
}

function Register-TimeSync
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [timespan]$RepetitionInterval = (New-TimeSpan -Minutes 5),

        [Parameter()]
        [timespan]$ExecutionTimeLimit = (New-TimeSpan -Minutes 3)
    )

    $Invocation = {
        $NtpTime = Get-NtpTime
        $Delta   = [datetime]::Now - $NtpTime
        if ([Math]::Abs($Delta.TotalSeconds) -gt 30)
        {
            Set-Date $NtpTime
        }
    }

    $PSName     = if ($PSVersionTable.PSVersion.Major -le 5) {'powershell'} else {'pwsh'}
    $Path       = (Get-Command $PSName).Source
    $Command    = Get-Command Get-NtpTime
    $Definition = "function Get-NtpTime`n{$($Command.Definition)}"
    $Invocation = $Definition, $Invocation -join "`n"
    $Bytes      = [Text.Encoding]::Unicode.GetBytes($Invocation)
    $Encoded    = [Convert]::ToBase64String($Bytes)

    $TriggerParams = @{
        Once               = $true
        At                 = [datetime]::Today
        RepetitionInterval = $RepetitionInterval
    }
    $Trigger   = New-ScheduledTaskTrigger @TriggerParams
    $Action    = New-ScheduledTaskAction -Execute $Path -Argument "-NoProfile -EncodedCommand $Encoded"
    $Settings  = New-ScheduledTaskSettingsSet -ExecutionTimeLimit $ExecutionTimeLimit -MultipleInstances IgnoreNew
    $Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest

    $RegisterParams = @{
        TaskName  = "Update system time from NTP"
        Trigger   = $Trigger
        Action    = $Action
        Settings  = $Settings
        Principal = $Principal
        Force     = $true
    }

    Register-ScheduledTask @RegisterParams
}

Usage (run as admin):

Register-TimeSync

Upvotes: 0

Related Questions