package upgrade import ( "bytes" "crypto/sha256" "errors" "fmt" "io" "os" "path/filepath" ) // The engine placed by hand, kept current (novox/hq issue 356). // // The first engine on a machine is copied there by a person (novox/hq ADR 0141): the launcher runs it // while nothing is delivered, and it is the one `nox-mesh-host` on any PATH. Every engine after it is // delivered under VersionsDirName and run from there, so the file on PATH fell behind: on 2026-10-09 the // operator typed its `hand-over` and was answered "unexpected argument" by a build months older than the // one running the machine. A command that answers for a build that is not running is the lie this // repository exists to refuse. // // So a proved engine (Proved) writes itself over the one placed by hand when the two differ: a plain // copy, renamed into place — never a link, which the mesh creates none of (novox/hq ADR 0012), and never // a write through one. The launcher's fallback is then the newest proved build, which is also the best // one to fall back on. // DefaultFallback is where the engine placed by hand lives: the launcher's fallback, and the command. const DefaultFallback = "/usr/bin/nox-mesh-host" // FallbackVar is the environment the launcher reads the fallback's path from; read here too, so the two // never name different files. const FallbackVar = "MESH_HOST_BIN" // FallbackPath is the engine placed by hand: FallbackVar, or DefaultFallback. func FallbackPath() string { if p := os.Getenv(FallbackVar); p != "" { return p } return DefaultFallback } // RenewFallback makes the file at fallback a copy of the executable at self, when it is not one already. // // Nothing is written when the two are the same file (an engine running from the fallback itself, while // nothing is delivered) or hold the same bytes. A fallback that is a link is refused: the mesh writes // through no link, and a link there would be somebody else's arrangement. Reports whether it was renewed. func RenewFallback(self, fallback string) (bool, error) { if self == "" || fallback == "" { return false, errors.New("the running engine or the fallback has no path") } mine, err := os.Stat(self) if err != nil { return false, fmt.Errorf("cannot read the running engine at %s: %w", self, err) } if !mine.Mode().IsRegular() { return false, fmt.Errorf("the running engine at %s is not a regular file", self) } placed, err := os.Lstat(fallback) switch { case errors.Is(err, os.ErrNotExist): // Nothing placed by hand: the launcher would have nothing to fall back on. Placed now. case err != nil: return false, fmt.Errorf("cannot read the engine placed by hand at %s: %w", fallback, err) case placed.Mode()&os.ModeSymlink != 0: return false, fmt.Errorf("%s is a link, and the mesh writes through none (novox/hq ADR 0012): "+ "it is left as it is", fallback) case !placed.Mode().IsRegular(): return false, fmt.Errorf("%s is not a regular file: it is left as it is", fallback) case os.SameFile(mine, placed): return false, nil default: same, err := sameBytes(self, fallback) if err != nil { return false, err } if same { return false, nil } } src, err := os.Open(self) if err != nil { return false, err } defer src.Close() tmp, err := os.CreateTemp(filepath.Dir(fallback), "."+filepath.Base(fallback)+"-*") if err != nil { return false, fmt.Errorf("cannot write beside %s: %w", fallback, err) } defer os.Remove(tmp.Name()) if _, err := io.Copy(tmp, src); err != nil { tmp.Close() return false, err } if err := tmp.Sync(); err != nil { tmp.Close() return false, err } if err := tmp.Close(); err != nil { return false, err } if err := os.Chmod(tmp.Name(), 0o755); err != nil { return false, err } // Renamed, never written in place: the kernel refuses to truncate an executable that runs, and a // rename leaves whatever runs from the old file untouched. if err := os.Rename(tmp.Name(), fallback); err != nil { return false, err } return true, nil } // sameBytes says whether two files hold the same bytes, by digest. func sameBytes(a, b string) (bool, error) { da, err := digestOf(a) if err != nil { return false, err } db, err := digestOf(b) if err != nil { return false, err } return bytes.Equal(da, db), nil } func digestOf(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } return h.Sum(nil), nil }