Renew the engine placed by hand to the proved build, and take a hand-over from the controller's terminal (hq issue 356)
The file on PATH was the engine a person copied at installation, and every delivered engine ran from the versions directory: the operator typed hand-over and a build from before it answered "unexpected argument". A proved engine now copies itself over that file when the bytes differ (never a link). And the hand-over of a directory used as found (issue 339) gets its channel: the controller's terminal asks this node's engine on mesh.node.<node>.ask.hand-over; the engine judges every value — the ask's size and shape, who asked, the path as it states it, a directory and no link, through no link — and records it or refuses and records nothing. Every text the engine addresses to the operator names the nox line (ADR 0272); the engine's own subcommands are install-time and rescue, and its usage says so.
This commit is contained in:
@@ -781,7 +781,7 @@ func ApplyMindingWindows(
|
||||
report.Outcomes = append(report.Outcomes, outcome)
|
||||
if outcome.asFound {
|
||||
// Never quiet (novox/hq issue 339): said on every apply, and stated unhealthy on every look until a
|
||||
// person hands it over (FoundDirectories, `mesh-host hand-over`).
|
||||
// person hands it over (FoundDirectories, `nox node hand-over`, novox/hq issue 356).
|
||||
log(fmt.Sprintf(" WARNING %s (%s): %s", outcome.ID, outcome.Target, outcome.Detail))
|
||||
}
|
||||
if handed {
|
||||
@@ -1047,7 +1047,7 @@ func applyDirectory(r *declaration.Directory, previous store.Applied, wasBefore
|
||||
}
|
||||
if existed && wasBefore {
|
||||
ours := previous.Target != "" && filepath.Clean(previous.Target) == filepath.Clean(r.Path) && !previous.AsFound
|
||||
// Or handed to the mesh by a person at the machine (`mesh-host hand-over`): then it is given the
|
||||
// Or handed to the mesh by the operator (`nox node hand-over`, or `hand-over` at the machine): then it is given the
|
||||
// declared owner and mode below, as any directory of the mesh's.
|
||||
if !ours && HandedOver()[handOverKey(r.ID, r.Path)] {
|
||||
ours = true
|
||||
@@ -1069,8 +1069,8 @@ func applyDirectory(r *declaration.Directory, previous store.Applied, wasBefore
|
||||
out.asFoundAt = previous.FoundAt
|
||||
}
|
||||
out.Detail = fmt.Sprintf("found here before the mesh and used as found: its owner %s and mode %o are "+
|
||||
"left, though %s were declared (novox/hq issue 339); `mesh-host hand-over %s` at the machine "+
|
||||
"hands it to the mesh", ownerText(before), before.Mode().Perm(), out.asFoundDeclared, r.Path)
|
||||
"left, though %s were declared (novox/hq issue 339); `nox node hand-over <node> %s` on the "+
|
||||
"control-node hands it to the mesh", ownerText(before), before.Mode().Perm(), out.asFoundDeclared, r.Path)
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
@@ -3417,7 +3417,7 @@ func removeFile(a store.Applied, now time.Time) (string, string, error) {
|
||||
"remove it by hand once you know what it is", aside), nil
|
||||
}
|
||||
|
||||
// HandedOver is every directory a person handed to the mesh at the machine (`mesh-host hand-over`), by
|
||||
// HandedOver is every directory the operator handed to the mesh (`nox node hand-over`; `hand-over` at the machine), by
|
||||
// handOverKey: a found directory there is the mesh's, given its declared owner and mode at the next apply
|
||||
// (novox/hq issue 339). Set by the engine to read the file beside its state; a test sets its own.
|
||||
var HandedOver = func() map[string]bool { return nil }
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A hand-over asked over the bus (novox/hq issue 356, issue 339).
|
||||
//
|
||||
// A directory used as found is handed to the mesh by its operator, at the controller's terminal:
|
||||
// `nox node hand-over <node> <path>` on the control-node (novox/hq ADR 0272). The controller asks this
|
||||
// node's engine on AskHandOverSubject — a request on core NATS, its own machine's subject, which only the
|
||||
// controller may publish — and the engine answers once, on the reply the request carried. The engine
|
||||
// judges every value it is sent and records the hand-over, or refuses and records nothing; the next
|
||||
// apply gives the directory its declared owner and mode. The controller holds the same two shapes in its
|
||||
// own link code, with a test on each side holding the field names.
|
||||
|
||||
// AskHandOverSubject is where the controller asks this node's engine to take a directory used as found as
|
||||
// the mesh's.
|
||||
func AskHandOverSubject(node string) string { return "mesh.node." + node + ".ask.hand-over" }
|
||||
|
||||
// HandOverAsk is one hand-over asked: the directory's absolute path as the engine states it, and who asked,
|
||||
// in the controller's words.
|
||||
type HandOverAsk struct {
|
||||
Path string `json:"path"`
|
||||
By string `json:"by"`
|
||||
}
|
||||
|
||||
// HandOverAnswer is the engine's answer: what it recorded, or why it refused — never both, never neither.
|
||||
type HandOverAnswer struct {
|
||||
Said string `json:"said,omitempty"`
|
||||
Refused string `json:"refused,omitempty"`
|
||||
}
|
||||
|
||||
// HandOverFunc judges and records one hand-over: the words to answer with, or the refusal.
|
||||
type HandOverFunc func(path, by string) (string, error)
|
||||
|
||||
// MaxHandOverAsk bounds a request: a path and a name are hundreds of bytes.
|
||||
const MaxHandOverAsk = 8 << 10
|
||||
|
||||
// MaxHandOverBy bounds who asked.
|
||||
const MaxHandOverBy = 200
|
||||
|
||||
// AnswerHandOver is the engine's judgement of one ask's bytes: every value is checked before anything is
|
||||
// recorded, and a refusal records nothing. The handler is given only what passed.
|
||||
func AnswerHandOver(handle HandOverFunc, body []byte) HandOverAnswer {
|
||||
if handle == nil {
|
||||
return HandOverAnswer{Refused: "this engine takes no hand-over"}
|
||||
}
|
||||
if len(body) > MaxHandOverAsk {
|
||||
return HandOverAnswer{Refused: fmt.Sprintf("the ask is %d bytes, more than a path and a name; nothing was handed over",
|
||||
len(body))}
|
||||
}
|
||||
var ask HandOverAsk
|
||||
if err := json.Unmarshal(body, &ask); err != nil {
|
||||
return HandOverAnswer{Refused: "the ask is not a hand-over: " + err.Error() + "; nothing was handed over"}
|
||||
}
|
||||
by := strings.TrimSpace(ask.By)
|
||||
switch {
|
||||
case by == "":
|
||||
return HandOverAnswer{Refused: "the ask names nobody as asking; nothing was handed over"}
|
||||
case len(by) > MaxHandOverBy || strings.ContainsAny(by, "\r\n\x00"):
|
||||
return HandOverAnswer{Refused: "who asked is not one short line; nothing was handed over"}
|
||||
case ask.Path == "":
|
||||
return HandOverAnswer{Refused: "the ask names no directory; nothing was handed over"}
|
||||
}
|
||||
said, err := handle(ask.Path, by)
|
||||
if err != nil {
|
||||
return HandOverAnswer{Refused: err.Error()}
|
||||
}
|
||||
return HandOverAnswer{Said: said}
|
||||
}
|
||||
|
||||
// HandOvers is a link that hears the controller asking this node to hand a directory over. Optional, as
|
||||
// Asked is.
|
||||
type HandOvers interface {
|
||||
HearHandOvers(HandOverFunc)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The hand-over's ask and answer hold these field names; the controller's side holds the same list
|
||||
// (mesh-controller internal/link, TestTheHandOverAskAndAnswerKeepTheirFieldNames).
|
||||
func TestTheHandOverAskAndAnswerKeepTheirFieldNames(t *testing.T) {
|
||||
body, _ := json.Marshal(HandOverAsk{Path: "/srv/notes", By: "jo through mesh-cli on anchor"})
|
||||
if got := keysOf(t, body); got != "by path" {
|
||||
t.Fatalf("the ask's fields are %q", got)
|
||||
}
|
||||
body, _ = json.Marshal(HandOverAnswer{Said: "s", Refused: "r"})
|
||||
if got := keysOf(t, body); got != "refused said" {
|
||||
t.Fatalf("the answer's fields are %q", got)
|
||||
}
|
||||
if AskHandOverSubject("laptop") != "mesh.node.laptop.ask.hand-over" {
|
||||
t.Fatalf("the subject is %q", AskHandOverSubject("laptop"))
|
||||
}
|
||||
}
|
||||
|
||||
func keysOf(t *testing.T, body []byte) string {
|
||||
t.Helper()
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(body, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return strings.Join(keys, " ")
|
||||
}
|
||||
|
||||
// Every value of an ask is judged before the handler sees it, and a refusal hands nothing to the handler:
|
||||
// an ask too large, not a hand-over, naming nobody, naming somebody over more than a line, or naming no
|
||||
// directory. What passes reaches the handler with its path as given and who asked trimmed.
|
||||
func TestAHandOverAskIsJudgedBeforeAnythingIsRecorded(t *testing.T) {
|
||||
var given []string
|
||||
handle := func(path, by string) (string, error) {
|
||||
given = append(given, path+" by "+by)
|
||||
if path == "/refused" {
|
||||
return "", errors.New("not a directory this machine uses as found")
|
||||
}
|
||||
return path + " is handed to the mesh", nil
|
||||
}
|
||||
refusals := map[string][]byte{
|
||||
"too large": []byte(`{"path":"/` + strings.Repeat("a", MaxHandOverAsk) + `","by":"jo"}`),
|
||||
"not a hand-over": []byte(`not json`),
|
||||
"nobody": []byte(`{"path":"/srv/notes","by":" "}`),
|
||||
"two lines": []byte(`{"path":"/srv/notes","by":"jo\nroot"}`),
|
||||
"a name too long": []byte(`{"path":"/srv/notes","by":"` + strings.Repeat("x", MaxHandOverBy+1) + `"}`),
|
||||
"no directory": []byte(`{"path":"","by":"jo"}`),
|
||||
"a NUL in the name": []byte(`{"path":"/srv/notes","by":"jo\u0000"}`),
|
||||
}
|
||||
for name, body := range refusals {
|
||||
a := AnswerHandOver(handle, body)
|
||||
if a.Refused == "" || a.Said != "" {
|
||||
t.Fatalf("%s was answered %+v; want a refusal", name, a)
|
||||
}
|
||||
if !strings.Contains(a.Refused, "nothing was handed over") {
|
||||
t.Fatalf("%s's refusal does not say nothing was handed over: %q", name, a.Refused)
|
||||
}
|
||||
}
|
||||
if len(given) != 0 {
|
||||
t.Fatalf("the handler was given %v although every ask was refused", given)
|
||||
}
|
||||
if a := AnswerHandOver(nil, []byte(`{"path":"/srv/notes","by":"jo"}`)); a.Refused == "" {
|
||||
t.Fatal("an engine with no handler took a hand-over")
|
||||
}
|
||||
|
||||
a := AnswerHandOver(handle, []byte(`{"path":"/refused","by":"jo"}`))
|
||||
if a.Refused != "not a directory this machine uses as found" || a.Said != "" {
|
||||
t.Fatalf("the handler's refusal was answered as %+v", a)
|
||||
}
|
||||
a = AnswerHandOver(handle, []byte(`{"path":"/srv/notes","by":" jo through mesh-cli on anchor "}`))
|
||||
if a.Said != "/srv/notes is handed to the mesh" || a.Refused != "" {
|
||||
t.Fatalf("answered %+v", a)
|
||||
}
|
||||
if strings.Join(given, "; ") != "/refused by jo; /srv/notes by jo through mesh-cli on anchor" {
|
||||
t.Fatalf("the handler was given %v", given)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -60,6 +61,7 @@ type natsLink struct {
|
||||
sub *nats.Subscription
|
||||
asking *nats.Subscription
|
||||
asked chan struct{}
|
||||
handing *nats.Subscription
|
||||
node string
|
||||
arrived chan Declaration
|
||||
lost chan error
|
||||
@@ -192,6 +194,31 @@ func (l *natsLink) hearAsks() {
|
||||
}
|
||||
}
|
||||
|
||||
// HearHandOvers listens for the controller asking this node to hand a directory used as found to the mesh
|
||||
// (novox/hq issue 356): a request on this machine's own subject, answered once on the reply it carried.
|
||||
// The engine judges the ask (AnswerHandOver) and runs nothing it is sent. A bus whose user list is older
|
||||
// than the grant refuses the subscription asynchronously, said by the client; the operator's line then
|
||||
// says that nothing on this node answers.
|
||||
func (l *natsLink) HearHandOvers(handle HandOverFunc) {
|
||||
if handle == nil {
|
||||
return
|
||||
}
|
||||
handing, err := l.conn.Subscribe(AskHandOverSubject(l.node), func(msg *nats.Msg) {
|
||||
if msg.Reply == "" {
|
||||
// Nobody to answer: nothing is handed over on a word nobody waits for.
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(AnswerHandOver(handle, msg.Data))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = msg.Respond(body)
|
||||
})
|
||||
if err == nil {
|
||||
l.handing = handing
|
||||
}
|
||||
}
|
||||
|
||||
func (l *natsLink) Declarations() <-chan Declaration { return l.arrived }
|
||||
func (l *natsLink) AskedToReport() <-chan struct{} { return l.asked }
|
||||
func (l *natsLink) Lost() <-chan error { return l.lost }
|
||||
@@ -203,6 +230,9 @@ func (l *natsLink) Close() {
|
||||
if l.asking != nil {
|
||||
_ = l.asking.Unsubscribe()
|
||||
}
|
||||
if l.handing != nil {
|
||||
_ = l.handing.Unsubscribe()
|
||||
}
|
||||
if l.conn != nil {
|
||||
l.conn.Close()
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ const KindAccount = "account"
|
||||
|
||||
// KindDirectory is a directory a module declares that the engine uses as found (novox/hq issue 339): there before
|
||||
// the mesh, with another owner or mode than declared, and left so. Said unhealthy on every look, its reason
|
||||
// starting ReasonUsedAsFound, until a person hands it to the mesh at the machine (`mesh-host hand-over`).
|
||||
// starting ReasonUsedAsFound, until the operator hands it to the mesh (`nox node hand-over <node> <path>` on the control-node, novox/hq issue 356).
|
||||
const KindDirectory = "directory"
|
||||
|
||||
// ReasonUsedAsFound starts the reason of a directory used as found.
|
||||
|
||||
@@ -51,8 +51,11 @@ type Queue struct {
|
||||
// Numbers keeps the report sequence and the refusals counted, across restarts and self-updates.
|
||||
// Nil counts in memory, which is only right where nothing outlives the process — a test.
|
||||
Numbers Numbers
|
||||
Say Announce
|
||||
Timeout time.Duration
|
||||
// HandOver judges and records a directory used as found handed to the mesh by the operator, asked by
|
||||
// the controller over the link (novox/hq issue 356). Nil answers every such ask with a refusal.
|
||||
HandOver HandOverFunc
|
||||
Say Announce
|
||||
Timeout time.Duration
|
||||
|
||||
once sync.Once
|
||||
wake chan struct{}
|
||||
|
||||
@@ -239,6 +239,11 @@ func serve(ctx context.Context, link Link, m Membership, queue *Queue, say Annou
|
||||
if a, ok := link.(Asked); ok {
|
||||
asks = a.AskedToReport()
|
||||
}
|
||||
// And the controller asking this node to hand a directory used as found to the mesh (novox/hq issue
|
||||
// 356), answered on the link the question came over.
|
||||
if h, ok := link.(HandOvers); ok {
|
||||
h.HearHandOvers(queue.HandOver)
|
||||
}
|
||||
|
||||
for {
|
||||
// Asked to stop — by standing aside, say — nothing further is taken in.
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The engine placed by hand becomes a copy of the proved one when the two differ (novox/hq issue 356): the
|
||||
// command on PATH is the build that runs the machine.
|
||||
func TestTheFallbackBecomesACopyOfTheProvedEngine(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
self := filepath.Join(dir, VersionsDirName, "v2", BinaryName)
|
||||
if err := os.MkdirAll(filepath.Dir(self), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(self, []byte("the proved engine"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fallback := filepath.Join(dir, "bin", BinaryName)
|
||||
if err := os.MkdirAll(filepath.Dir(fallback), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(fallback, []byte("the stale engine placed by hand"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
renewed, err := RenewFallback(self, fallback)
|
||||
if err != nil || !renewed {
|
||||
t.Fatalf("renewed %v, %v", renewed, err)
|
||||
}
|
||||
got, err := os.ReadFile(fallback)
|
||||
if err != nil || string(got) != "the proved engine" {
|
||||
t.Fatalf("the fallback holds %q, %v", got, err)
|
||||
}
|
||||
info, err := os.Stat(fallback)
|
||||
if err != nil || info.Mode().Perm() != 0o755 {
|
||||
t.Fatalf("the fallback's mode is %v, %v; want 0755 so it can be run", info.Mode(), err)
|
||||
}
|
||||
if _, err := os.Stat(self); err != nil {
|
||||
t.Fatalf("the running engine was touched: %v", err)
|
||||
}
|
||||
|
||||
// Already a copy: left alone, so a proof every start does not rewrite it every start.
|
||||
before, _ := os.Stat(fallback)
|
||||
renewed, err = RenewFallback(self, fallback)
|
||||
if err != nil || renewed {
|
||||
t.Fatalf("a fallback that is already a copy was renewed again: %v, %v", renewed, err)
|
||||
}
|
||||
after, _ := os.Stat(fallback)
|
||||
if !os.SameFile(before, after) {
|
||||
t.Fatal("a fallback that is already a copy was replaced")
|
||||
}
|
||||
if left, _ := filepath.Glob(filepath.Join(filepath.Dir(fallback), "."+BinaryName+"-*")); len(left) > 0 {
|
||||
t.Fatalf("temporary files left beside the fallback: %v", left)
|
||||
}
|
||||
}
|
||||
|
||||
// An engine running from the fallback itself — nothing delivered yet — renews nothing: it would copy a file
|
||||
// over itself.
|
||||
func TestAnEngineRunningFromTheFallbackRenewsNothing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fallback := filepath.Join(dir, BinaryName)
|
||||
if err := os.WriteFile(fallback, []byte("placed by hand and running"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
renewed, err := RenewFallback(fallback, fallback)
|
||||
if err != nil || renewed {
|
||||
t.Fatalf("renewed %v, %v", renewed, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing placed by hand: the proved engine is placed, so the launcher has a fallback and the machine a command.
|
||||
func TestAMissingFallbackIsPlaced(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
self := filepath.Join(dir, "v1", BinaryName)
|
||||
if err := os.MkdirAll(filepath.Dir(self), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(self, []byte("proved"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fallback := filepath.Join(dir, BinaryName)
|
||||
renewed, err := RenewFallback(self, fallback)
|
||||
if err != nil || !renewed {
|
||||
t.Fatalf("renewed %v, %v", renewed, err)
|
||||
}
|
||||
if got, _ := os.ReadFile(fallback); string(got) != "proved" {
|
||||
t.Fatalf("placed %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A link at the fallback's path is refused and left as it is: the mesh creates no link and writes through none
|
||||
// (novox/hq ADR 0012). The link's target is untouched too.
|
||||
func TestALinkAtTheFallbackIsRefusedAndLeft(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
self := filepath.Join(dir, "v1", BinaryName)
|
||||
if err := os.MkdirAll(filepath.Dir(self), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(self, []byte("proved"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := filepath.Join(dir, "somebody-elses")
|
||||
if err := os.WriteFile(target, []byte("theirs"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fallback := filepath.Join(dir, BinaryName)
|
||||
if err := os.Symlink(target, fallback); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
renewed, err := RenewFallback(self, fallback)
|
||||
if err == nil || renewed || !strings.Contains(err.Error(), "link") {
|
||||
t.Fatalf("a link was taken: %v, %v", renewed, err)
|
||||
}
|
||||
if got, _ := os.ReadFile(target); string(got) != "theirs" {
|
||||
t.Fatalf("the link's target was written: %q", got)
|
||||
}
|
||||
if info, err := os.Lstat(fallback); err != nil || info.Mode()&os.ModeSymlink == 0 {
|
||||
t.Fatalf("the link was replaced: %v %v", info, err)
|
||||
}
|
||||
}
|
||||
|
||||
// A running engine that cannot be read is said, not copied as nothing.
|
||||
func TestAnUnreadableRunningEngineIsSaid(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fallback := filepath.Join(dir, BinaryName)
|
||||
if err := os.WriteFile(fallback, []byte("placed"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
renewed, err := RenewFallback(filepath.Join(dir, "gone"), fallback)
|
||||
if err == nil || renewed {
|
||||
t.Fatalf("renewed %v, %v", renewed, err)
|
||||
}
|
||||
if got, _ := os.ReadFile(fallback); string(got) != "placed" {
|
||||
t.Fatalf("the fallback was written from nothing: %q", got)
|
||||
}
|
||||
if _, err := RenewFallback("", fallback); err == nil {
|
||||
t.Fatal("an empty path was taken")
|
||||
}
|
||||
}
|
||||
|
||||
// The fallback's path is the launcher's: its environment variable, or the default.
|
||||
func TestTheFallbackPathIsTheLaunchers(t *testing.T) {
|
||||
t.Setenv(FallbackVar, "")
|
||||
if FallbackPath() != DefaultFallback {
|
||||
t.Fatalf("the default is %q", FallbackPath())
|
||||
}
|
||||
t.Setenv(FallbackVar, "/elsewhere/nox-mesh-host")
|
||||
if FallbackPath() != "/elsewhere/nox-mesh-host" {
|
||||
t.Fatalf("the override is %q", FallbackPath())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user