Files
newt/updates/replace_windows.go
2026-10-28 09:42:56 -04:00

71 lines
2.2 KiB
Go

//go:build windows
package updates
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/fosrl/newt/logger"
)
// oldBinaryPrefix returns the prefix used for previous binaries parked
// during an update. Each update gets a unique suffix because a parked
// binary can stay locked for a long time (for example by a service host
// process that supervises the updated one), and a locked file cannot be
// renamed over.
func oldBinaryPrefix(exePath string) string {
return exePath + ".old"
}
// replaceBinary swaps the binary at exePath for the one at newPath.
//
// Windows refuses to overwrite (or delete) an executable that is mapped by a
// running process, so renaming newPath directly over exePath fails with
// "Access is denied". A running executable can however be renamed, so we
// first move it aside and then move the new binary into its place. The
// parked copy is removed by removeStaleBinary once nothing is running it.
func replaceBinary(newPath, exePath string) error {
oldPath := fmt.Sprintf("%s-%d", oldBinaryPrefix(exePath), time.Now().UnixNano())
if err := os.Rename(exePath, oldPath); err != nil {
return fmt.Errorf("failed to move running binary aside: %w", err)
}
if err := os.Rename(newPath, exePath); err != nil {
// Put the original binary back so the next start still works.
if rbErr := os.Rename(oldPath, exePath); rbErr != nil {
return fmt.Errorf("failed to install new binary: %w (restoring previous binary also failed: %v)", err, rbErr)
}
return fmt.Errorf("failed to install new binary: %w", err)
}
return nil
}
// removeStaleBinary deletes previous binaries left behind by replaceBinary.
// A parked binary can only be deleted once every process running it has
// exited, so this is best effort and anything still locked is retried on the
// next check.
func removeStaleBinary(exePath string) {
dir := filepath.Dir(exePath)
prefix := filepath.Base(oldBinaryPrefix(exePath))
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, e := range entries {
if e.IsDir() || !strings.HasPrefix(e.Name(), prefix) {
continue
}
oldPath := filepath.Join(dir, e.Name())
if err := os.Remove(oldPath); err == nil {
logger.Debug("checkAndSelfUpdate: removed previous binary %s", oldPath)
}
}
}