Start no account's manager to look at its units (hq issue 764) #112
@@ -1130,7 +1130,7 @@ func runLink(ctx context.Context, opts options) error {
|
||||
// The one liveness judge on this machine (novox/hq ADR 0240), made before anything applies: every
|
||||
// report this host makes carries what it says.
|
||||
if j, err := liveness.Open(filepath.Join(filepath.Dir(opts.state), liveness.FileName),
|
||||
&liveness.Exec{Run: apply.ExecRunner}); j != nil {
|
||||
&liveness.Exec{Run: apply.ExecRunner, System: builtFor}); j != nil {
|
||||
if err != nil {
|
||||
say(err.Error())
|
||||
}
|
||||
@@ -1794,7 +1794,8 @@ func withUnits(h *link.Health, us *units.Statement) *link.Health {
|
||||
if us == nil || us.State == "" {
|
||||
return h
|
||||
}
|
||||
h.Units = &link.UnitsHealth{State: us.State, Failed: []link.FailedUnit{}, Unread: us.Unread}
|
||||
h.Units = &link.UnitsHealth{State: us.State, Failed: []link.FailedUnit{}, Unread: us.Unread,
|
||||
Away: us.Away}
|
||||
for _, f := range us.Failed {
|
||||
if f.Module == "" {
|
||||
h.Units.Failed = append(h.Units.Failed, link.FailedUnit{Unit: f.Unit, Scope: f.Scope, Load: f.Load,
|
||||
|
||||
@@ -359,6 +359,8 @@ type UnitsHealth struct {
|
||||
Failed []FailedUnit `json:"failed"`
|
||||
// Unread names a manager that could not be read, with why.
|
||||
Unread []string `json:"unread,omitempty"`
|
||||
// Away names an account's own manager not running, so not read: nothing in it is failed (issue 764).
|
||||
Away []string `json:"away,omitempty"`
|
||||
}
|
||||
|
||||
// FailedUnit is one failed unit no module places.
|
||||
|
||||
@@ -330,3 +330,56 @@ func TestOneLookIsOneReadOfEveryContainer(t *testing.T) {
|
||||
t.Fatalf("a container the inspect found, beside one it did not: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
// **A look starts no account's manager** (novox/hq issue 764, ADR 0177): a user-scoped unit's manager is
|
||||
// asked of the machine's first; one not running is not read, nothing is started, and its unit is said
|
||||
// unknown with why — away, never down; one running is read as before.
|
||||
func TestAUserUnitsManagerNotRunningIsNotReadAndNotDown(t *testing.T) {
|
||||
var asked []string
|
||||
manager := "inactive"
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
asked = append(asked, name+" "+strings.Join(args, " "))
|
||||
switch {
|
||||
case name == "getent":
|
||||
return "operator:x:1000:1000::/home/operator:/bin/zsh\n", nil
|
||||
case name == "systemctl" && len(args) > 0 && args[0] == "is-active":
|
||||
if manager != "active" {
|
||||
return manager + "\n", errors.New("systemctl exited 3")
|
||||
}
|
||||
return "active\n", nil
|
||||
case name == "systemctl":
|
||||
return "Id=dunst.service\nLoadState=loaded\nActiveState=active\nSubState=running\nNRestarts=0\nInvocationID=i1\n", nil
|
||||
}
|
||||
return "", errors.New("not a command the judge may run")
|
||||
}
|
||||
c := &clock{now: t0}
|
||||
j := aJudge(t, &Exec{Run: run, System: "arch"}, c, "")
|
||||
j.Set([]Resource{{Module: "dunst", ID: "dunst.daemon", Kind: KindService, Target: "dunst.service",
|
||||
Scope: "user", User: "operator"}})
|
||||
var st Statement
|
||||
for i := 0; i < 4; i++ {
|
||||
c.now = t0.Add(time.Duration(i) * time.Minute)
|
||||
st, _ = j.Look(t.Context())
|
||||
}
|
||||
if s := stateOf(t, st, "dunst.daemon"); s.State != Unknown || !strings.Contains(s.Reason, "not logged in") {
|
||||
t.Fatalf("a unit in a manager not running is away, not down: %+v", s)
|
||||
}
|
||||
for _, a := range asked {
|
||||
if strings.Contains(a, "--machine=") {
|
||||
t.Fatalf("the account's own manager was asked while it was not running: %q", a)
|
||||
}
|
||||
if a != "getent passwd operator" && a != "systemctl is-active user@1000.service" {
|
||||
t.Errorf("asked something but whether the manager runs: %q", a)
|
||||
}
|
||||
}
|
||||
// Logged in: read in the account's own manager, as before.
|
||||
manager, asked = "active", nil
|
||||
c.now = t0.Add(5 * time.Minute)
|
||||
st, _ = j.Look(t.Context())
|
||||
if s := stateOf(t, st, "dunst.daemon"); s.State == Unknown || s.State == Unhealthy {
|
||||
t.Fatalf("a unit in a running manager is read: %+v", s)
|
||||
}
|
||||
if last := asked[len(asked)-1]; !strings.HasPrefix(last, "systemctl --user --machine=operator@ show ") {
|
||||
t.Fatalf("the running manager is read in the account's own: %q", last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
)
|
||||
|
||||
// Runner runs a command and answers what it printed — the apply's own (apply.ExecRunner), so a look
|
||||
@@ -16,9 +18,13 @@ type Runner func(ctx context.Context, name string, args ...string) (string, erro
|
||||
|
||||
// Exec reads the container runtime and the service manager through their command lines: one inspect of
|
||||
// every container, one show of every unit per manager. **Reads only** — the whole of what this file may
|
||||
// ask either of them is `container inspect`, `version` and `show` (ADR 0240 rule 6, and a test holds it).
|
||||
// ask either of them is `container inspect`, `version` and `show`, and of the machine's manager whether an
|
||||
// account's own is active (ADR 0240 rule 6, and a test holds it).
|
||||
type Exec struct {
|
||||
Run Runner
|
||||
// System is what the host was built for: it says how the machine's manager is asked whether an
|
||||
// account's own runs (system.ManagerRunning).
|
||||
System string
|
||||
|
||||
mu sync.Mutex
|
||||
// cri is the runtime that answered, once one has.
|
||||
@@ -146,6 +152,19 @@ func (e *Exec) Units(ctx context.Context, scope, user string, units []string) (m
|
||||
}
|
||||
args := []string{"show", "--property=" + unitProperties, "--"}
|
||||
if scope == "user" && user != "" {
|
||||
// **A look starts no account's manager** (novox/hq issue 764, ADR 0177): reading it is a login,
|
||||
// which starts it when none runs. The machine's manager is asked first; a manager not running is
|
||||
// away — its units unread, said unknown with why, never down — as the apply says them waiting.
|
||||
running, exists, err := system.ManagerRunning(ctx, e.System, system.Runner(e.Run), user)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("whether %s's own service manager runs could not be read: %w", user, err)
|
||||
case !exists:
|
||||
return nil, fmt.Errorf("the machine has no account %q, whose own manager these units are in", user)
|
||||
case !running:
|
||||
return nil, fmt.Errorf("%s's own service manager is not running: the account is not logged in "+
|
||||
"and does not linger", user)
|
||||
}
|
||||
args = append([]string{"--user", "--machine=" + user + "@"}, args...)
|
||||
}
|
||||
said, err := e.Run(ctx, "systemctl", append(args, units...)...)
|
||||
|
||||
@@ -304,6 +304,27 @@ func For(name string) (System, error) {
|
||||
name, strings.Join(names, ", "))
|
||||
}
|
||||
|
||||
// userManagers is a service manager that runs a manager per account (novox/hq ADR 0177).
|
||||
type userManagers interface {
|
||||
UserManagerRunning(ctx context.Context, run Runner, account string) (running, exists bool, err error)
|
||||
}
|
||||
|
||||
// ManagerRunning is whether account's own manager runs on a machine built for name, asked of the
|
||||
// machine's manager through run — never of the account's, which a question would start (see
|
||||
// arch.UserManagerRunning). A look asks it before it reads anything in an account's manager (novox/hq
|
||||
// issue 764). A system with no manager per account is an error: nothing can be read there.
|
||||
func ManagerRunning(ctx context.Context, name string, run Runner, account string) (running, exists bool, err error) {
|
||||
sys, err := For(name)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
um, ok := sys.(userManagers)
|
||||
if !ok {
|
||||
return false, false, fmt.Errorf("this machine's service manager (%s) has no manager per account", sys.Name())
|
||||
}
|
||||
return um.UserManagerRunning(ctx, run, account)
|
||||
}
|
||||
|
||||
// All is every system the host can be built for.
|
||||
func All() []System {
|
||||
return []System{arch{}, alpine{}, android{}}
|
||||
|
||||
+12
-2
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
)
|
||||
|
||||
// Runner runs a command and answers what it printed — the apply's own (apply.ExecRunner), so a look reads
|
||||
@@ -12,8 +14,9 @@ import (
|
||||
type Runner func(ctx context.Context, name string, args ...string) (string, error)
|
||||
|
||||
// Exec reads the service managers through systemctl and the package database through the system's own
|
||||
// owner query. **Reads only**: `list-units`, `show` and the owner query are the whole of what it asks
|
||||
// (ADR 0240 rule 6, and a test holds it).
|
||||
// owner query. **Reads only**: `list-units`, `show`, the owner query, and — of the machine's manager and
|
||||
// user database — whether an account's manager is active, are the whole of what it asks (ADR 0240 rule 6,
|
||||
// and a test holds it).
|
||||
type Exec struct {
|
||||
Run Runner
|
||||
// System is what the host was built for; it says how a file's package is asked. A system with no
|
||||
@@ -103,3 +106,10 @@ func (e Exec) PackageOwning(ctx context.Context, path string) (string, bool, err
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// UserManagerRunning asks the machine's manager whether an account's own runs, as the apply does before it
|
||||
// touches a user-scoped unit (system.UserManagerRunning; novox/hq ADR 0177, issue 764). A system with no
|
||||
// manager per account cannot say, and is an error: that manager is said unread.
|
||||
func (e Exec) UserManagerRunning(ctx context.Context, account string) (running, exists bool, err error) {
|
||||
return system.ManagerRunning(ctx, e.System, system.Runner(e.Run), account)
|
||||
}
|
||||
|
||||
+48
-12
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user