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.
65 lines
2.9 KiB
Go
65 lines
2.9 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
// How a one-shot unit's state is read: what it did, not whether a process is up.
|
|
//
|
|
// **A one-shot that did its work and exited is satisfied, not stopped.** Its unit loads a rule set
|
|
// and exits, so it is inactive the instant it succeeds; reading that as stopped made the firewall
|
|
// permanently unsatisfiable, on every apply, with the rules correctly in place the whole time.
|
|
//
|
|
// **One that failed is still stopped**, or the host would report success for work that did not
|
|
// happen — the opposite mistake and the worse one.
|
|
//
|
|
// **One that never ran is stopped** (novox/hq issue 1126, the kernel's boot-time cleanup on ace):
|
|
// systemd says ExecMainStatus=0 for a unit that never ran exactly as for one that succeeded, and
|
|
// only its start timestamp separates them. Read as "running", a package's one-shot declared
|
|
// running was never started, and one declared stopped could never be.
|
|
//
|
|
// **At rest** is whether a one-shot that does not linger has nothing running now: a declaration
|
|
// that it be stopped is then satisfied, whatever it last did, because there is nothing to stop.
|
|
func TestAOneShotIsReadByWhatItDid(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name, show, want string
|
|
atRest bool
|
|
}{
|
|
{"finished", "ActiveState=inactive\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=0\n" +
|
|
"ExecMainStartTimestampMonotonic=5123456\n", "running", true},
|
|
{"finished, no timestamp read", "ActiveState=inactive\nType=oneshot\nRemainAfterExit=no\n" +
|
|
"ExecMainStatus=0\n", "running", true},
|
|
{"failed", "ActiveState=inactive\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=1\n" +
|
|
"ExecMainStartTimestampMonotonic=5123456\n", "stopped", true},
|
|
{"never ran", "ActiveState=inactive\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=0\n" +
|
|
"ExecMainStartTimestampMonotonic=0\n", "stopped", true},
|
|
{"running now", "ActiveState=activating\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=0\n" +
|
|
"ExecMainStartTimestampMonotonic=5123456\n", "running", false},
|
|
{"lingers", "ActiveState=active\nType=oneshot\nRemainAfterExit=yes\nExecMainStatus=0\n",
|
|
"running", false},
|
|
{"a daemon, stopped", "ActiveState=inactive\nType=simple\nRemainAfterExit=no\nExecMainStatus=0\n",
|
|
"stopped", false},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
run := func(_ context.Context, _ string, _ ...string) (string, error) {
|
|
return "LoadState=loaded\n" + c.show, nil
|
|
}
|
|
state, err := arch{}.ServiceState(context.Background(), run, "thing.service")
|
|
if err != nil {
|
|
t.Fatalf("read as an error: %v", err)
|
|
}
|
|
if state != c.want {
|
|
t.Errorf("reads as %q, want %q", state, c.want)
|
|
}
|
|
rest, err := arch{}.ServiceAtRest(context.Background(), run, "thing.service")
|
|
if err != nil {
|
|
t.Fatalf("at rest read as an error: %v", err)
|
|
}
|
|
if rest != c.atRest {
|
|
t.Errorf("at rest is %v, want %v", rest, c.atRest)
|
|
}
|
|
})
|
|
}
|
|
}
|