feat(scheduling): systemd --user backend for the 11 remaining launchd-only installers - #1698
Open
elhoim wants to merge 1 commit into
Open
feat(scheduling): systemd --user backend for the 11 remaining launchd-only installers#1698elhoim wants to merge 1 commit into
elhoim wants to merge 1 commit into
Conversation
…-only installers Follows the shape set by the WorkSweep systemd PR (danielmiessler#1692) and applies it to every other scheduled LifeOS job. Each installer gains a `process.platform === "linux"` branch; the launchd path is untouched, so a macOS install behaves exactly as before. 352 insertions, 3 deletions, and each of those three is one line replaced by its platform-branching form. This is danielmiessler#1650 and danielmiessler#1653 redone in the shape the review asked for. Those two routed launchd's jobs through Pulse cron, which risked two schedulers owning one job on macOS. Here launchd keeps owning the job on darwin and systemd owns it on linux, so no install ever has two. Covered (11): BookmarkSweep, HealthSync, BlogDiscovery, CodexUpdate, CommitmentSweep, UsageAggregator, ConveyorRunner, ConveyorWatcher, DerivedSync, Conduit, ConduitInsight. WorkSweep is deliberately excluded — already ported. FOUR SHAPES, NOT ONE The jobs use four different launchd scheduling mechanisms, each needing its own systemd form: StartInterval N + RunAtLoad → .timer OnBootSec=2min, OnUnitActiveSec=N StartCalendarInterval H:M → .timer OnCalendar=*-*-* H:M:00 KeepAlive + ThrottleInterval → .service Restart=always, RestartSec=N WatchPaths [dirs] → .path PathModified= per directory Timers carry Persistent=true, the closest systemd has to launchd firing a missed calendar job on wake. Not identical: RunAtLoad fires on every load, Persistent only when the window was actually missed. For sweeps and syncs that are safe late and never early, that is the better direction. WHY A SHARED HELPER RATHER THAN UNIT TEMPLATES danielmiessler#1692 shipped a .service.template + .timer.template pair beside its plist, which is right for one job. Across eleven it would be ~20 near-identical template files, each able to drift from the installer that materializes it. So lib/SystemdUser.ts generates unit text from a small typed spec and each installer declares only what is its own: label, command, log path, schedule. Adding the twelfth job is a ten-line branch. WorkSweep could migrate onto the same helper later to collapse the two idioms; this PR does not touch it. A BUG THIS FOUND `systemctl --user` needs XDG_RUNTIME_DIR to reach the user bus, and a non-interactive shell does not set it — which is exactly how an installer runs from a script, a cron job, or a non-login SSH session. Without it every call fails with "Failed to connect to user scope bus ... $DBUS_SESSION_BUS_ADDRESS and $XDG_RUNTIME_DIR not defined" while the user manager is running fine. The helper now resolves /run/user/<uid> itself and passes it to every systemctl and loginctl call. Same class as the process.env.USER issue a review caught on danielmiessler#1692: a variable that is set in a terminal and absent everywhere else. install() also preflights `is-system-running` so a host with no user session (some containers) reports a diagnosis instead of a raw dbus error. loginctl enable-linger uses `id -un`, not process.env.USER, inheriting danielmiessler#1692's fix rather than repeating its bug.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Takes the advice on #1653 and builds on #1692: a
systemd --userbackend for every remaining launchd-only installer, in the shape you asked for.So this is #1650 and #1653 redone. Those routed launchd's jobs through Pulse cron, which risked two schedulers owning one job on macOS. Here launchd keeps owning the job on darwin and systemd owns it on linux, so no install ever has two.
352 insertions, 3 deletions across the eleven installers. Each of those three deletions is a single line replaced by its platform-branching form (
if (arg === "--uninstall") uninstall()in the two Conduit installers,function main()→async function main()in CommitmentSweep). Nine of eleven files have zero deletions. Every installer gains oneif (systemd.isLinux()) return linuxMain(arg);line and two functions below its existing code.Covered: BookmarkSweep, HealthSync, BlogDiscovery, CodexUpdate, CommitmentSweep, UsageAggregator, ConveyorRunner, ConveyorWatcher, DerivedSync, Conduit, ConduitInsight. WorkSweep is deliberately excluded — #1692 already ported it.
Four shapes, not one
The eleven jobs use four different launchd mechanisms, each needing its own systemd form:
StartInterval N+RunAtLoad.timerOnBootSec=2min,OnUnitActiveSec=NStartCalendarInterval H:M.timerOnCalendar=*-*-* H:M:00KeepAlive+ThrottleInterval.serviceRestart=always,RestartSec=NWatchPaths [dirs].pathPathModified=per dirTimers carry
Persistent=true, the closest systemd has to launchd firing a missed calendar job on wake. It is not identical, and the difference is worth stating:RunAtLoadfires on every load,Persistentonly when the window was genuinely missed. For sweeps and syncs that are safe late and never early, that is the better direction.Why a shared helper instead of unit templates
#1692 shipped a
.service.template+.timer.templatepair beside its plist. That is right for one job. Across eleven it becomes ~20 near-identical template files, each able to drift from the installer that materializes it. Solib/SystemdUser.tsgenerates the unit text from a small typed spec, and each installer declares only what is genuinely its own: label, command, log path, schedule. Adding the twelfth job is a ten-line branch rather than two new files.If you would rather have one idiom than two, WorkSweep could move onto the same helper in a follow-up. This PR does not touch it.
A bug this turned up
systemctl --userneedsXDG_RUNTIME_DIRto reach the user bus, and a non-interactive shell does not set it — which is exactly how an installer runs when driven from a script, a cron job, or a non-login SSH session. Without it every call fails:…while the user manager is running perfectly well. The helper now resolves
/run/user/<uid>itself and passes it to everysystemctlandloginctlcall. This is the same class as theprocess.env.USERissue a review caught on #1692: a variable that is set in a terminal and absent everywhere else.enable-lingerhere usesid -un, inheriting that fix rather than repeating the bug.install()also preflightsis-system-running, so a host with no user session (some containers) gets a diagnosis instead of a raw dbus error.Testing evidence
Verified on Linux (Ubuntu, systemd 259). Every command below was run with
XDG_RUNTIME_DIRandDBUS_SESSION_BUS_ADDRESSexplicitly unset, to prove the fix above.1. All eleven specs build for real (
--statusconstructs the spec —which bun, config reads, path resolution — and only reads systemd state):Note each shape resolves to the right primary unit:
.timerfor interval/calendar,.servicefor the KeepAlive daemons,.pathfor the watcher.2. systemd's own parser accepts all four shapes (
systemd-analyze --user verify, no warnings):3. A real end-to-end install, with the schedule confirmed by systemd (usage-aggregator, chosen because a 03:30 daily timer cannot fire mid-test):
03:30 matches the plist's
StartCalendarIntervalHour=3 Minute=30 exactly.4. Full install → status → uninstall cycle per shape, driven through the real helper with
/bin/trueandsleepso no actual LifeOS job runs:Everything was uninstalled afterwards;
systemctl --user list-timersshows 0 lifeos timers and no unit files remain.Not run: the darwin path, for lack of a macOS host. That is also the reason it is untouched — the three deletions above are the entire blast radius, and none changes what launchd does.