Walk for setuid programs in the engine and carry an interrupted walk on (hq issue 361)
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/361-the-setuid-search-runs-to-completion ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/361-the-setuid-search-runs-to-completion ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
The control-node's walk takes far longer than the engine runs between updates, so a search that starts over at every restart never completes. The walk is the engine's own, in lexical order, so its progress is one cursor per filesystem, kept every minute beside the engine's state and carried on after a restart while younger than rootsearch.FreshFor. It runs on a thread of its own at nice 19 and the idle I/O class.
This commit is contained in:
@@ -2,6 +2,7 @@ package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -34,7 +35,7 @@ func newGate() *gate {
|
||||
return &gate{release: make(chan struct{}, 16), started: make(chan context.Context, 16)}
|
||||
}
|
||||
|
||||
func (g *gate) search(ctx context.Context, _ []string) ([]string, error) {
|
||||
func (g *gate) search(ctx context.Context, p *SetuidProgress, _ func()) ([]string, error) {
|
||||
g.started <- ctx
|
||||
<-g.release
|
||||
return g.found, g.err
|
||||
@@ -290,3 +291,103 @@ func TestTheIdleRunnerLowersItsCommand(t *testing.T) {
|
||||
t.Fatalf("its failure is worded as CLocale's: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func fromJSON(t *testing.T, raw []byte, v any) {
|
||||
t.Helper()
|
||||
if err := json.Unmarshal(raw, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// interrupted is a search that walks to a cursor, keeps it, and never returns: the engine stopped there.
|
||||
func interrupted(seen chan<- *SetuidProgress) SetuidSearch {
|
||||
return func(ctx context.Context, p *SetuidProgress, checkpoint func()) ([]string, error) {
|
||||
p.Found = append(p.Found, "/opt/a")
|
||||
p.Cursor["/"] = "/opt/a"
|
||||
checkpoint()
|
||||
seen <- p
|
||||
select {} // the engine stops here
|
||||
}
|
||||
}
|
||||
|
||||
func TestASearchInterruptedByARestartCarriesOnAndIsDatedByItsStart(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
paths := func() *SetuidCache {
|
||||
return &SetuidCache{Path: filepath.Join(dir, SetuidSearchFile), ProgressPath: filepath.Join(dir, SetuidProgressFile)}
|
||||
}
|
||||
seen := make(chan *SetuidProgress, 1)
|
||||
first := paths()
|
||||
first.Look(at, root, interrupted(seen))
|
||||
<-seen
|
||||
// The engine restarts two hours later.
|
||||
restarted := paths()
|
||||
var resumedWith SetuidProgress
|
||||
done := make(chan struct{})
|
||||
_, _, err := restarted.Look(at.Add(2*time.Hour), root, func(ctx context.Context, p *SetuidProgress, _ func()) ([]string, error) {
|
||||
resumedWith = *p
|
||||
close(done)
|
||||
return p.Found, nil
|
||||
})
|
||||
notJudged(t, err, true)
|
||||
<-done
|
||||
restarted.Wait()
|
||||
if !resumedWith.Started.Equal(at) || resumedWith.Cursor["/"] != "/opt/a" || len(resumedWith.Found) != 1 {
|
||||
t.Fatalf("the restarted search did not carry on the walk: %+v", resumedWith)
|
||||
}
|
||||
found, when, err := restarted.Look(at.Add(2*time.Hour), root, newGate().search)
|
||||
if err != nil || !when.Equal(at) || len(found) != 1 {
|
||||
t.Fatalf("the complete search is dated by its start, not its resumption: %v %v %v", found, when, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, SetuidProgressFile)); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("the finished walk's progress is still kept: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWalkTooOldOrAskedAgainStartsOver(t *testing.T) {
|
||||
for name, c := range map[string]struct {
|
||||
after time.Duration
|
||||
again bool
|
||||
}{"older than it may judge": {rootsearch.FreshFor, false}, "an apply asked for a new one": {time.Hour, true}} {
|
||||
dir := t.TempDir()
|
||||
paths := func() *SetuidCache {
|
||||
return &SetuidCache{Path: filepath.Join(dir, SetuidSearchFile), ProgressPath: filepath.Join(dir, SetuidProgressFile)}
|
||||
}
|
||||
seen := make(chan *SetuidProgress, 1)
|
||||
paths().Look(at, root, interrupted(seen))
|
||||
<-seen
|
||||
restarted := paths()
|
||||
if c.again {
|
||||
restarted.SearchAgain()
|
||||
}
|
||||
var with SetuidProgress
|
||||
restarted.Look(at.Add(c.after), root, func(ctx context.Context, p *SetuidProgress, _ func()) ([]string, error) {
|
||||
with = *p
|
||||
return nil, nil
|
||||
})
|
||||
restarted.Wait()
|
||||
if !with.Started.Equal(at.Add(c.after)) || len(with.Cursor) != 0 || len(with.Found) != 0 {
|
||||
t.Fatalf("%s: the old walk was carried on: %+v", name, with)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestASearchWhosePriorityCannotBeLoweredDoesNotWalk(t *testing.T) {
|
||||
c := &SetuidCache{Lower: func() error { return errors.New("not permitted") }}
|
||||
walked := false
|
||||
c.Look(at, root, func(context.Context, *SetuidProgress, func()) ([]string, error) { walked = true; return nil, nil })
|
||||
c.Wait()
|
||||
_, _, err := c.Look(at.Add(time.Minute), root, newGate().search)
|
||||
notJudged(t, err, false)
|
||||
if walked {
|
||||
t.Fatal("it walked at full priority")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user