Scaffold ipswap: tray-driven Windows IP preset switcher
Implements the design in claude.md as a building skeleton: pure Go, no cgo, cross-compiles to a single Windows .exe from Linux. Architecture follows the spec's deliberate split. The fast path is native — tray icon, grouped submenus, a Win32 MessageBox showing a live before/after diff, then netsh. The slow path is an embedded web editor served on a random loopback port and opened in the default browser. Two decisions worth recording: Reads use GetAdaptersAddresses, writes use netsh. The spec left the enumeration mechanism open; parsing `netsh show config` breaks on a non-English Windows because the output is localised. DNS static-vs-DHCP origin is not exposed by that API, so it comes from one registry read. The netsh command plan is built in portable code. That puts the delete-every- existing-address step — the one that stops secondary addresses leaking across switches — under test without needing a Windows box. The editor requires the session token in a header for mutations, not just the cookie: any page in the browser can make it POST to 127.0.0.1 with the cookie attached, but it cannot set a header. The updater refuses to install a release that publishes no SHA256. Not yet done: no group picker for export (the API supports it), no single-instance guard, and internal/server/web/app.css is reconstructed from the description in claude.md rather than the canonical apointless.css. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
//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
|
||||
)
|
||||
Reference in New Issue
Block a user