//go:build windows package version import ( "errors" "fmt" "os" "time" "golang.org/x/sys/windows" ) const updateMoveFlags = windows.MOVEFILE_REPLACE_EXISTING | windows.MOVEFILE_WRITE_THROUGH func moveUpdateFileReplacing(sourcePath, targetPath string) error { source, err := windows.UTF16PtrFromString(sourcePath) if err != nil { return err } target, err := windows.UTF16PtrFromString(targetPath) if err != nil { return err } // NTFS may still deny replacing an open destination despite delete // sharing. Journal polling and antivirus readers are short-lived; retain // both paths and retry only sharing/access conflicts for a bounded window. deadline := time.Now().Add(time.Second) for { err := windows.MoveFileEx(source, target, updateMoveFlags) if err == nil || time.Now().After(deadline) || (!errors.Is(err, windows.ERROR_SHARING_VIOLATION) && !errors.Is(err, windows.ERROR_LOCK_VIOLATION) && !errors.Is(err, windows.ERROR_ACCESS_DENIED)) { return err } time.Sleep(20 * time.Millisecond) } } // Windows cannot rename over an existing executable with os.Rename. Preserve // a durable backup first, then ask the kernel to replace the existing target // in one write-through operation so the public executable path never vanishes. func replaceUpdateFile(targetPath, replacementPath, backupPath string) error { if err := copyFileDurable(targetPath, backupPath, 0o755); err != nil { return fmt.Errorf("create durable update backup: %w", err) } if err := moveUpdateFileReplacing(replacementPath, targetPath); err != nil { _ = os.Remove(backupPath) return fmt.Errorf("atomically replace update target: %w", err) } return nil } func restoreUpdateFile(targetPath, backupPath string) error { if err := moveUpdateFileReplacing(backupPath, targetPath); err != nil { return fmt.Errorf("atomically restore update backup: %w", err) } return nil }