Skip to content

linux,macos: start-at-login adapters behind the startup seam (#215) - #672

Merged
rowkav09 merged 5 commits into
mainfrom
feat/215-posix-autostart
Sep 27, 2026
Merged

rowkav09 merged 5 commits into
mainfrom
feat/215-posix-autostart

Conversation

@rowkav09

Copy link
Copy Markdown
Member

Slice A of the #215 breakdown (scoping comment). Sequencing note: this is main-branch code, not a platform release, so it does not jump the issue's "blocked behind the Windows v0.2.0 testable release" note.

What

  • src/linux-startup.js - XDG autostart .desktop entry ($XDG_CONFIG_HOME/autostart or ~/.config/autostart), with desktop-entry-spec quoting/escaping for Exec. XDG only; no systemd user unit (flagged as Rowan's call in the breakdown).
  • src/macos-startup.js - per-user LaunchAgent ~/Library/LaunchAgents/dev.rowkav.nowplaying.plist, XML-escaped; the written plist passes plutil -lint on macOS (test runs in the macOS CI job).
  • Both mirror the windows-startup contract: status() / isEnabled() / setEnabled(), a missing file is disabled (not broken), an entry pointing at a moved binary reports broken and re-enabling repairs it. Like the Windows side, they read only their own named file, so no arbitrary paths leave the Settings API.
  • Enabling takes effect at the next login, matching the Windows Startup shortcut; no launchctl bootstrap, so this also works from a headless shell.
  • scripts/nowplaying.js wires them into startAppFromConfig, so the existing "Launch app on system startup" settings control (which renders from startup.available) now works on Linux and macOS. Entry points at this same node scripts/nowplaying.js start command; packaged builds (slice B) will pass their bundle path instead.

Tests

12 new cases: dir resolution (XDG rules, HOME required), missing/disabled, enable -> status -> disable round trips with content assertions, stale-entry broken/repair, quoting/escaping (spaces, $, XML entities), input validation, and plutil -lint on macOS. Full suite: 1145 pass locally (including the labeler rules for the new files).

Slice A of the platform breakdown. src/linux-startup.js manages an XDG
autostart .desktop entry (XDG_CONFIG_HOME/autostart or ~/.config/
autostart, spec quoting and escaping for Exec); src/macos-startup.js
manages a per-user LaunchAgent (~/Library/LaunchAgents/
dev.rowkav.nowplaying.plist, XML-escaped, plutil-linted on macOS CI).
Both mirror the windows-startup contract - status / isEnabled /
setEnabled, missing file is disabled, a stale entry pointing elsewhere
is broken and re-enabling repairs it - and both read only their own
named file, so no arbitrary paths leave the Settings API. Enabling
takes effect at the next login, matching the Windows shortcut (no
launchctl bootstrap, so this also works from a headless shell).
scripts/nowplaying.js wires them in, so the existing 'Launch app on
system startup' settings control now works on Linux and macOS.

Sequencing: this is main-branch code, not a platform release, so it
does not jump the issue's 'blocked behind the Windows v0.2.0 testable
release' note.
@rowkav09

Copy link
Copy Markdown
Member Author

CHANGES REQUESTED at exact head c8e60121cfd48e87657e9cf8bc7b6341319e4063 (a push resets this verdict). I found two status false-positives in the new autostart adapters:

  1. macOS status() checks only the first ProgramArguments string (the Node executable), not the script path or start argument. I enabled a plist for /usr/bin/node /good/nowplaying.js start, changed only the script string to /other/nowplaying.js, and status() still returned { enabled: true, broken: false }. A moved source checkout would be reported healthy even though launchd starts the old path. Compare the entire expected command vector (and ideally Label/RunAtLoad), not just the executable; add a test for a changed script/argument.
  2. Linux status() checks only Exec=. After enabling, I added Hidden=true to the .desktop entry; status still returned { enabled: true, broken: false }, although XDG Hidden disables that autostart entry. Check effective disabled keys (Hidden=true, and X-GNOME-Autostart-enabled=false if supported), plus a regression test.

Targeted Linux/macOS adapter tests pass 12/12 with the macOS-only plutil lint skipped on Linux; the cases above are not covered. Diff check clean. Sole commit author/committer is rowkav09 <rowkav0809@highgateschool.org.uk>, no extra attribution. No merge or push from this review.

@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.23529% with 38 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
scripts/nowplaying.js 66.33% 34 Missing ⚠️
src/linux-startup.js 98.16% 2 Missing ⚠️
src/macos-startup.js 98.23% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

Review rework on c8e6012:

- macOS: status compared only the first ProgramArguments string, so an
  agent whose script or arguments were edited still read enabled. It now
  compares the whole command vector (XML-unescaped), and regression tests
  cover an edited script path and a dropped argument
- Linux: an entry with Hidden=true or X-GNOME-Autostart-enabled=false is
  disabled by the desktop even though the file matches, so status now
  reports broken (settings shows off, re-enabling repairs the entry),
  with regression tests for both keys
@rowkav09

Copy link
Copy Markdown
Member Author

Rework on the review, new head 3663391:

  1. macOS full command vector: status now compares every
    ProgramArguments string (XML-unescaped) against the expected
    [programPath, ...arguments], so your repro - enable with
    /usr/bin/node /good/nowplaying.js start, edit the script to
    /other/nowplaying.js - reports {enabled: false, broken: true}.
    Regression tests cover the edited script path and a dropped argument;
    re-enabling repairs the agent.
  2. Linux disable keys: Hidden=true (XDG "as if deleted") and
    X-GNOME-Autostart-enabled=false now report {enabled: false,
    broken: true} despite the matching file, so the Settings toggle shows
    off and re-enabling repairs the entry. Regression tests for both keys,
    including your append-Hidden=true repro.

16/16 startup tests pass; full suite 1149 pass locally.

@rowkav09

Copy link
Copy Markdown
Member Author

CHANGES STILL REQUESTED at exact head 36633916f7066c307d78075dedb7332cc07c3ecb (a push resets this verdict). Both earlier reproductions are fixed: the edited macOS script path and Linux Hidden=true now return { enabled: false, broken: true }; focused startup tests pass 16/16 (macOS plutil lint skipped on Linux), and diff check is clean.

One related status false-positive remains: macos-startup.status() still matches only ProgramArguments. I enabled a plist for /usr/bin/node /good/nowplaying.js start, changed <key>RunAtLoad</key><true/> to <false/>, and it still returned { enabled: true, broken: false }. Launchd will not run this agent at login. Please validate RunAtLoad is true (and the expected Label) before reporting enabled, and add a regression for an inactive plist. The prior command-vector and Linux disabled-key fixes should remain. Both commits are rowkav09 <rowkav0809@highgateschool.org.uk> author/committer without extra attribution. No merge/push.

Review rework on 3663391: a LaunchAgent whose RunAtLoad is flipped to
false (or dropped) cannot start at login, and an edited Label breaks the
agent's identity with launchd - but status read both as enabled. Status
now requires the full command vector, the expected Label and
RunAtLoad=true; regression tests cover the flip, the missing key and
the edited Label.
@rowkav09

Copy link
Copy Markdown
Member Author

Rework, new head ffa46f6: status now requires the full command
vector AND the expected Label AND RunAtLoad=. Your RunAtLoad
true->false flip now reports {enabled: false, broken: true}; the missing
RunAtLoad key and an edited Label report broken too. Regression tests for
all three plus re-enable repair of the flip. 11/11 macOS startup tests,
full suite 1152 pass locally.

@rowkav09

Copy link
Copy Markdown
Member Author

CHANGES STILL REQUESTED at exact head ffa46f681280ca8a24c01560ba9464455b4944eb (a push resets this verdict). The three macOS fixes now pass: full command vector, Label, and RunAtLoad=true. I repeated the RunAtLoad=false reproduction; it reports broken and re-enabling repairs it. Focused adapter tests pass 19/19 (macOS plutil lint skipped on Linux); diff check clean.

A Linux Exec encoding defect remains. The freedesktop Desktop Entry specification says literal % in Exec must be written as %% because % introduces a field code: https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html . I enabled an adapter with script path /home/u/100% done/nowplaying.js; it wrote Exec=/usr/bin/node "/home/u/100% done/nowplaying.js" start and status() returned enabled:true/broken:false. That entry does not preserve the literal path under the field-code rules (an unrecognized field code makes it invalid). Escape % to %% in every Exec part and add a round-trip test for a path containing percent. The existing macOS and Linux status fixes should remain. Three commits have rowkav09 as author/committer with no extra attribution. No merge/push.

Review rework on ffa46f6: the XDG desktop entry spec requires a literal
% as %% (single percent signs start field codes like %f and %U that the
launcher expands or rejects). Exec parts now double % on write; the
status comparison stays byte-for-byte against the escaped form, so a
hand-edited single % reads as broken. Tests cover the 100% done path
round trip and the invalid single-% case.
@rowkav09

Copy link
Copy Markdown
Member Author

Rework, new head 5e9ef05: Exec parts now double literal % on write
(%% per the desktop entry spec, so 100% done paths can't be read as
field codes). Status compares byte-for-byte against the escaped form, so
a hand-edited single % reports broken. Tests: the /home/u/100%
done/nowplaying.js round trip and the invalid bare-% case. 10/10 Linux
startup tests, full suite 1154 pass locally.

@rowkav09

Copy link
Copy Markdown
Member Author

CHANGES STILL REQUESTED at exact head 5e9ef053bfd123c956e48fa96f4611819b700c33 (a push resets this verdict). The literal-percent fix passes: an Exec part containing 100% now writes 100%%, and the focused adapter tests pass 21/21 (macOS plutil lint skipped on Linux). Diff check clean.

I found another desktop-entry reserved character omitted from quoteExecArg: semicolon (;). With script path /home/u/a;b/nowplaying.js, setEnabled(true) writes Exec=/usr/bin/node /home/u/a;b/nowplaying.js start, then status() claims enabled:true. Running desktop-file-validate on that generated file fails: contains a reserved character ';' outside of a quote. The freedesktop Exec spec lists semicolon among reserved characters: https://specifications.freedesktop.org/desktop-entry/latest/exec-variables.html . Add ; to the quote-detection set and a test using desktop-file-validate where available, including a semicolon path. This is the same load-bearing promise: an entry reported enabled must be launchable by the desktop. All four commits are rowkav09 author/committer without extra attribution. No merge/push.

Review rework on 5e9ef05: the desktop entry spec reserves ; alongside
the other shell punctuation, so an unquoted /home/u/a;b path produced an
entry desktop-file-validate rejects. ; is now in the quoting class, and
a validator-backed test asserts the written entry passes
desktop-file-validate where the tool is installed (skips elsewhere).
@rowkav09

Copy link
Copy Markdown
Member Author

Rework, new head f28dbd9: semicolon added to the Exec quoting
class - your /home/u/a;b/nowplaying.js repro now writes
Exec=/usr/bin/node "/home/u/a;b/nowplaying.js" start. New
validator-backed test: the written entry (with ; and % and spaces in the
path) passes the real desktop-file-validate where installed (verified
locally: it rejected the unquoted form, passes the quoted form); skips
cleanly where the tool is absent. 12/12 Linux startup tests, full suite
1157 pass locally.

@rowkav09

Copy link
Copy Markdown
Member Author

APPROVE at exact head f28dbd9ef3195f7d8badc1eabeaa3dbab2924b04 (a push resets this verdict). The earlier status false-positives are fixed: macOS compares the full command vector, Label and RunAtLoad; Linux sees disabled keys. The XDG Exec writer now doubles literal % and quotes reserved ;. I reran the Linux and macOS adapter suites (23 pass, macOS-only plutil lint skipped on Linux) and independently generated an entry with ;, % and spaces: Exec=/usr/bin/node "/home/u/a;b/100%% done/now playing.js" start; desktop-file-validate accepted it and status() reported enabled. Diff check clean. All five commits are authored and committed by rowkav09 <rowkav0809@highgateschool.org.uk> with no extra attribution trailers. This is code approval, not a macOS launchd E2E, packaged release, hosted-CI or merge verdict. No merge/push.

@rowkav09
rowkav09 merged commit 5ffbfe7 into main Sep 27, 2026
14 checks passed
@rowkav09
rowkav09 deleted the feat/215-posix-autostart branch September 27, 2026 18:57
@github-project-automation github-project-automation Bot moved this from Backlog to Done in nowplaying Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant