From 6724dab0cf96ab2de446e28eebd742be24b72d61 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 15:30:54 +0100 Subject: [PATCH] power: bound every shutdown at ten minutes, not systemd's thirty (issue 1126) On ace's reboot of 2026-10-28 every unit had stopped within 17 s, yet the bus and polkit were started again during the shutdown, nothing stopped them a second time, and the reboot waited on reboot.target's 30-minute job timeout, silent, until the operator forced it. node-power owns the shutdown on every machine, so its drop-ins on reboot, poweroff and halt shorten the bound; each target keeps its own forcing action, so the last phase still ends every process, unmounts and syncs. The cause, node-tools starting bundles again while it stops, is fixed in mesh-tools. --- modules/power/README.md | 4 ++ modules/power/cmd/power/templates_test.go | 50 +++++++++++++++++++ modules/power/module.json | 42 ++++++++++++++++ .../system/halt.target.d/mesh-power.conf | 8 +++ .../system/poweroff.target.d/mesh-power.conf | 8 +++ .../system/reboot.target.d/mesh-power.conf | 8 +++ 6 files changed, 120 insertions(+) create mode 100644 modules/power/templates/systemd/system/halt.target.d/mesh-power.conf create mode 100644 modules/power/templates/systemd/system/poweroff.target.d/mesh-power.conf create mode 100644 modules/power/templates/systemd/system/reboot.target.d/mesh-power.conf diff --git a/modules/power/README.md b/modules/power/README.md index b23268f5..7d17e226 100644 --- a/modules/power/README.md +++ b/modules/power/README.md @@ -14,6 +14,7 @@ states on the bus. | `/usr/local/lib/mesh-power/bin/power-moment` | the runner: each module's piece on its own, as root, with `sh`, bounded (`MESH_POWER_BOUND`, default 30 s); every outcome in the journal under `mesh-power` | | five units | `mesh-power-after-boot`, `-before-shutdown` (its stop is the shutdown, while the network is still up), `-before-sleep`, `-after-wake`, `-supply` | | `mesh-power-draw` (process) | samples the power drawn for the module's bundle, which puts it on the bus as `power.draw` (below) | +| `reboot.target.d`, `poweroff.target.d`, `halt.target.d` (`mesh-power.conf`) | a shutdown is bounded at ten minutes, not systemd's thirty; the target keeps its own forcing action (issue 1126: on ace's reboot of 2026-10-28 the bus was started again during the shutdown, nothing stopped it a second time, and the reboot waited 29 minutes) | | `/etc/udev/rules.d/90-mesh-power.rules` | a power supply's change starts `mesh-power-supply`, which runs `on-mains` or `on-battery` once per change of source | ## The moments, and how a module adds code @@ -146,4 +147,7 @@ Each directory or archive this module declares says how it treats a file it did - `/etc/systemd/system/hibernate.target.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). - `/etc/systemd/system/hybrid-sleep.target.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). - `/etc/systemd/system/suspend-then-hibernate.target.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). +- `/etc/systemd/system/reboot.target.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). +- `/etc/systemd/system/poweroff.target.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). +- `/etc/systemd/system/halt.target.d`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). - `/run/mesh-power`: everything in it is the program's; the mesh moves nothing out (ADR 0422 §2). diff --git a/modules/power/cmd/power/templates_test.go b/modules/power/cmd/power/templates_test.go index db4b3f77..1ba16b8c 100644 --- a/modules/power/cmd/power/templates_test.go +++ b/modules/power/cmd/power/templates_test.go @@ -127,3 +127,53 @@ func TestTheBarBlocksAreTwoItemsOfTheBarsCollection(t *testing.T) { } } } + +// A shutdown is bounded at ten minutes, not systemd's thirty (issue 1126, ace's reboot of 2026-10-28): every unit had +// stopped within 17 s, yet the bus and the authorization manager were started again during the shutdown, nothing +// stopped them a second time, and the reboot waited on its job's own timeout of 30 min with nothing on the console. +// The bound keeps each target's own forcing action (reboot-force and the like): systemd's last phase still ends +// every process, unmounts and syncs. Ten minutes stays above the longest stop the catalogue declares (120 s) chained. +func TestEveryShutdownIsBoundedAtTenMinutes(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Resources []map[string]any `json:"resources"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + byPath := map[string]map[string]any{} + for _, r := range m.Resources { + if p, ok := r["path"].(string); ok { + byPath[p] = r + } + } + for _, target := range []string{"reboot", "poweroff", "halt"} { + dir := "/etc/systemd/system/" + target + ".target.d" + if d := byPath[dir]; d == nil || d["type"] != "directory" || d["unknown"] != "program" { + t.Errorf("%s: no directory resource %s whose unknown files are the program's: %v", target, dir, d) + } + f := byPath[dir+"/mesh-power.conf"] + if f == nil || f["type"] != "file" { + t.Errorf("%s: no file resource %s/mesh-power.conf", target, dir) + continue + } + name, _ := f["template"].(string) + text, err := os.ReadFile(filepath.Join("../..", name)) + if err != nil { + t.Errorf("%s: %v", target, err) + continue + } + var lines []string + for _, l := range strings.Split(string(text), "\n") { + if l != "" && !strings.HasPrefix(l, "#") { + lines = append(lines, l) + } + } + if strings.Join(lines, "\n") != "[Unit]\nJobTimeoutSec=10min" { + t.Errorf("%s: %s sets %q, want only [Unit] JobTimeoutSec=10min (the target keeps its own action)", target, name, lines) + } + } +} diff --git a/modules/power/module.json b/modules/power/module.json index a110d410..909499ad 100644 --- a/modules/power/module.json +++ b/modules/power/module.json @@ -314,6 +314,48 @@ "mode": "0644", "template": "templates/systemd/system/suspend-then-hibernate.target.d/mesh-power.conf" }, + { + "id": "reboot-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/reboot.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "reboot-bound", + "type": "file", + "path": "/etc/systemd/system/reboot.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/reboot.target.d/mesh-power.conf" + }, + { + "id": "poweroff-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/poweroff.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "poweroff-bound", + "type": "file", + "path": "/etc/systemd/system/poweroff.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/poweroff.target.d/mesh-power.conf" + }, + { + "id": "halt-drop-ins", + "type": "directory", + "path": "/etc/systemd/system/halt.target.d", + "mode": "0755", + "unknown": "program" + }, + { + "id": "halt-bound", + "type": "file", + "path": "/etc/systemd/system/halt.target.d/mesh-power.conf", + "mode": "0644", + "template": "templates/systemd/system/halt.target.d/mesh-power.conf" + }, { "id": "udev-rule", "type": "file", diff --git a/modules/power/templates/systemd/system/halt.target.d/mesh-power.conf b/modules/power/templates/systemd/system/halt.target.d/mesh-power.conf new file mode 100644 index 00000000..569d7fd9 --- /dev/null +++ b/modules/power/templates/systemd/system/halt.target.d/mesh-power.conf @@ -0,0 +1,8 @@ +# Written by the mesh (module power, novox/hq ADR 0211; issue 1126). Replaced on every push. +# +# A shutdown is bounded at ten minutes, not systemd's thirty. A unit started again during the shutdown +# (on ace, 2026-10-28, the bus and the authorization manager) is never stopped a second time, and the +# shutdown then waits on this job's timeout with nothing on the console. The target keeps its own +# forcing action: the last phase still ends every process, unmounts and syncs. +[Unit] +JobTimeoutSec=10min diff --git a/modules/power/templates/systemd/system/poweroff.target.d/mesh-power.conf b/modules/power/templates/systemd/system/poweroff.target.d/mesh-power.conf new file mode 100644 index 00000000..569d7fd9 --- /dev/null +++ b/modules/power/templates/systemd/system/poweroff.target.d/mesh-power.conf @@ -0,0 +1,8 @@ +# Written by the mesh (module power, novox/hq ADR 0211; issue 1126). Replaced on every push. +# +# A shutdown is bounded at ten minutes, not systemd's thirty. A unit started again during the shutdown +# (on ace, 2026-10-28, the bus and the authorization manager) is never stopped a second time, and the +# shutdown then waits on this job's timeout with nothing on the console. The target keeps its own +# forcing action: the last phase still ends every process, unmounts and syncs. +[Unit] +JobTimeoutSec=10min diff --git a/modules/power/templates/systemd/system/reboot.target.d/mesh-power.conf b/modules/power/templates/systemd/system/reboot.target.d/mesh-power.conf new file mode 100644 index 00000000..569d7fd9 --- /dev/null +++ b/modules/power/templates/systemd/system/reboot.target.d/mesh-power.conf @@ -0,0 +1,8 @@ +# Written by the mesh (module power, novox/hq ADR 0211; issue 1126). Replaced on every push. +# +# A shutdown is bounded at ten minutes, not systemd's thirty. A unit started again during the shutdown +# (on ace, 2026-10-28, the bus and the authorization manager) is never stopped a second time, and the +# shutdown then waits on this job's timeout with nothing on the console. The target keeps its own +# forcing action: the last phase still ends every process, unmounts and syncs. +[Unit] +JobTimeoutSec=10min