Read a one-shot that never ran as stopped, and one at rest as stopped to a stopped declaration (issue 1126)

The kernel module's boot-time cleanup, linux-modules-cleanup.service (Type=oneshot,
declared stopped and enabled at boot), failed its delivery on ace with 'was asked to
be stopped and is running': ServiceState read every inactive one-shot with
ExecMainStatus=0 as running, and systemd says 0 for one that never ran too. Stopping
an inactive unit changes nothing, so the read back said running again; after a boot
that ran it, the same declaration would fail every apply.

ServiceState now reads ExecMainStartTimestampMonotonic: 0 means never ran, read as
stopped (a package's one-shot declared running is then started, and the hold no
longer counts it as in use). The service apply reads a one-shot at rest as stopped
to a declaration that it be stopped (ServiceAtRest), whatever its last run did.

Quick fix weighed: declare the cleanup running in the catalogue. Thorough chosen:
the reading was wrong for every one-shot, and this is its second fault of the kind
(the firewall's was the first). Unit tests: three arch one-shot tests merged into one
table (7 rows), one apply test (2 rows); 1263 to 1262.
This commit is contained in:
2026-10-28 14:29:16 +01:00
parent 7a2af23764
commit cf738cdb90
4 changed files with 203 additions and 79 deletions
+80 -36
View File
@@ -187,38 +187,11 @@ func describeAge(when, now time.Time) string {
// so LoadState is what is read — and it is the thing an interface spanning systemd and OpenRC
// would have had to drop.
func (arch) ServiceState(ctx context.Context, run Runner, unit string) (string, error) {
out, asked := run(ctx, "systemctl", "show",
"--property=LoadState", "--property=ActiveState", "--property=Type",
"--property=RemainAfterExit", "--property=ExecMainStatus", "--", unit)
var load, active, kind, remains, exited string
for _, line := range strings.Split(out, "\n") {
key, value, found := strings.Cut(strings.TrimSpace(line), "=")
if !found {
continue
}
switch key {
case "LoadState":
load = value
case "ActiveState":
active = value
case "Type":
kind = value
case "RemainAfterExit":
remains = value
case "ExecMainStatus":
exited = value
}
u, err := showUnit(ctx, run, unit)
if err != nil {
return "", err
}
switch load {
case "":
// With why, where it said why: an account's manager that could not be reached says so
// here, and nowhere else (novox/hq ADR 0177).
if asked != nil {
return "", fmt.Errorf("the service manager said nothing about %s: %w", unit, asked)
}
return "", fmt.Errorf("the service manager said nothing about %s", unit)
switch u.load {
case "not-found":
return "", fmt.Errorf(
"%s does not exist on this machine. A declaration naming a unit that is not "+
@@ -227,7 +200,7 @@ func (arch) ServiceState(ctx context.Context, run Runner, unit string) (string,
case "masked":
return "", fmt.Errorf("%s is masked, so its state cannot be declared", unit)
case "error", "bad-setting":
return "", fmt.Errorf("%s is installed but its unit file cannot be loaded (%s)", unit, load)
return "", fmt.Errorf("%s is installed but its unit file cannot be loaded (%s)", unit, u.load)
}
// **A one-shot that finished is not stopped.** A unit whose whole job is to apply something
@@ -243,24 +216,95 @@ func (arch) ServiceState(ctx context.Context, run Runner, unit string) (string,
// wrong with.
//
// So for that shape, what "running" means is "it ran, and it worked".
if kind == "oneshot" && remains != "yes" && active == "inactive" {
if exited == "0" || exited == "" {
//
// **And one that never ran is not one that worked** (novox/hq issue 1126). systemd says
// ExecMainStatus=0 for a unit that has not run since the manager loaded it exactly as for one
// that succeeded; only its start timestamp, 0, separates them. Read as "running", a package's
// boot-time one-shot declared stopped could never be satisfied — stopping an inactive unit
// changes nothing, and the read back said running again — and one declared running was never
// started. A timestamp the manager did not say keeps the reading it had before.
if u.oneShotAtRest() {
if u.started == "0" {
return "stopped", nil
}
if u.exited == "0" || u.exited == "" {
return "running", nil
}
return "stopped", nil
}
switch active {
switch u.active {
case "active", "activating", "reloading":
return "running", nil
case "inactive", "failed", "deactivating":
return "stopped", nil
default:
return "", fmt.Errorf(
"the service manager reports %s as %q, which is neither running nor stopped", unit, active)
"the service manager reports %s as %q, which is neither running nor stopped", unit, u.active)
}
}
// ServiceAtRest is whether unit is a one-shot that does not linger and has nothing running now
// (novox/hq issue 1126). Such a unit has no state in which a process is up between its runs, so a
// declaration that it be stopped is satisfied whatever its last run did: there is nothing to stop,
// and asking the manager to stop it changes nothing. A boot-time cleanup a package ships is declared
// this way — enabled at boot, never started by an apply.
func (arch) ServiceAtRest(ctx context.Context, run Runner, unit string) (bool, error) {
u, err := showUnit(ctx, run, unit)
if err != nil {
return false, err
}
return u.oneShotAtRest(), nil
}
// shownUnit is what `systemctl show` says of a unit, as far as its state is read.
type shownUnit struct {
load, active, kind, remains, exited, started string
}
// oneShotAtRest is a one-shot that does not linger and is not running now.
func (u shownUnit) oneShotAtRest() bool {
return u.kind == "oneshot" && u.remains != "yes" && u.active == "inactive"
}
// showUnit reads a unit's state properties. A manager that said nothing is an error, with why
// where it said why: an account's manager that could not be reached says so here, and nowhere
// else (novox/hq ADR 0177).
func showUnit(ctx context.Context, run Runner, unit string) (shownUnit, error) {
out, asked := run(ctx, "systemctl", "show",
"--property=LoadState", "--property=ActiveState", "--property=Type",
"--property=RemainAfterExit", "--property=ExecMainStatus",
"--property=ExecMainStartTimestampMonotonic", "--", unit)
var u shownUnit
for _, line := range strings.Split(out, "\n") {
key, value, found := strings.Cut(strings.TrimSpace(line), "=")
if !found {
continue
}
switch key {
case "LoadState":
u.load = value
case "ActiveState":
u.active = value
case "Type":
u.kind = value
case "RemainAfterExit":
u.remains = value
case "ExecMainStatus":
u.exited = value
case "ExecMainStartTimestampMonotonic":
u.started = value
}
}
if u.load == "" {
if asked != nil {
return u, fmt.Errorf("the service manager said nothing about %s: %w", unit, asked)
}
return u, fmt.Errorf("the service manager said nothing about %s", unit)
}
return u, nil
}
func (arch) SetServiceState(ctx context.Context, run Runner, unit, state string) error {
verb := "start"
if state == "stopped" {