Repository navigation
fix(autostart): a launch agent left half-written reads as Open at log… - #1079
Open
TryWorld2026 wants to merge 1 commit into
Open
TryWorld2026 wants to merge 1 commit into
TryWorld2026 wants to merge 1 commit into
Conversation
…in off, not on The Mac's launch agent, the Linux .desktop and the Android boot script were written with a plain write, which empties the file before it fills it, so a crash or a full disk part way through left a record that is empty or half there. launchd refuses a job that names neither its own label nor a program, and the desktop starts no entry with no Exec, so magpie simply never opened at login. Enabled() on the Mac and on Linux asked only whether the file was there, so Settings kept showing the switch on; and Refresh(), which the app runs at every start, rewrites only a record its ProgramArguments regexp can still find a program in, which a half-written one does not have. Nothing repaired it and nothing said so, so the user was left believing a switch that did nothing. The four writes of these records now go through edit.WriteAtomic, which writes a temp file beside the record and renames it over: a write lands whole or not at all, and one cut short leaves the previous record, still loadable, where it was. No import cycle: internal/edit brings in only internal/steady and internal/filememo. enabled() on the Mac and on Linux now asks whether the record is one that starts magpie — the launch agent names both its label and its program, the desktop entry has both its Type and its Exec — instead of whether the file is there. A record a write cut short now reads as off, so the switch stops saying magpie opens at login when it does not, and the user sees the off that Refresh() cannot repair. A record magpie did not write is still left alone by Refresh() (TestRefreshOlderLaunchAgent), which is unchanged: refresh() rewrites what it can parse, and enabled() reports off for what it cannot. Windows' Run key is not touched here. TestSetThenTruncatedRecordReadsAsOff is new, once for the Mac (autostart_darwin_test.go) and once for Linux (autostart_other_test.go), where the record is written whole and then left as a write cut short leaves it: the head, stopping before the program it names, and the same record emptied. Against the old code, whose check was os.Stat, the Mac one fails with "a launch agent with no ProgramArguments reads as on" and the Linux one with "a desktop entry with no Exec reads as on", each on both records. With the change both pass, and TestSet, TestRefreshOlderLaunchAgent and TestLaunchAgentLetsTheRelaunchLive are unchanged. go vet, the windows, darwin and linux builds and test compiles, and gofmt on the seven files (LF-normalized first, since the worktree is CRLF) pass. Not run on a Mac or with plutil: this is a Windows box, so the darwin and linux behaviour is checked by GOOS=darwin and GOOS=linux builds plus their test compiles, and the two new predicates were run here against the bytes those platforms write from a scratch harness, which showed the old os.Stat check reading all three records as on. That harness is not committed.
TryWorld2026
force-pushed
the
fix/autostart-atomic-write
branch
from
October 7, 2026 01:00
8ebcf88 to
70e015a
Compare
This branch has not been deployed
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.
fix(autostart): a launch agent left half-written reads as Open at login off, not on
What is wrong
Two things about Open at login, on the three file-backed platforms.
A half-written record read as on.
enabled()wasos.Stat(record()) == nilon darwin and on the XDG platforms, so a launch agent or
.desktopthat acrash, a full disk or a kill left short — the header but no
Label/ProgramArguments, noType/Exec— read as on. launchd loads nojob that names neither its label nor a program, and the desktop starts no entry
that is not an application entry or names nothing to run, so such a record
refuses to load and magpie never opens at login however long the file sits
there. Settings said it would.
The writes were the kind that can leave exactly that file behind. Four
os.WriteFilecalls wrote the record in place, truncating first, so a crash ora kill mid-write left a truncated one — the very thing the read then reported as
on.
What changed
os.WriteFilecalls becomeedit.WriteAtomic, which writes a tempfile beside the record and renames it over. A file's mode is preserved, as
os.WriteFile's mode argument only ever applied at creation anyway, soAndroid's follow-up
Chmod 0o700still applies and an existing record keepsits mode. One nuance: a record being created for the first time on Android
goes through
0o644for the instant before thatChmod, where the old callcreated it at
0o700.enabled()reads the record and asks for the keys the platform needs beforeit will start anything:
<key>Label</key>with the label magpie writes, plusProgramArgumentsnaming the program, on darwin;^Type=Application$plus^Exec=on the XDG platforms. A record short of either reads as off, noton.
Windows's registry record has neither shape and is a separate change.
Semantic change (Autostart)
.desktopread as on, and the writesthat created one were not atomic.
write cannot leave a truncated record behind.
docs/subsystems/README.mdowns this subsystem; the autostartpackage is not documented in a page of its own, so there is no reference to
correct here. Implementation:
enabled,enableandrefreshininternal/autostart/autostart_darwin.go,autostart_other.goandautostart_android.go.Verification
Both new checks are named
TestSetThenTruncatedRecordReadsAsOff, one perplatform file. Neither runs on Windows — the darwin one is behind its filename
and the XDG one behind
!darwin && !windows && !android— so this box cannotexecute them as they stand. What I did instead: in a scratch build, with the
platform's own implementation selected and the other ones out of the way, the
logic under test is plain file-and-regex and runs anywhere, so it ran here.
PASS. Base code (os.Stat):FAILwitha launch agent with no ProgramArguments reads as onandan empty launch agent reads as on.other, branch code:PASS. Base code (os.Stat):FAILwitha desktop entry with no Exec reads as onandan empty desktop entry reads as on.The scratch build was a throwaway worktree, removed afterwards; neither this
branch nor any other was changed to make them run.
go veton the package and the three platformGOOSbuilds(
darwin,linux,windows) pass, as doesgo test -race ./internal/autostarton Windows; gofmt is clean on LF-normalized copies, since the worktree is CRLF
under
core.autocrlf.Windows box. What the two platforms do with a record that names no program is
stated from their documented behaviour, not from a record watched being
refused.
Not in this PR
Windows's Open at login has the same class of lie —
offreported successwhen it had done nothing, and a key it cannot read is not read as off — with a
different cause and a different fix. It is the companion PR,
fix/autostart-honest-errors.