An account's manager runs only while it is logged in or lingers. A user-scoped unit whose manager is not running is now "waiting" rather than failed, its record kept as it was; its removal is never fatal (kept recorded, retried) and an account that is gone is forgotten. Whether the manager runs is asked of user@<uid>.service in the machine's manager: asking the account's own, through --machine, logs it in. The user shape gains `linger`, set with loginctl, read back from logind's record, and given back on removal like the shell. Unit files the mesh writes under ~/.config/systemd/user or /etc/systemd/user make that unit the mesh's, and made, holds and found units are keyed by manager and name, so an account's unit and the machine's of one name are two units. A service moved between managers gives the old one back through the manager it was in. OpenRC refuses both.
76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// novox/hq ADR 0177: whether an account's own manager runs is asked of the machine's manager, by
|
|
// the account's number — never of the account's, which the question would start.
|
|
func TestAnAccountsManagerIsAskedOfTheMachinesManager(t *testing.T) {
|
|
var asked []string
|
|
up := false
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
line := name + " " + strings.Join(args, " ")
|
|
asked = append(asked, line)
|
|
switch {
|
|
case line == "getent passwd ops":
|
|
return "ops:x:1001:1001::/home/ops:/bin/bash\n", nil
|
|
case name == "getent":
|
|
return "", errors.New("getent exited 2: ")
|
|
case line == "systemctl is-active user@1001.service":
|
|
if up {
|
|
return "active\n", nil
|
|
}
|
|
return "inactive\n", errors.New("systemctl exited 3: ")
|
|
}
|
|
t.Fatalf("asked %q", line)
|
|
return "", nil
|
|
}
|
|
a := arch{}
|
|
for _, want := range []bool{false, true} {
|
|
up = want
|
|
running, exists, err := a.UserManagerRunning(context.Background(), run, "ops")
|
|
if err != nil || !exists || running != want {
|
|
t.Fatalf("up %v: running %v exists %v err %v", want, running, exists, err)
|
|
}
|
|
}
|
|
if _, exists, err := a.UserManagerRunning(context.Background(), run, "nobody-here"); err != nil || exists {
|
|
t.Fatalf("an account the machine does not have: exists %v err %v", exists, err)
|
|
}
|
|
for _, c := range asked {
|
|
if strings.Contains(c, "--user") || strings.Contains(c, "--machine") {
|
|
t.Fatalf("the account's own manager was asked: %s", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLingeringIsReadFromLogindsRecordAndSetThroughLoginctl(t *testing.T) {
|
|
dir := t.TempDir()
|
|
defer LingerIn(dir)()
|
|
a := arch{}
|
|
if on, err := a.Lingering(context.Background(), nil, "ops"); err != nil || on {
|
|
t.Fatalf("no record read as lingering: %v %v", on, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "ops"), nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if on, err := a.Lingering(context.Background(), nil, "ops"); err != nil || !on {
|
|
t.Fatalf("logind's record not read as lingering: %v %v", on, err)
|
|
}
|
|
var asked []string
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
asked = append(asked, name+" "+strings.Join(args, " "))
|
|
return "", nil
|
|
}
|
|
_ = a.SetLingering(context.Background(), run, "ops", true)
|
|
_ = a.SetLingering(context.Background(), run, "ops", false)
|
|
if strings.Join(asked, "; ") != "loginctl enable-linger ops; loginctl disable-linger ops" {
|
|
t.Fatalf("%v", asked)
|
|
}
|
|
}
|