//go:build windows package updater import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "syscall" ) // swapScript waits for ipswap to exit, replaces the binary, restarts it and // deletes itself. // // A running .exe on Windows is locked against overwriting, so the swap cannot // be done by the process being replaced — it has to outlive it. A tiny batch // file is the least machinery that does this without shipping a second binary. // // %1 is the PID to wait for, %2 the freshly downloaded exe, %3 the exe to // replace. The tasklist/find pair is the standard "is this PID still alive" // idiom; `timeout` rather than `ping -n` keeps it readable. const swapScript = `@echo off setlocal set PID=%~1 set NEWEXE=%~2 set TARGET=%~3 rem Give up after ~30s rather than spinning forever if the old process hangs. set /a TRIES=0 :wait tasklist /FI "PID eq %PID%" 2>nul | find "%PID%" >nul if errorlevel 1 goto swap set /a TRIES+=1 if %TRIES% GEQ 30 goto giveup timeout /t 1 /nobreak >nul goto wait :swap copy /y "%NEWEXE%" "%TARGET%" >nul if errorlevel 1 goto giveup del /q "%NEWEXE%" >nul 2>&1 start "" "%TARGET%" goto done :giveup rem Leave the download in place so the user can swap it in by hand. exit /b 1 :done endlocal rem Delete this script last; cmd tolerates a batch file removing itself. del /q "%~f0" >nul 2>&1 ` // InstallAndRestart writes the swap helper, launches it detached, and returns. // The caller must exit promptly afterwards — the helper is already waiting on // this process to go away. // // This is only ever reached from an explicit click on "Update available": the // updater never installs on its own. func InstallAndRestart(downloadedExe string) error { target, err := os.Executable() if err != nil { return fmt.Errorf("locating the running executable: %w", err) } target, err = filepath.Abs(target) if err != nil { return err } script := filepath.Join(os.TempDir(), "ipswap-update.cmd") if err := os.WriteFile(script, []byte(swapScript), 0o755); err != nil { return fmt.Errorf("writing the update helper: %w", err) } cmd := exec.Command("cmd.exe", "/c", script, strconv.Itoa(os.Getpid()), downloadedExe, target) // Detach: the helper has to survive this process exiting, and it must not // flash a console window while it waits. cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: true, CreationFlags: windowsCreateNoWindow | windowsDetachedProcess, } if err := cmd.Start(); err != nil { return fmt.Errorf("starting the update helper: %w", err) } return cmd.Process.Release() } const ( windowsDetachedProcess = 0x00000008 windowsCreateNoWindow = 0x08000000 )