Skip to content

Commit d89e06c

Browse files
fix(update): report failure when systemctl will not start the update timer (#282)
`moshcode update --timer --install` writes the systemd units, runs `systemctl daemon-reload` and `systemctl enable --now`, then always prints "checking on a schedule now" and exits 0 — even when the enable fails. On a host without systemd (a container, WSL, macOS) or without root, the timer never starts, so the tool promises an auto-update that will never fire. Two layers were dropping the failure: - bin/moshcode.mjs wired `runner` to swallow execFile's error and always resolve { ok: true }, so selfUpdateCommand could never see a failure. - selfUpdateCommand ignored the runner results entirely. Surface execFile's error as { ok: !err }, and when daemon-reload or enable reports ok:false, tell the user the timer is not scheduled yet and exit 1 instead of claiming success. The existing tests already inject runner: () => ({ ok: true }), so the { ok } contract is honored. Co-authored-by: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7e4e07f commit d89e06c

3 files changed

Lines changed: 33 additions & 3 deletions

File tree

bin/moshcode.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ async function main() {
417417
return results.filter((r) => !r.ok).length ? 1 : 0;
418418
},
419419
write: (path, body) => fsp.writeFile(path, body),
420-
runner: (cmd2, args2) => new Promise((res) => execFile(cmd2, args2, () => res({ ok: true }))),
420+
runner: (cmd2, args2) => new Promise((res) => execFile(cmd2, args2, (err) => res({ ok: !err }))),
421421
})) || 0;
422422
return;
423423
}

src/selfupdate.mjs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,16 @@ export async function selfUpdateCommand(args = [], out = console.log, deps = {})
189189
await write(`/etc/systemd/system/${name}`, body);
190190
out(` wrote /etc/systemd/system/${name}`);
191191
}
192-
await runner("systemctl", ["daemon-reload"]);
193-
await runner("systemctl", ["enable", "--now", "moshcode-update.timer"]);
192+
const reload = await runner("systemctl", ["daemon-reload"]);
193+
const enable = await runner("systemctl", ["enable", "--now", "moshcode-update.timer"]);
194+
// The units are on disk, but they only run if systemd actually took them.
195+
// On a host without systemd (a container, WSL, macOS) or without root, the
196+
// enable fails — and saying "checking on a schedule now" then would promise
197+
// an auto-update that will never fire. Report the failure instead.
198+
if (reload?.ok === false || enable?.ok === false) {
199+
out("moshcode update: wrote the units but systemctl could not start the timer — it is not checking on a schedule yet. run `systemctl enable --now moshcode-update.timer` as root.");
200+
return 1;
201+
}
194202
out("checking on a schedule now. `systemctl list-timers moshcode-update` to see when.");
195203
return 0;
196204
}

test/selfupdate.test.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,28 @@ test("control: a valid --interval still writes the units it always did", async (
148148
assert.match(lines.join("\n"), /checking on a schedule/);
149149
});
150150

151+
// The units on disk mean nothing if systemd never took them. When `systemctl
152+
// enable --now` fails — no systemd (container/WSL/macOS) or no root — the
153+
// command must not claim it is "checking on a schedule now"; that would promise
154+
// an auto-update that never fires.
155+
test("--install reports failure when systemctl cannot start the timer", async () => {
156+
const written = new Map();
157+
const lines = [];
158+
const code = await selfUpdateCommand(
159+
["--timer", "--interval", "1h", "--install"],
160+
(l) => lines.push(l),
161+
{
162+
write: async (p, b) => written.set(p, b),
163+
runner: async (cmd, args) => (cmd === "systemctl" && args[0] === "enable" ? { ok: false } : { ok: true }),
164+
},
165+
);
166+
167+
assert.equal(code, 1);
168+
assert.equal(written.size, 2); // units were still written, we just did not lie about the timer
169+
assert.doesNotMatch(lines.join("\n"), /checking on a schedule now/);
170+
assert.match(lines.join("\n"), /systemctl could not start the timer/);
171+
});
172+
151173
test("control: --timer with no --interval is still the default", async () => {
152174
const lines = [];
153175
assert.equal(await selfUpdateCommand(["--timer"], (l) => lines.push(l), {}), 0);

0 commit comments

Comments
 (0)