@rubytech/create-maxy-code 0.1.333 → 0.1.336
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
// Contract for the hardware watchdog
|
|
1
|
+
// Contract for the hardware watchdog feature.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// History: an early version armed RuntimeWatchdogSec=20s at install step 1
|
|
4
|
+
// and the install reboot-looped its own build. A follow-up deferred the
|
|
5
|
+
// arm to install end and applied it via daemon-reexec. That arm landed at
|
|
6
|
+
// the peak of post-install load (services initialising, chattr +i sweep
|
|
7
|
+
// just finished, neo4j seeding); the 20s timer fired before PID1 caught up
|
|
8
|
+
// and beacons reset immediately after install end with the connection
|
|
9
|
+
// dropped mid-banner.
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// Raising RuntimeWatchdogSec is inert above the kernel watchdog cap
|
|
12
|
+
// (~15s on BCM2835), so the timer cannot simply be loosened. The feature
|
|
13
|
+
// has been retired. The contract is now:
|
|
14
|
+
// 1. configureHardwareWatchdog() arms nothing — no drop-in write, no
|
|
15
|
+
// daemon-reexec, no RuntimeWatchdogSec=20s reference.
|
|
16
|
+
// 2. disableHardwareWatchdogForInstall() runs at install start and
|
|
17
|
+
// removes the drop-in (rm -f) on devices that carry one from an
|
|
18
|
+
// earlier installer version, plus a daemon-reexec to apply.
|
|
19
|
+
//
|
|
20
|
+
// Static-grep, not behaviour: the call sites live in the top-level install
|
|
21
|
+
// flow of index.ts. Read src/index.ts at runtime as the authoritative source.
|
|
14
22
|
import test from "node:test";
|
|
15
23
|
import assert from "node:assert/strict";
|
|
16
24
|
import { readFileSync } from "node:fs";
|
|
17
25
|
import { fileURLToPath } from "node:url";
|
|
18
26
|
import { dirname, resolve } from "node:path";
|
|
19
|
-
// dist/__tests__/watchdog-deferred-arming.test.js → ../../src/index.ts
|
|
20
27
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
28
|
const INDEX_TS = resolve(here, "../../src/index.ts");
|
|
22
29
|
const SRC = readFileSync(INDEX_TS, "utf-8");
|
|
@@ -28,35 +35,30 @@ function watchdogFnBody() {
|
|
|
28
35
|
assert.ok(end > start, "could not bound configureHardwareWatchdog body");
|
|
29
36
|
return SRC.slice(start, end);
|
|
30
37
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
assert.ok(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
assert.ok(body.includes('"daemon-reexec"'), "configureHardwareWatchdog must apply via daemon-reexec");
|
|
47
|
-
assert.equal(body.includes('"daemon-reload"'), false, "daemon-reload does not apply RuntimeWatchdogSec");
|
|
38
|
+
/** Body of disableHardwareWatchdogForInstall(), up to the next top-level function. */
|
|
39
|
+
function disableFnBody() {
|
|
40
|
+
const start = SRC.indexOf("function disableHardwareWatchdogForInstall");
|
|
41
|
+
assert.ok(start >= 0, "could not locate disableHardwareWatchdogForInstall in index.ts");
|
|
42
|
+
// The function lives immediately before configureHardwareWatchdog().
|
|
43
|
+
const end = SRC.indexOf("function configureHardwareWatchdog", start);
|
|
44
|
+
assert.ok(end > start, "could not bound disableHardwareWatchdogForInstall body");
|
|
45
|
+
return SRC.slice(start, end);
|
|
46
|
+
}
|
|
47
|
+
test("watchdog disable runs at install start, before any heavy step", () => {
|
|
48
|
+
const disableCall = SRC.indexOf("disableHardwareWatchdogForInstall();");
|
|
49
|
+
const installSystemDeps = SRC.indexOf("installSystemDeps();");
|
|
50
|
+
assert.ok(disableCall >= 0, "no disableHardwareWatchdogForInstall() call in the install flow");
|
|
51
|
+
assert.ok(installSystemDeps >= 0, "expected installSystemDeps() call");
|
|
52
|
+
assert.ok(disableCall < installSystemDeps, "disableHardwareWatchdogForInstall() must precede installSystemDeps() so the heavy steps run unarmed");
|
|
48
53
|
});
|
|
49
|
-
test("
|
|
50
|
-
// A drop-in on disk does not prove the live PID1 has the watchdog active
|
|
51
|
-
// (prior daemon-reload, or a just-removed disable override). The function must
|
|
52
|
-
// read RuntimeWatchdogUSec and re-arm when it is 0, so the muvin remediation
|
|
53
|
-
// (drop-in present, previously disabled) actually arms.
|
|
54
|
+
test("watchdog post-install arm is retired — no drop-in write, no daemon-reexec", () => {
|
|
54
55
|
const body = watchdogFnBody();
|
|
55
|
-
assert.
|
|
56
|
-
assert.
|
|
56
|
+
assert.equal(body.includes("RuntimeWatchdogSec=20s"), false, "configureHardwareWatchdog must not re-introduce the 20s timer that reboot-looped constrained Pis");
|
|
57
|
+
assert.equal(body.includes("RebootWatchdogSec=2min"), false, "configureHardwareWatchdog must not re-introduce the reboot timer drop-in line");
|
|
58
|
+
assert.equal(body.includes('"daemon-reexec"'), false, "configureHardwareWatchdog must not arm via daemon-reexec — the post-install arm killed beacons");
|
|
57
59
|
});
|
|
58
|
-
test("
|
|
59
|
-
const body =
|
|
60
|
-
assert.ok(body.includes("
|
|
61
|
-
assert.ok(body.includes("
|
|
60
|
+
test("disable-for-install scrubs the drop-in (rm) and daemon-reexecs", () => {
|
|
61
|
+
const body = disableFnBody();
|
|
62
|
+
assert.ok(body.includes('"-f"') && body.includes("dropinPath"), "must remove the drop-in (rm -f dropinPath) so the live RuntimeWatchdogUSec returns to 0");
|
|
63
|
+
assert.ok(body.includes('"daemon-reexec"'), "must daemon-reexec after removing the drop-in so the change applies live without waiting for reboot");
|
|
62
64
|
});
|
package/dist/index.js
CHANGED
|
@@ -625,98 +625,73 @@ function writeChromiumBinaryPathFile() {
|
|
|
625
625
|
// after the build/swap window, then applying it with `daemon-reexec` so it is
|
|
626
626
|
// active immediately (RuntimeWatchdogUSec non-zero), keeps kernel-hang recovery
|
|
627
627
|
// without the loop.
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
// boot resumes with the watchdog re-armed, the install never completes, and
|
|
635
|
-
// the box wedges (SSH never returns; observed on beacons).
|
|
628
|
+
// Remove any prior hardware-watchdog drop-in this installer (or an earlier
|
|
629
|
+
// version) wrote, then daemon-reexec so the live RuntimeWatchdogUSec
|
|
630
|
+
// goes back to systemd's default (0 = off). The feature is permanently
|
|
631
|
+
// disabled; configureHardwareWatchdog() no longer re-creates the file at
|
|
632
|
+
// install end. See configureHardwareWatchdog() comment for the rationale
|
|
633
|
+
// (the 20s timer reboot-looped constrained Pis when re-armed at install end).
|
|
636
634
|
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
// at install end by configureHardwareWatchdog(). If install fails between
|
|
640
|
-
// the two, the box is unarmed — recoverable, far better than reboot-loop.
|
|
635
|
+
// Runs unconditionally at install start. Idempotent: if no drop-in is on
|
|
636
|
+
// disk, this is a no-op.
|
|
641
637
|
function disableHardwareWatchdogForInstall() {
|
|
642
638
|
try {
|
|
643
639
|
if (!isLinux()) {
|
|
644
|
-
logFile(" hardware watchdog:
|
|
640
|
+
logFile(" hardware watchdog: scrub-on-install skipped (not Linux)");
|
|
645
641
|
return;
|
|
646
642
|
}
|
|
647
|
-
|
|
648
|
-
|
|
643
|
+
const dropinPath = "/etc/systemd/system.conf.d/10-maxy-watchdog.conf";
|
|
644
|
+
if (!existsSync(dropinPath)) {
|
|
645
|
+
logFile(` hardware watchdog: no prior drop-in to scrub (${dropinPath})`);
|
|
649
646
|
return;
|
|
650
647
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const dropinPath = `${dropinDir}/10-maxy-watchdog.conf`;
|
|
654
|
-
const tmpPath = "/tmp/10-maxy-watchdog.conf";
|
|
655
|
-
writeFileSync(tmpPath, disabled);
|
|
656
|
-
console.log(" [privileged] disable hardware watchdog for install duration (RuntimeWatchdogSec=0)");
|
|
657
|
-
shell("mkdir", ["-p", dropinDir], { sudo: true });
|
|
658
|
-
shell("cp", [tmpPath, dropinPath], { sudo: true });
|
|
659
|
-
spawnSync("rm", ["-f", tmpPath]);
|
|
648
|
+
console.log(` [privileged] remove prior hardware-watchdog drop-in (${dropinPath}) — feature retired`);
|
|
649
|
+
shell("rm", ["-f", dropinPath], { sudo: true });
|
|
660
650
|
spawnSync("sudo", ["systemctl", "daemon-reexec"], { stdio: "inherit" });
|
|
661
|
-
logFile(` hardware watchdog:
|
|
651
|
+
logFile(` hardware watchdog: drop-in removed; daemon-reexec applied — RuntimeWatchdogUSec back to default`);
|
|
662
652
|
}
|
|
663
653
|
catch (err) {
|
|
664
|
-
console.error(` WARNING: failed to
|
|
654
|
+
console.error(` WARNING: failed to scrub hardware-watchdog drop-in: ${err instanceof Error ? err.message : String(err)}`);
|
|
665
655
|
}
|
|
666
656
|
}
|
|
667
657
|
function configureHardwareWatchdog() {
|
|
658
|
+
// The hardware-watchdog drop-in was hostile to constrained Pis. Task 997
|
|
659
|
+
// deferred the arm to install end so a tight 20s timer would not reboot the
|
|
660
|
+
// box mid-build. But arming via daemon-reexec at install end lands the live
|
|
661
|
+
// timer at the very peak of post-install load (services initialising,
|
|
662
|
+
// chattr +i sweep just finished, neo4j seeding). PID1 cannot feed
|
|
663
|
+
// /dev/watchdog fast enough — the SoC resets, next boot starts the same
|
|
664
|
+
// services under the same load, resets again. Beacons reboot-looped on
|
|
665
|
+
// every install after the previous fix until the operator deleted the
|
|
666
|
+
// drop-in by hand.
|
|
667
|
+
//
|
|
668
|
+
// Raising RuntimeWatchdogSec is inert above the kernel watchdog cap
|
|
669
|
+
// (~15s on BCM2835), so the timer cannot simply be loosened. Until a
|
|
670
|
+
// version of this feature actually survives a constrained-Pi install
|
|
671
|
+
// unattended, it is removed. The disable-at-install-start path
|
|
672
|
+
// (disableHardwareWatchdogForInstall) stays — it scrubs any prior
|
|
673
|
+
// drop-in off devices that had it from earlier installer versions, so the
|
|
674
|
+
// hostile timer is cleared from the fleet on first run of this installer
|
|
675
|
+
// version. Kernel-freeze recovery is deferred to the in-server software
|
|
676
|
+
// watchdog (Type=notify WATCHDOG_USEC, already shipped on the brand
|
|
677
|
+
// service) which restarts the UI process, not the whole SoC.
|
|
668
678
|
try {
|
|
669
679
|
if (!isLinux()) {
|
|
670
680
|
logFile(" hardware watchdog: skipped (not Linux)");
|
|
671
681
|
return;
|
|
672
682
|
}
|
|
673
|
-
|
|
674
|
-
logFile(" hardware watchdog: skipped (/dev/watchdog absent)");
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
const desired = "[Manager]\nRuntimeWatchdogSec=20s\nRebootWatchdogSec=2min\n";
|
|
678
|
-
const dropinDir = "/etc/systemd/system.conf.d";
|
|
679
|
-
const dropinPath = `${dropinDir}/10-maxy-watchdog.conf`;
|
|
680
|
-
let current = "";
|
|
683
|
+
const dropinPath = "/etc/systemd/system.conf.d/10-maxy-watchdog.conf";
|
|
681
684
|
if (existsSync(dropinPath)) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// Unreadable existing drop-in — treat as changed so we rewrite.
|
|
687
|
-
current = "";
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (current !== desired) {
|
|
691
|
-
const tmpPath = "/tmp/10-maxy-watchdog.conf";
|
|
692
|
-
writeFileSync(tmpPath, desired);
|
|
693
|
-
console.log(" [privileged] install systemd hardware watchdog drop-in (RuntimeWatchdogSec=20s RebootWatchdogSec=2min)");
|
|
694
|
-
shell("mkdir", ["-p", dropinDir], { sudo: true });
|
|
695
|
-
shell("cp", [tmpPath, dropinPath], { sudo: true });
|
|
696
|
-
spawnSync("rm", ["-f", tmpPath]);
|
|
697
|
-
}
|
|
698
|
-
// Arm now, idempotently. The drop-in being on disk does NOT mean the live
|
|
699
|
-
// PID1 has the watchdog active — a prior install that only ran daemon-reload,
|
|
700
|
-
// or a just-removed disable override (RuntimeWatchdogSec=0), leaves the file
|
|
701
|
-
// present but RuntimeWatchdogUSec=0. So read the live value and only skip the
|
|
702
|
-
// re-exec when the watchdog is genuinely armed already; otherwise daemon-reexec
|
|
703
|
-
// re-execs PID1 in place (host-wide; running services are preserved) to apply
|
|
704
|
-
// the system-manager setting. daemon-reload does not apply RuntimeWatchdogSec.
|
|
705
|
-
const liveUsec = spawnSync("systemctl", ["show", "-p", "RuntimeWatchdogUSec", "--value"], {
|
|
706
|
-
encoding: "utf-8",
|
|
707
|
-
stdio: "pipe",
|
|
708
|
-
timeout: 5_000,
|
|
709
|
-
});
|
|
710
|
-
const usec = (liveUsec.stdout ?? "").trim();
|
|
711
|
-
if (current === desired && usec !== "" && usec !== "0") {
|
|
712
|
-
logFile(` hardware watchdog: already active (${dropinPath}, RuntimeWatchdogUSec=${usec})`);
|
|
685
|
+
// Defence in depth — disableHardwareWatchdogForInstall() already runs
|
|
686
|
+
// at install start, but log here too so the install summary makes the
|
|
687
|
+
// policy explicit even if step 0 was skipped.
|
|
688
|
+
logFile(` hardware watchdog: drop-in present at ${dropinPath} but feature is disabled (see disableHardwareWatchdogForInstall)`);
|
|
713
689
|
return;
|
|
714
690
|
}
|
|
715
|
-
|
|
716
|
-
logFile(` hardware watchdog: enabled (${dropinPath}; armed now via daemon-reexec, after the build/swap window)`);
|
|
691
|
+
logFile(" hardware watchdog: feature disabled (constrained-Pi reboot loops)");
|
|
717
692
|
}
|
|
718
693
|
catch (err) {
|
|
719
|
-
console.error(` WARNING:
|
|
694
|
+
console.error(` WARNING: hardware watchdog post-install probe failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
720
695
|
}
|
|
721
696
|
}
|
|
722
697
|
function installSystemDeps() {
|
|
@@ -1564,6 +1539,28 @@ WantedBy=multi-user.target
|
|
|
1564
1539
|
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "inherit" });
|
|
1565
1540
|
console.log(" [privileged] systemctl enable");
|
|
1566
1541
|
shell("systemctl", ["enable", serviceName], { sudo: true });
|
|
1542
|
+
// Tear down a legacy-package dedicated Neo4j unit pinning the same port.
|
|
1543
|
+
// The `-code` rewrite packages (`create-<brand>-code`) replace the legacy
|
|
1544
|
+
// `create-<brand>` packages. When migrating a device from legacy to -code,
|
|
1545
|
+
// both packages' dedicated Neo4j units exist (`neo4j-<brand>.service` and
|
|
1546
|
+
// `neo4j-<brand>-code.service`) and both try to bind the same dedicated
|
|
1547
|
+
// bolt port. The legacy unit wins the race (it boots first because it's
|
|
1548
|
+
// alphabetically earlier in systemd's load order), the new unit silently
|
|
1549
|
+
// fails with `bind failed -98 Address already in use`, and the seed step
|
|
1550
|
+
// talks to the legacy instance with the wrong password — auth fails with
|
|
1551
|
+
// a misleading "client is unauthorized" (observed on beacons).
|
|
1552
|
+
if (brandSuffix.endsWith("-code")) {
|
|
1553
|
+
const legacyBrand = brandSuffix.slice(0, -"-code".length);
|
|
1554
|
+
const legacyUnit = `neo4j-${legacyBrand}.service`;
|
|
1555
|
+
const legacyUnitFile = `/etc/systemd/system/${legacyUnit}`;
|
|
1556
|
+
if (existsSync(legacyUnitFile)) {
|
|
1557
|
+
const legacyMsg = ` [neo4j] legacy unit detected (${legacyUnit}) — stopping + disabling so the -code unit owns port ${NEO4J_PORT}`;
|
|
1558
|
+
console.log(legacyMsg);
|
|
1559
|
+
logFile(legacyMsg);
|
|
1560
|
+
shell("systemctl", ["stop", legacyUnit], { sudo: true, bestEffort: true });
|
|
1561
|
+
shell("systemctl", ["disable", legacyUnit], { sudo: true, bestEffort: true });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1567
1564
|
// skip stop+disable when a peer brand on this host still depends
|
|
1568
1565
|
// on the apt `neo4j.service` (port 7687). Disabling it would kill the peer's
|
|
1569
1566
|
// database reproducer on Neo's laptop. The kept-active path is
|
|
@@ -2856,7 +2853,29 @@ function writeInstallDefaults(accountId) {
|
|
|
2856
2853
|
mkdirSync(publicDir, { recursive: true });
|
|
2857
2854
|
const publicTemplatesDir = join(INSTALL_DIR, "platform/templates/agents/public");
|
|
2858
2855
|
const publicIdentityDst = join(publicDir, "IDENTITY.md");
|
|
2859
|
-
|
|
2856
|
+
const publicIdentitySrc = join(publicTemplatesDir, "IDENTITY.md");
|
|
2857
|
+
cpSync(publicIdentitySrc, publicIdentityDst);
|
|
2858
|
+
// Defensive verify: a 0-byte IDENTITY.md silently traps the brand server
|
|
2859
|
+
// in STARTING (the reachability auditor logs `reachable=false reason=files
|
|
2860
|
+
// -missing IDENTITY.md:empty` every minute and the splash never clears).
|
|
2861
|
+
// Observed on 192.168.88.16 after a re-install: source 1655 bytes, dest
|
|
2862
|
+
// 0 bytes. Loud-fail here so a corrupt copy can never reach the device's
|
|
2863
|
+
// runtime.
|
|
2864
|
+
try {
|
|
2865
|
+
const srcBytes = statSync(publicIdentitySrc).size;
|
|
2866
|
+
const dstBytes = statSync(publicIdentityDst).size;
|
|
2867
|
+
if (dstBytes === 0 || dstBytes !== srcBytes) {
|
|
2868
|
+
throw new Error(`public-identity copy verify failed: src=${publicIdentitySrc} (${srcBytes} bytes) ` +
|
|
2869
|
+
`dst=${publicIdentityDst} (${dstBytes} bytes). A 0-byte or partial copy traps the brand ` +
|
|
2870
|
+
`server in STARTING forever — re-run the installer after restoring the template, or ` +
|
|
2871
|
+
`report this as a copy regression.`);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
catch (err) {
|
|
2875
|
+
if (err instanceof Error && err.message.startsWith("public-identity copy verify failed"))
|
|
2876
|
+
throw err;
|
|
2877
|
+
throw new Error(`public-identity verify stat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2878
|
+
}
|
|
2860
2879
|
console.log(` [install-defaults] public-identity path=${publicIdentityDst}`);
|
|
2861
2880
|
logFile(` [install-defaults] public-identity path=${publicIdentityDst}`);
|
|
2862
2881
|
}
|
package/package.json
CHANGED
|
@@ -145,6 +145,15 @@ SETTINGS_EOF
|
|
|
145
145
|
[ "$_AGENT_NAME" = "admin" ] && continue
|
|
146
146
|
[ -f "$_AGENT_DIR/IDENTITY.md" ] || continue
|
|
147
147
|
cp "$TEMPLATES_DIR/agents/public/IDENTITY.md" "$_AGENT_DIR/IDENTITY.md"
|
|
148
|
+
# Defensive verify — a 0-byte cp traps the brand server in STARTING (the
|
|
149
|
+
# reachability auditor reports `reachable=false reason=files-missing
|
|
150
|
+
# IDENTITY.md:empty` every minute). Observed on 192.168.88.16 after a
|
|
151
|
+
# re-install: source 1655 bytes, dest 0 bytes. Loud-fail here so the
|
|
152
|
+
# operator sees the bad write at install time, not the splash forever.
|
|
153
|
+
if [ ! -s "$_AGENT_DIR/IDENTITY.md" ]; then
|
|
154
|
+
echo " [setup] FATAL agents/$_AGENT_NAME/IDENTITY.md is empty after copy from $TEMPLATES_DIR/agents/public/IDENTITY.md ($(wc -c < "$TEMPLATES_DIR/agents/public/IDENTITY.md") bytes); aborting before the brand server starts" >&2
|
|
155
|
+
return 1
|
|
156
|
+
fi
|
|
148
157
|
echo " agents/$_AGENT_NAME/IDENTITY.md re-synced"
|
|
149
158
|
done
|
|
150
159
|
|