Promise unlink-dangling on the service manager, optional (hq issue 332)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
disable cannot remove an enable link whose unit file is gone, so a leftover unit stays wanted at every login with no verb to end it. Optional until the systemd module serves it (ADR 0246 step 1).
This commit is contained in:
@@ -43,7 +43,7 @@ func TestTheServiceManagerSeatServesTheUnitVerbs(t *testing.T) {
|
|||||||
if seat.Scope != ScopeNode {
|
if seat.Scope != ScopeNode {
|
||||||
t.Fatalf("the service manager is a role each machine has once, and the seat is %s-scoped", seat.Scope)
|
t.Fatalf("the service manager is a role each machine has once, and the seat is %s-scoped", seat.Scope)
|
||||||
}
|
}
|
||||||
want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed", "reset-failed", "wanted-by"}
|
want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed", "reset-failed", "wanted-by", "unlink-dangling"}
|
||||||
var got []string
|
var got []string
|
||||||
for _, v := range seat.Serves {
|
for _, v := range seat.Serves {
|
||||||
got = append(got, v.Name)
|
got = append(got, v.Name)
|
||||||
@@ -81,7 +81,7 @@ func TestFailedIsAnOptionalVerbOfTheServiceManager(t *testing.T) {
|
|||||||
if err := CanHold(holder(eight[1:]...), seat); err == nil || !strings.Contains(err.Error(), "does not serve units") {
|
if err := CanHold(holder(eight[1:]...), seat); err == nil || !strings.Contains(err.Error(), "does not serve units") {
|
||||||
t.Fatalf("a holder missing a required verb was accepted: %v", err)
|
t.Fatalf("a holder missing a required verb was accepted: %v", err)
|
||||||
}
|
}
|
||||||
optional := map[string]bool{"failed": true, "reset-failed": true, "wanted-by": true}
|
optional := map[string]bool{"failed": true, "reset-failed": true, "wanted-by": true, "unlink-dangling": true}
|
||||||
for _, v := range seat.Serves {
|
for _, v := range seat.Serves {
|
||||||
if v.Optional != optional[v.Name] {
|
if v.Optional != optional[v.Name] {
|
||||||
t.Errorf("%s optional: %v", v.Name, v.Optional)
|
t.Errorf("%s optional: %v", v.Name, v.Optional)
|
||||||
@@ -105,10 +105,14 @@ func TestResetFailedAndWantedByAreOptionalVerbsOfTheServiceManager(t *testing.T)
|
|||||||
if err := CanHold(holder(append(nine, "reset-failed", "wanted-by")...), seat); err != nil {
|
if err := CanHold(holder(append(nine, "reset-failed", "wanted-by")...), seat); err != nil {
|
||||||
t.Fatalf("a holder serving both is refused: %v", err)
|
t.Fatalf("a holder serving both is refused: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := CanHold(holder(append(nine, "reset-failed", "wanted-by", "unlink-dangling")...), seat); err != nil {
|
||||||
|
t.Fatalf("a holder serving unlink-dangling too is refused: %v", err)
|
||||||
|
}
|
||||||
if err := CanHold(holder(append(nine, "why-started")...), seat); err == nil || !strings.Contains(err.Error(), "does not promise") {
|
if err := CanHold(holder(append(nine, "why-started")...), seat); err == nil || !strings.Contains(err.Error(), "does not promise") {
|
||||||
t.Fatalf("a verb the seat does not promise was accepted: %v", err)
|
t.Fatalf("a verb the seat does not promise was accepted: %v", err)
|
||||||
}
|
}
|
||||||
replaces := map[string]string{"reset-failed": "systemctl reset-failed", "wanted-by": "systemctl list-dependencies --reverse"}
|
replaces := map[string]string{"reset-failed": "systemctl reset-failed", "wanted-by": "systemctl list-dependencies --reverse",
|
||||||
|
"unlink-dangling": "rm ~/.config/systemd/user/*.wants/<unit>"}
|
||||||
for _, v := range seat.Serves {
|
for _, v := range seat.Serves {
|
||||||
want, ok := replaces[v.Name]
|
want, ok := replaces[v.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -121,6 +125,12 @@ func TestResetFailedAndWantedByAreOptionalVerbsOfTheServiceManager(t *testing.T)
|
|||||||
if _, has := props["unit"]; !has {
|
if _, has := props["unit"]; !has {
|
||||||
t.Errorf("%s takes no unit", v.Name)
|
t.Errorf("%s takes no unit", v.Name)
|
||||||
}
|
}
|
||||||
|
if v.Name == "unlink-dangling" {
|
||||||
|
// Removing is the person's act: their reason is required, and kept with the record.
|
||||||
|
if required, _ := v.Input["required"].([]string); !slices.Contains(required, "why") {
|
||||||
|
t.Errorf("unlink-dangling does not require why: %v", v.Input["required"])
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(replaces, v.Name)
|
delete(replaces, v.Name)
|
||||||
}
|
}
|
||||||
if len(replaces) > 0 {
|
if len(replaces) > 0 {
|
||||||
|
|||||||
@@ -620,8 +620,8 @@ func SeatsWithAProtocol() []Seat {
|
|||||||
// 0177): the units on the machine in both scopes, read and acted on by name. Every verb takes an
|
// 0177): the units on the machine in both scopes, read and acted on by name. Every verb takes an
|
||||||
// optional scope — "system" when absent, "user" for the operator account's own manager — so a
|
// optional scope — "system" when absent, "user" for the operator account's own manager — so a
|
||||||
// caller asks for a user unit the way it asks for a system one; `failed` alone reads both managers
|
// caller asks for a user unit the way it asks for a system one; `failed` alone reads both managers
|
||||||
// when none is named. `failed`, `reset-failed` and `wanted-by` are optional (Verb.Optional) until every
|
// when none is named. `failed`, `reset-failed`, `wanted-by` and `unlink-dangling` are optional
|
||||||
// holder serves them (ADR 0246).
|
// (Verb.Optional) until every holder serves them (ADR 0246).
|
||||||
func serviceManagerVerbs() []Verb {
|
func serviceManagerVerbs() []Verb {
|
||||||
scoped := func(more map[string]string, required []string) map[string]any {
|
scoped := func(more map[string]string, required []string) map[string]any {
|
||||||
props := map[string]string{"scope": "\"system\" (the default) or \"user\": the operator account's own manager"}
|
props := map[string]string{"scope": "\"system\" (the default) or \"user\": the operator account's own manager"}
|
||||||
@@ -685,6 +685,18 @@ func serviceManagerVerbs() []Verb {
|
|||||||
Input: scoped(unit, []string{"unit"}),
|
Input: scoped(unit, []string{"unit"}),
|
||||||
Replaces: []string{"systemctl list-dependencies --reverse", "systemctl show -p WantedBy",
|
Replaces: []string{"systemctl list-dependencies --reverse", "systemctl show -p WantedBy",
|
||||||
"ls ~/.config/systemd/user/*.wants"}},
|
"ls ~/.config/systemd/user/*.wants"}},
|
||||||
|
// **A dangling enable link removed on the person's word** (novox/hq issue 332): `systemctl disable`
|
||||||
|
// leaves a link whose unit file is gone, so the manager keeps asking for the unit. Optional until its
|
||||||
|
// holders serve it (ADR 0246 step 1).
|
||||||
|
{Name: "unlink-dangling", Optional: true, Description: "Remove the dangling enable links named for one " +
|
||||||
|
"unit — links in a *.wants, *.requires or *.upholds directory of the account's or the machine's own " +
|
||||||
|
"configuration whose target does not exist, which `disable` leaves once the unit's file is gone — on " +
|
||||||
|
"the person's word: `why` is required. Each is recorded (its path, its target, the reason) before it is " +
|
||||||
|
"removed, and the manager reloaded; a link whose target exists, or one a package, a generator or the " +
|
||||||
|
"runtime placed, is left and said.",
|
||||||
|
Input: scoped(map[string]string{"unit": unit["unit"],
|
||||||
|
"why": "the person's reason for removing the links, kept in the record"}, []string{"unit", "why"}),
|
||||||
|
Replaces: []string{"rm ~/.config/systemd/user/*.wants/<unit>", "unlink /etc/systemd/system/*.wants/<unit>"}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user