Start no account's manager to look at its units (hq issue 764)
mesh/merge-gate head 62af89f85d0d merged into trunk 1a2677882380 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 62af89f85d0d merged into trunk 1a2677882380 — pass: its merge-check.sh passed
mesh/delivery delivered

Every 15 s the failed-units look read the own manager of every account
the declaration creates, and reading one is a login that starts it: six
module accounts' managers on the control node started and stopped all
day, most of its journal. A look only reads (ADR 0240 rule 6) and never
asks an account's manager (ADR 0177).

An account's manager is read now only when the declaration places a
user-scoped unit in it, and only after the machine's own manager says
user@<uid>.service is active, as the apply asks before it. A manager not
running is said away in the units statement, with no failed units; in
liveness its units read unknown with the apply's words, never down. The
operator's account on a desktop is still read while logged in, since a
module places a user unit there (issue 315's Razer daemon stays said).
This commit is contained in:
2026-10-15 00:33:34 +02:00
parent 1a26778823
commit 62af89f85d
8 changed files with 276 additions and 17 deletions
+117
View File
@@ -21,6 +21,11 @@ type fakeReader struct {
owners map[string]string // by path
unread map[string]error
ownerQs int
// away is each account whose own manager is not running; noAccount each the machine lacks.
away map[string]bool
noAccount map[string]bool
// read is every manager whose failed units were asked, in order.
read []string
}
func mgr(scope, user string) string {
@@ -31,6 +36,7 @@ func mgr(scope, user string) string {
}
func (f *fakeReader) Failed(_ context.Context, scope, user string) ([]Listed, error) {
f.read = append(f.read, mgr(scope, user))
if err := f.unread[mgr(scope, user)]; err != nil {
return nil, err
}
@@ -45,6 +51,13 @@ func (f *fakeReader) Show(_ context.Context, _, _ string, units []string) (map[s
return out, nil
}
func (f *fakeReader) UserManagerRunning(_ context.Context, account string) (bool, bool, error) {
if f.noAccount[account] {
return false, false, nil
}
return !f.away[account], true, nil
}
func (f *fakeReader) PackageOwning(_ context.Context, path string) (string, bool, error) {
f.ownerQs++
p, ok := f.owners[path]
@@ -75,6 +88,11 @@ func declared() *declaration.Declaration {
&declaration.Package{ID: "openrazer.daemon", Type: declaration.TypePackage, Package: "openrazer-daemon"},
&declaration.Package{ID: "clipmenu.greenclip", Type: declaration.TypePackage, Package: "rofi-greenclip", Absent: true},
&declaration.User{ID: "zsh.account", Type: declaration.TypeUser, Name: "operator"},
// The operator's own manager is read because a module places a unit in it (dunst, on a desktop).
&declaration.Service{ID: "dunst.reload", Type: declaration.TypeService, Unit: "dunst.service",
Scope: declaration.ScopeUser, User: "operator"},
// A module account the declaration only creates: its manager is never asked (issue 764).
&declaration.User{ID: "telegram.account", Type: declaration.TypeUser, Name: "telegram"},
&declaration.Service{ID: "sshd.run", Type: declaration.TypeService, Unit: "sshd.service", State: "running"},
&declaration.Service{ID: "power.after-boot", Type: declaration.TypeService, Unit: "mesh-power-after-boot.service"},
&declaration.File{ID: "watcher.unit", Type: declaration.TypeFile, Path: "/home/operator/.config/systemd/user/watcher.service"},
@@ -283,3 +301,102 @@ func TestNothingIsReadBeforeADeclaration(t *testing.T) {
t.Fatalf("before a declaration every unit would read as the machine's: %+v", st)
}
}
// Issue 764: a look starts no account's manager. Only an account the declaration places a user-scoped
// unit for is read at all, and its manager only while the machine's says it runs.
func TestAnAccountWithNoUserUnitsIsNeverRead(t *testing.T) {
o := OwnedBy(declared(), nil)
if strings.Join(o.Accounts, " ") != "operator" {
t.Fatalf("only an account with a user-scoped unit is read: %v", o.Accounts)
}
r := shanks()
j := New(r)
j.Set(o)
lookTwice(t, j)
for _, m := range r.read {
if m == "user:telegram" {
t.Fatalf("a module account the declaration only creates had its manager read: %v", r.read)
}
}
}
func TestAManagerNotRunningIsAwayAndNotRead(t *testing.T) {
r := shanks()
j := New(r)
j.Set(OwnedBy(declared(), nil))
lookTwice(t, j)
if len(j.Last().Owned()) != 1 {
t.Fatalf("logged in, the Razer daemon is said: %+v", j.Last().Failed)
}
// The operator logs out: the manager is not asked, nothing in it is failed, it is said away.
r.away = map[string]bool{"operator": true}
r.read = nil
st, changed := j.Look(context.Background())
if strings.Join(r.read, " ") != "system" {
t.Fatalf("a manager not running is not read: %v", r.read)
}
if !changed || len(st.Away) != 1 || !strings.Contains(st.Away[0], "operator") || len(st.Unread) != 0 {
t.Fatalf("away, said, and not unread: %+v", st)
}
for _, f := range st.Failed {
if f.Scope == ScopeUser {
t.Fatalf("a manager not running has no failed units: %+v", f)
}
}
if st.State != Degraded {
t.Fatalf("the machine's own failed mounts still say degraded: %q", st.State)
}
// Back in: read as before, and a failure is new again (two looks).
r.away = nil
st, _ = j.Look(context.Background())
if len(st.Away) != 0 || len(st.Owned()) != 0 {
t.Fatalf("a manager running again is read, its failure on its first look not yet said: %+v", st)
}
st, _ = j.Look(context.Background())
if len(st.Owned()) != 1 {
t.Fatalf("and said on the second: %+v", st.Failed)
}
}
func TestAnAccountTheMachineLacksIsUnread(t *testing.T) {
r := shanks()
r.noAccount = map[string]bool{"operator": true}
j := New(r)
j.Set(OwnedBy(declared(), nil))
st, _ := j.Look(context.Background())
if strings.Join(r.read, " ") != "system" || len(st.Unread) != 1 {
t.Fatalf("an account the machine lacks is unread and its manager never asked: %v %+v", r.read, st)
}
}
func TestWhetherAManagerRunsIsAskedOfTheMachines(t *testing.T) {
var asked []string
state := "inactive"
run := func(_ context.Context, name string, args ...string) (string, error) {
asked = append(asked, name+" "+strings.Join(args, " "))
switch name {
case "getent":
return "telegram:x:10007:10007::/var/lib/telegram:/usr/bin/nologin\n", nil
case "systemctl":
if state != "active" {
return state + "\n", errors.New("systemctl exited 3")
}
return state + "\n", nil
}
return "", errors.New("unexpected")
}
e := Exec{Run: run, System: "arch"}
if running, exists, err := e.UserManagerRunning(context.Background(), "telegram"); running || !exists || err != nil {
t.Fatalf("an inactive manager is not running: %v %v %v", running, exists, err)
}
state = "active"
if running, _, err := e.UserManagerRunning(context.Background(), "telegram"); !running || err != nil {
t.Fatalf("an active manager runs: %v %v", running, err)
}
for _, a := range asked {
if a != "getent passwd telegram" && a != "systemctl is-active user@10007.service" {
t.Errorf("asked something but the user database and the machine's manager: %q", a)
}
}
}