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
+48 -12
View File
@@ -9,8 +9,8 @@
// own mount table, and a unit a removed package left behind, failed beside it, said by nobody.
//
// On every look the engine asks each service manager the mesh places units in — the machine's, and the
// account manager of every account the declaration names — which units failed, and says each one's
// owner:
// account manager of every account the declaration places a user-scoped unit for — which units failed,
// and says each one's owner:
//
// 1. a service or process the declaration states, by its unit and manager;
// 2. a file the declaration writes, when the unit's file is that file;
@@ -23,6 +23,12 @@
//
// **It reads; it never acts** (ADR 0240 rule 6): `list-units`, `show` and the package manager's owner
// query are the whole of what it asks. It neither resets nor restarts anything.
//
// **A look starts no account's manager** (novox/hq issue 764, ADR 0177). Reading an account's manager is
// a login, and a login starts the manager when none runs; so the machine's own manager is asked first
// whether user@<uid>.service is active, and the account's is read only then. A manager that is not
// running is away: no unit in it is failed, none is read, and the statement says it away — the apply's
// word for the same absence ("waiting" for its manager).
package units
import (
@@ -72,7 +78,9 @@ type Owned struct {
// Files is keyed by path; Packages by package name, only those declared present.
Files map[string]owner
Packages map[string]owner
// Accounts are the accounts whose own managers are read, sorted.
// Accounts are the accounts whose own managers are read, sorted: each one the declaration places a
// user-scoped unit for, and no other (novox/hq issue 764). An account the declaration only creates
// has no unit of the mesh's in its manager to fail, and asking that manager starts it.
Accounts []string
}
@@ -123,8 +131,6 @@ func OwnedBy(d *declaration.Declaration, held map[string]bool) Owned {
if !v.Absent {
o.Packages[v.Package] = own
}
case *declaration.User:
accounts[v.Name] = true
}
}
for a := range accounts {
@@ -169,6 +175,10 @@ type Reader interface {
Show(ctx context.Context, scope, user string, units []string) (map[string]Shown, error)
// PackageOwning is the package a file belongs to; false when none does or the machine cannot say.
PackageOwning(ctx context.Context, path string) (string, bool, error)
// UserManagerRunning is whether an account's own manager runs now, asked of the machine's manager
// and never of the account's, which a question would start (novox/hq issue 764); exists is false
// for an account the machine does not have.
UserManagerRunning(ctx context.Context, account string) (running, exists bool, err error)
}
// Failed is one failed unit as the statement says it.
@@ -198,6 +208,9 @@ type Statement struct {
Failed []Failed
// Unread names each manager that could not be read, with why.
Unread []string
// Away names each account manager not running, so not read: its account is not logged in and does
// not linger. Nothing in it is failed (novox/hq issue 764).
Away []string
}
// Owned answers the failures a module owns; Unowned those no module does.
@@ -290,16 +303,39 @@ func (j *Judge) Look(ctx context.Context) (Statement, bool) {
st := Statement{At: now, State: Unknown}
read := 0
failedNow := map[string]bool{}
keep := func(m manager) {
// What it held is neither cleared nor counted while it cannot be read.
for k := range j.seen {
if strings.HasPrefix(k, key(m.scope, m.user, "")) {
failedNow[k] = true
}
}
}
for _, m := range managers {
if m.scope == ScopeUser {
// Asked of the machine's manager first: reading the account's own would start it.
running, exists, err := j.reader.UserManagerRunning(ctx, m.user)
switch {
case err != nil:
st.Unread = append(st.Unread, fmt.Sprintf("%s could not be read: %s", m, firstLine(err.Error())))
keep(m)
continue
case !exists:
st.Unread = append(st.Unread, fmt.Sprintf("%s could not be read: the machine has no account %q",
m, m.user))
keep(m)
continue
case !running:
// Away: nothing in it runs, so nothing in it is failed; what it held is cleared.
st.Away = append(st.Away, m.user+"'s own service manager is not running: the account is not "+
"logged in and does not linger, so its units are not read")
continue
}
}
listed, err := j.reader.Failed(ctx, m.scope, m.user)
if err != nil {
st.Unread = append(st.Unread, fmt.Sprintf("%s could not be read: %s", m, firstLine(err.Error())))
// What it held is neither cleared nor counted while it cannot be read.
for k := range j.seen {
if strings.HasPrefix(k, key(m.scope, m.user, "")) {
failedNow[k] = true
}
}
keep(m)
continue
}
read++
@@ -353,7 +389,7 @@ func (j *Judge) Look(ctx context.Context) (Statement, bool) {
if read == 0 {
st.State = Unknown
}
word := st.State
word := st.State + "|away:" + strings.Join(st.Away, ",")
for _, f := range st.Failed {
word += "|" + f.Scope + "/" + f.Unit + "/" + f.Module
}