mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-16 13:49:58 +00:00
The auto-update feature was driven by two narrow Wails events (netbird:update:available and :progress) plus a SystemEvent-metadata iteration on the React side. Both surfaces had to know the daemon metadata schema (new_version_available, enforced, progress_window), and the frontend had no pull endpoint to seed its state on mount. Extract the state machine into a new client/ui/updater package, mirroring how i18n and preferences are split between domain logic and a thin services facade. The package owns the State type, the metadata-key parsing, the mutex-guarded Holder, and the single netbird:update:state event. services.Update keeps the daemon RPCs (Trigger, GetInstallerResult, Quit) and gains GetState as a Wails pull endpoint. Tray-side update behaviour moves out of tray.go into a dedicated trayUpdater (tray_update.go): owns its menu item, OS notification, click handler, and the /update window opener triggered by the daemon's progress_window:show. tray.go drops three callbacks and four fields, and reads hasUpdate through the updater. Frontend ClientVersionContext now seeds from Update.GetState() and subscribes to netbird:update:state; the status.events iteration and metadata-key string literals are gone. UpdateAvailableBanner renders only for the enforced && !installing branch and labels its action "Install now"; UpdateVersionCard splits the install vs. download branches by Enforced so the disabled flow routes to GitHub.
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
//go:build !android && !ios && !freebsd && !js
|
|
|
|
// Package updater carries the auto-update domain: the typed State the UI
|
|
// renders, the daemon-SystemEvent metadata schema, and the Holder that
|
|
// caches the latest state and broadcasts changes. Mirrors the layout of
|
|
// client/ui/i18n and client/ui/preferences — no Wails dependency, just an
|
|
// optional Emitter interface so callers can pass either the Wails event
|
|
// processor or a fake in tests.
|
|
package updater
|
|
|
|
import (
|
|
"sync"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/proto"
|
|
)
|
|
|
|
// EventStateChanged is the single Wails event the frontend and tray
|
|
// subscribe to. The payload is the full State snapshot, so consumers
|
|
// never need to combine multiple events to know what to render.
|
|
const EventStateChanged = "netbird:update:state"
|
|
|
|
// State is the typed snapshot of the daemon's update situation, covering
|
|
// the three branches the UI cares about:
|
|
//
|
|
// - Disabled / opt-in: Available=true, Enforced=false, Installing=false.
|
|
// Tray shows "Download latest", frontend shows a "Get installer" hint
|
|
// pointing at GitHub.
|
|
// - Enforced, user-driven: Available=true, Enforced=true, Installing=false.
|
|
// Tray shows "Install version X", frontend shows the install banner.
|
|
// - Forced, daemon already installing: Available=true, Enforced=true,
|
|
// Installing=true. Both surfaces show the install-in-progress UI.
|
|
//
|
|
// Installing is driven only by the daemon's progress_window:show event;
|
|
// a UI-side Update.Trigger() does not flip it. The frontend tracks its own
|
|
// "Trigger() in flight" state for the enforced flow.
|
|
type State struct {
|
|
Available bool `json:"available"`
|
|
Version string `json:"version"`
|
|
Enforced bool `json:"enforced"`
|
|
Installing bool `json:"installing"`
|
|
}
|
|
|
|
// Emitter is the dependency Holder needs to broadcast changes. The Wails
|
|
// app.Event processor satisfies this; tests pass nil or a fake. Same shape
|
|
// the preferences package uses, intentionally — both are "broadcast to the
|
|
// frontend" hooks with no other contract.
|
|
type Emitter interface {
|
|
Emit(name string, data ...any) bool
|
|
}
|
|
|
|
// Holder caches the latest update State and broadcasts changes. Fed by
|
|
// services.Peers, which forwards every daemon SystemEvent here via
|
|
// OnSystemEvent. The state is read by the Wails-bound services.Update
|
|
// facade (Get) and pushed to subscribers via the Emitter.
|
|
type Holder struct {
|
|
emitter Emitter
|
|
|
|
mu sync.Mutex
|
|
state State
|
|
}
|
|
|
|
// NewHolder constructs an empty-state Holder. The emitter is optional —
|
|
// pass nil in tests to skip the broadcast.
|
|
func NewHolder(emitter Emitter) *Holder {
|
|
return &Holder{emitter: emitter}
|
|
}
|
|
|
|
// Get returns a copy of the cached State. Used by the Wails facade so the
|
|
// frontend can pull the current value on mount before its push subscription
|
|
// has anything to deliver.
|
|
func (h *Holder) Get() State {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.state
|
|
}
|
|
|
|
// OnSystemEvent inspects the daemon's SystemEvent metadata for the three
|
|
// update-related keys (new_version_available, enforced, progress_window
|
|
// plus version) and folds the result into the cached state. Emits
|
|
// EventStateChanged only when the state actually changed, so subscribers
|
|
// do not see redundant pushes when the daemon repeats a snapshot.
|
|
//
|
|
// The metadata schema is owned here and nowhere else — neither Peers nor
|
|
// the tray nor the frontend reaches into ev.Metadata directly.
|
|
func (h *Holder) OnSystemEvent(ev *proto.SystemEvent) {
|
|
md := ev.GetMetadata()
|
|
if len(md) == 0 {
|
|
return
|
|
}
|
|
|
|
h.mu.Lock()
|
|
changed := false
|
|
if v, ok := md["new_version_available"]; ok {
|
|
_, enforced := md["enforced"]
|
|
if !h.state.Available || h.state.Version != v || h.state.Enforced != enforced {
|
|
h.state.Available = true
|
|
h.state.Version = v
|
|
h.state.Enforced = enforced
|
|
changed = true
|
|
}
|
|
}
|
|
if md["progress_window"] == "show" {
|
|
if !h.state.Installing {
|
|
h.state.Installing = true
|
|
changed = true
|
|
}
|
|
if v, ok := md["version"]; ok && v != "" && h.state.Version != v {
|
|
h.state.Version = v
|
|
h.state.Available = true
|
|
changed = true
|
|
}
|
|
}
|
|
snap := h.state
|
|
h.mu.Unlock()
|
|
|
|
if !changed {
|
|
return
|
|
}
|
|
log.Infof("update state: available=%v version=%q enforced=%v installing=%v",
|
|
snap.Available, snap.Version, snap.Enforced, snap.Installing)
|
|
if h.emitter != nil {
|
|
h.emitter.Emit(EventStateChanged, snap)
|
|
}
|
|
}
|