Skip to content

feat(updater): hot update for Windows NSIS installs - #720

Merged
kdroidFilter merged 2 commits into
nucleus-2.6from
feat/windows-hot-update
Sep 25, 2026
Merged

kdroidFilter merged 2 commits into
nucleus-2.6from
feat/windows-hot-update

Conversation

@kdroidFilter

Copy link
Copy Markdown
Collaborator

Summary

On Windows, installAndRestart used to close the app, run the NSIS installer and relaunch it — about 13-15 s with nothing on screen. The app now stays visible for the whole update.

  • Versioned layout (plugin, every NSIS installer of a JVM app): App.exe + app\App.cfg stay at the install root, the runtime and the app move to versions\<version>\. The .cfg points there with app.runtime / $ROOTDIR, so a new version installs next to the running one without touching a file it holds open.
  • Install while running: the running launcher is renamed and copied back (a running exe can be renamed, not overwritten; shortcuts keep working), and the installer runs with NUCLEUS_HOT_UPDATE=1. Generated NSIS hooks (customCheckAppRunning, customRemoveFiles) then neither kill the app nor delete its files; without the variable they reproduce electron-builder's defaults.
  • Handoff: the old version releases the single-instance lock, launches the new one, and exits once TaoWindow reports the new version's first presented frame (UpdateHandoff.signalReady()). The new version then deletes the retired version and launcher (rename-then-delete, so a version still in use is kept).
  • Failure handling: if the hot path cannot start, the classic update runs. If the installer fails, the app keeps running (no close/reopen loop). If a non-hot installer closes the app, a guard relaunches it, but not when the user quit it. Per-machine installs (non-writable Program Files) use the classic update.
  • Multi-instance (Chromium's model):
    • installs are serialized by a cross-process lock;
    • other instances learn about an installed update through pendingRestartVersion without downloading anything;
    • they switch with restartToInstalledVersion(relaunchArguments);
    • relaunchArguments is explicit because replaying the command line would resend the autostart marker or a deep link.
  • Fixes found along the way:
    • update scripts are now written with a UTF-8 BOM. Windows PowerShell 5.1 read them as ANSI, so updates silently failed for accented profile paths (C:\Users\Hélène\…), classic path included.
    • consumeUpdateEvent / wasJustUpdated no longer report an update whose install failed.

Public API (additive): UpdateHandoff, VersionedInstall, SingleInstanceManager.releaseForHandoff(), NucleusUpdater.installAndRestart(file, relaunchArguments), pendingRestartVersion, restartToInstalledVersion().

Not covered: GraalVM native images (no .cfg indirection, would need a stub launcher), MSI and portable builds. They keep the classic update.

Test plan

  • Unit tests: UpdateHandoffTest, WindowsHotUpdateTest, WindowsHotUpdateMultiInstanceTest, WindowsHotUpdateLayoutTest, UpdateEventTest
  • ktlint, detekt, apiCheck on the touched modules
  • E2E on real NSIS installers (scripts/windows-hot-update-e2e.ps1 + examples/hot-update-demo), sampling visible windows and the on-screen pixel every ~18 ms:
    • update: 0 ms gap, retired version and launcher cleaned up
    • relaunch-during-install: launcher starts fine mid-install, one window left
    • close-during-install: app not relaunched, install completes, next start runs the new version
    • failing-installer: app stays on its version, same process, no gap
    • stale-target-dir
    • two-instances: installs serialized, each instance comes back with its own document
    • notify-other-instance: second instance downloads nothing, restarts through pendingRestartVersion
    • chained 1.0.0 → 1.1.0 → 1.2.0, 0 ms gap on both hops
    • install dir with spaces, apostrophe and accents
    • migration from a pre-hot (flat) install: first hop classic, next hop hot, no flat leftovers
    • classic mode control: ~15 s gap
    • Add/Remove Programs version and full uninstall
  • NativeLibraryLoaderTest (5) and ElectronBuilderRpmConfigTest (2) fail on Windows. They fail identically on the base commit, so they are unrelated to this PR.

The app no longer disappears while it updates on Windows. NSIS installs of
JVM apps are laid out as App.exe + app\App.cfg at the root and
versions\<version>\{app,runtime}, so installAndRestart installs the new
version next to the running one, launches it, and exits once the new
version's first window is on screen.

- plugin: versioned app image layout and NSIS hooks that neither kill the
  running app nor delete its files when NUCLEUS_HOT_UPDATE=1
- updater: install while running, handoff on the new version's first frame,
  cleanup of retired versions, classic fallback when the hot path cannot start
- multi-instance: cross-process install lock, pendingRestartVersion /
  restartToInstalledVersion, explicit relaunchArguments
- fix: PowerShell update scripts are written with a BOM, so updates work for
  accented profile paths (classic path included)
- fix: the post-update event is only reported when its target version runs
- E2E: examples/hot-update-demo + scripts/windows-hot-update-e2e.ps1
…ot-update

# Conflicts:
#	plugin-build/plugin/src/main/kotlin/dev/nucleusframework/desktop/application/tasks/AbstractElectronBuilderPackageTask.kt
@kdroidFilter
kdroidFilter merged commit 0c5c92d into nucleus-2.6 Sep 25, 2026
8 checks passed
kdroidFilter added a commit to NucleusFramework/nucleus-website that referenced this pull request Sep 25, 2026
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