Skip to content

Put the update in the window, and make it update the GUI - #57

Closed
YCistak wants to merge 3 commits into
feat/stop-listeningfrom
feat/gui-update
Closed

YCistak wants to merge 3 commits into
feat/stop-listeningfrom
feat/gui-update

Conversation

@YCistak

@YCistak YCistak commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Stacked on #56 — base is feat/stop-listening, so this diff is only its own work.

pylon update existed and nothing in the GUI reached it: installing a release meant knowing there was a terminal command for it. A new Hakkında tab offers it in two steps — check, then install — because replacing Pylon on disk is not a one-click action to take without first saying what is about to land.

The bigger half: the update never updated the GUI

The archive ships both binaries. selfupdate extracted only pylon. Every update left the interface behind at the old version, and nothing on screen said so — the GUI did not even know its own version, because the Makefile and the release workflow both passed -X main.version= to a variable pylon-ui never declared, and the linker dropped it in silence.

The GUI still cannot replace itself: on Windows a process cannot overwrite the binary it is running from, and on neither platform can it restart into a new one without losing its state. The daemon has neither problem with it — which is also why the button goes through IPC instead of doing the work in the window. macOS is excluded, where the GUI is a .app bundle and swapping one file inside it leaves a bundle disagreeing with itself.

The daemon is replaced first, on purpose. Two binaries cannot be swapped as one operation, so an interrupted update leaves a mismatched pair either way — and of the two, an old GUI against a new daemon is the harmless one. The wire protocol only grows, so an old GUI sends commands the new daemon still understands; the reverse produces "unknown command" against a daemon that is merely older. Hakkında shows both versions so that window is visible rather than mysterious.

Also fixed

  • 404 is not an error the user can act on. GitHub's "latest" endpoint answers 404 when nothing is published, and it skips prereleases — so this project's own state reaches it. It surfaced as check for updates: github returned 404 Not Found, which reads as a fault on the user's machine.
  • The GUI now carries a version, so the flag that was already being passed to it stops being discarded.

Verification

Apply was untestable: os.Executable() is the one input a test cannot supply. It is lifted into Apply, and everything downstream takes the path as an argument. Four tests drive a real signed release over httptest — a tar.gz nested under a versioned directory, SHA256SUMS signed with a throwaway ed25519 key:

Test Asserts
TestApplyReplacesBothBinaries daemon and GUI carry the new bytes
TestApplyWithoutAGUIInstalled a CLI-only install still updates, and no GUI appears where the user had none
TestApplyWhenTheArchiveHasNoGUI an older release is not an error; the installed GUI is untouched
TestApplyWritesNothingWhenTheChecksumIsWrong a tampered download leaves both files exactly as they were

The GUI swap was disabled to watch the first one fail before it was kept:

--- FAIL: TestApplyReplacesBothBinaries (0.00s)
    apply_test.go:211: GUI = "binary", want the new one — it was left at the old version

Live, against the running daemon:

version   : v0.1.0-alpha.1-12-g79bfcf6-dirty
update chk: {'ok': True, 'text': 'false\t\tHenüz güncellenecek yayınlanmış bir sürüm yok.'}

go test ./..., pylon-ui's tests, npm run check:i18n (7 languages, 174 keys) and make gui all pass.

⚠️ What is not verified: the install path against a real GitHub release, because there is none — the only tag is a prerelease, which is exactly what the 404 fix is about. The download, signature check and both swaps are proven against the fake release; the GitHub end of it is not. Clicking the buttons in the built window is also unverified (the same window-focus limitation as #55/#56).

🤖 Generated with Claude Code

`pylon update` existed and nothing in the GUI reached it, so installing a
release meant knowing there was a terminal command for it. A new Hakkında tab
offers it in two steps — check, then install — because replacing Pylon on disk
is not something to do on one click without first saying what is about to land.

The bigger half is that the update never updated the GUI. The archive ships
both binaries; selfupdate extracted only `pylon`. Every update left the
interface behind at the old version, and nothing on screen said so — the GUI
did not even know its own version, because the Makefile passed
-X main.version= to a variable pylon-ui never declared and the linker dropped
it in silence.

The GUI still cannot replace itself: on Windows a process cannot overwrite the
binary it is running from, and on neither platform can it restart into a new
one without losing its state. The daemon has neither problem with it, so the
daemon does both — which is also why the button goes through IPC rather than
doing the work in the window. macOS is left out, where the GUI is a .app
bundle and swapping one file inside it leaves a bundle disagreeing with itself
about its version.

The daemon is replaced first, on purpose. Two binaries cannot be swapped as one
operation, so an interrupted update leaves a mismatched pair either way, and of
the two an old GUI against a new daemon is the harmless one: the wire protocol
only grows, so old commands still resolve. The reverse produces "unknown
command" against a daemon that is merely older. Hakkında shows both versions so
that window is visible rather than mysterious.

Also fixed: GitHub's "latest" endpoint answers 404 when nothing is published,
and skips prereleases — so this project's own state reaches it. That surfaced
as "check for updates: github returned 404 Not Found", which reads as a fault
on the user's machine instead of the project not having released yet.

Apply is now testable: os.Executable() is the one input a test cannot supply,
so it is lifted into Apply and everything downstream takes the path as an
argument. Four tests drive a real signed release over httptest — both binaries
replaced, a CLI-only install, an older archive with no GUI in it, and a
tampered download that must leave both files untouched. The GUI swap was
disabled to watch the first one fail before it was kept.

Verified live: `version` answers, `update check` returns
"false\t\tHenüz güncellenecek yayınlanmış bir sürüm yok." instead of the 404.
Both were the tests' fault, not the code's, and both are the same class of
mistake this project has hit before: a test written on Linux that assumes
Linux.

  - Windows: t.TempDir() hands back an 8.3 short path (RUNNER~1), and
    guiBeside canonicalises it — the same EvalSymlinks call that makes an
    install through a symlinked ~/.local/bin land on the real file. The test
    compared against the path it had built rather than the resolved one.

  - macOS: guiName() is empty there, and buildArchive was handed it
    unconditionally, writing a tar header ending in a slash. The test failed
    on the archive it had built itself.
filepath.Join(dir, "") is dir, and guiName() is empty on macOS — so
"assert no GUI appeared" was asserting that the temp directory did not
exist. Guarded rather than skipped: the daemon half of that test is worth
running everywhere.

Verified by making guiName() return "" locally, which is exactly the macOS
condition, and running the package: it passes both ways now.
@YCistak
YCistak deleted the branch feat/stop-listening August 13, 2026 19:26
@YCistak YCistak closed this Aug 13, 2026
@YCistak

YCistak commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Closed automatically when the base branch was deleted on #56's merge — a closed PR cannot be reopened or retargeted. Continues as #60, same branch and commits, based on master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant