mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-17 12:09:58 +00:00
Detect fresh install from NetBird footprint instead of installer breadcrumb
Replace the installer-written breadcrumb discriminator with a GUI-side check. netbirdFootprintExists inspects the daemon config/state files (default.json, legacy config.json, state.json) under profilemanager's default config dir; combined with whether the UI preferences file already existed, this tells a genuinely fresh machine from an existing or upgrading user. Only the signed GUI, via Wails, ever enables launch-on-login, and a user's later manual disable is never overridden. The preferences store now exposes ExistedAtLoad and the --post-update flag is dropped.
This commit is contained in:
@@ -6,28 +6,21 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
"github.com/netbirdio/netbird/client/ui/preferences"
|
||||
"github.com/netbirdio/netbird/client/ui/services"
|
||||
)
|
||||
|
||||
// freshInstallBreadcrumbName is the marker file written by the platform
|
||||
// installers on a fresh install only, never on an upgrade. Its presence is
|
||||
// the update-safety gate for the autostart default: upgrading users never
|
||||
// have it, so an update can never trigger a login-item write.
|
||||
const freshInstallBreadcrumbName = ".fresh-install"
|
||||
|
||||
// autostartDefaultState carries the guard inputs of the one-time autostart
|
||||
// default decision so the decision itself stays a pure, testable function.
|
||||
type autostartDefaultState struct {
|
||||
supported bool
|
||||
mdmDisabled bool
|
||||
postUpdateRelaunch bool
|
||||
breadcrumbPresent bool
|
||||
supported bool
|
||||
mdmDisabled bool
|
||||
priorInstall bool
|
||||
}
|
||||
|
||||
// shouldEnableAutostartDefault applies the first-run guards in order and
|
||||
@@ -38,10 +31,8 @@ func shouldEnableAutostartDefault(s autostartDefaultState) (bool, string) {
|
||||
return false, "autostart not supported on this platform"
|
||||
case s.mdmDisabled:
|
||||
return false, "autostart disabled by MDM policy"
|
||||
case s.postUpdateRelaunch:
|
||||
return false, "post-update relaunch"
|
||||
case !s.breadcrumbPresent:
|
||||
return false, "no fresh-install breadcrumb"
|
||||
case s.priorInstall:
|
||||
return false, "existing NetBird installation"
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
@@ -57,45 +48,44 @@ func autostartDisabledByMDM(policy *mdm.Policy) bool {
|
||||
return !ok || disabled
|
||||
}
|
||||
|
||||
// freshInstallBreadcrumbPath returns the installer-written breadcrumb
|
||||
// location for the current platform, or "" when there is none.
|
||||
func freshInstallBreadcrumbPath() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Debugf("resolve executable path for fresh-install breadcrumb: %v", err)
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(filepath.Dir(exe), freshInstallBreadcrumbName)
|
||||
case "darwin":
|
||||
return filepath.Join("/Library/Application Support/NetBird", freshInstallBreadcrumbName)
|
||||
case "linux":
|
||||
return filepath.Join("/var/lib/netbird", freshInstallBreadcrumbName)
|
||||
// netbirdFootprintExists reports whether the machine already carries NetBird
|
||||
// daemon config or state, meaning this is not a genuinely fresh install. It is
|
||||
// the update-safety gate for the autostart default: upgrading users always
|
||||
// have a footprint, so an update can never trigger a login-item write.
|
||||
func netbirdFootprintExists() bool {
|
||||
candidates := []string{
|
||||
profilemanager.DefaultConfigPath,
|
||||
filepath.Join(profilemanager.DefaultConfigPathDir, "config.json"),
|
||||
filepath.Join(profilemanager.DefaultConfigPathDir, "state.json"),
|
||||
}
|
||||
return ""
|
||||
for _, path := range candidates {
|
||||
if path != "" && fileExists(path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// applyAutostartDefault runs the one-time launch-on-login default for fresh
|
||||
// installs. The autostartInitialized marker is persisted before any enable
|
||||
// attempt so a crash mid-flow degrades to "never enabled" instead of
|
||||
// applyAutostartDefault runs the one-time launch-on-login default for genuinely
|
||||
// fresh installs. The autostartInitialized marker is persisted before any
|
||||
// enable attempt so a crash mid-flow degrades to "never enabled" instead of
|
||||
// retrying login-item writes on every launch. A user's later disable in
|
||||
// Settings is never overridden: the marker guarantees at-most-once, ever.
|
||||
func applyAutostartDefault(ctx context.Context, autostart *services.Autostart, prefs *preferences.Store, postUpdateRelaunch bool) {
|
||||
func applyAutostartDefault(ctx context.Context, autostart *services.Autostart, prefs *preferences.Store, prefsFileExisted bool) {
|
||||
priorFootprint := netbirdFootprintExists() || prefsFileExisted
|
||||
|
||||
if prefs.Get().AutostartInitialized {
|
||||
return
|
||||
}
|
||||
if err := prefs.SetAutostartInitialized(true); err != nil {
|
||||
log.Warnf("failed to persist autostart marker, skipping autostart default: %v", err)
|
||||
log.Warnf("persist autostart marker, skipping autostart default: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
breadcrumb := freshInstallBreadcrumbPath()
|
||||
state := autostartDefaultState{
|
||||
supported: autostart.Supported(ctx),
|
||||
mdmDisabled: autostartDisabledByMDM(mdm.LoadPolicy()),
|
||||
postUpdateRelaunch: postUpdateRelaunch,
|
||||
breadcrumbPresent: breadcrumb != "" && fileExists(breadcrumb),
|
||||
supported: autostart.Supported(ctx),
|
||||
mdmDisabled: autostartDisabledByMDM(mdm.LoadPolicy()),
|
||||
priorInstall: priorFootprint,
|
||||
}
|
||||
enable, reason := shouldEnableAutostartDefault(state)
|
||||
if !enable {
|
||||
@@ -104,14 +94,10 @@ func applyAutostartDefault(ctx context.Context, autostart *services.Autostart, p
|
||||
}
|
||||
|
||||
if err := autostart.SetEnabled(ctx, true); err != nil {
|
||||
log.Warnf("failed to enable autostart on fresh install: %v", err)
|
||||
log.Warnf("enable autostart on fresh install: %v", err)
|
||||
return
|
||||
}
|
||||
log.Info("autostart enabled by default on fresh install")
|
||||
|
||||
if err := os.Remove(breadcrumb); err != nil {
|
||||
log.Debugf("failed to remove fresh-install breadcrumb %s: %v", breadcrumb, err)
|
||||
}
|
||||
}
|
||||
|
||||
// fileExists reports whether path exists.
|
||||
|
||||
@@ -80,7 +80,7 @@ func init() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
daemonAddr, userSetLogFile, postUpdate := parseFlagsAndInitLog()
|
||||
daemonAddr, userSetLogFile := parseFlagsAndInitLog()
|
||||
conn := NewConn(daemonAddr)
|
||||
|
||||
// Without --log-file, the GUI manages a gui-client.log that follows the
|
||||
@@ -198,8 +198,8 @@ func main() {
|
||||
// only reliable signal the user gets.
|
||||
go notifyIfDaemonOutdated(compat, notifier, localizer)
|
||||
// One-time launch-on-login default for fresh installs; gated by the
|
||||
// installer breadcrumb, MDM policy, and the persisted marker.
|
||||
go applyAutostartDefault(context.Background(), services.NewAutostart(app.Autostart), prefStore, postUpdate)
|
||||
// NetBird footprint check, MDM policy, and the persisted marker.
|
||||
go applyAutostartDefault(context.Background(), services.NewAutostart(app.Autostart), prefStore, prefStore.ExistedAtLoad())
|
||||
})
|
||||
|
||||
if err := app.Run(); err != nil {
|
||||
@@ -224,19 +224,17 @@ func requestNotificationAuthorization(notifier *notifications.NotificationServic
|
||||
}
|
||||
}
|
||||
|
||||
// parseFlagsAndInitLog returns the daemon gRPC address, userSetLogFile
|
||||
// (true when --log-file was passed), and postUpdate (true when the process
|
||||
// was relaunched by an installer/updater via --post-update). userSetLogFile
|
||||
// is the manual-override signal: true leaves logging alone, false lets the
|
||||
// GUI manage a daemon-driven gui-client.log. The flag default is empty (not
|
||||
// "console") so "no flag" and an explicit "--log-file console" stay
|
||||
// distinguishable; empty falls back to console for InitLog.
|
||||
func parseFlagsAndInitLog() (string, bool, bool) {
|
||||
// parseFlagsAndInitLog returns the daemon gRPC address and userSetLogFile
|
||||
// (true when --log-file was passed). userSetLogFile is the manual-override
|
||||
// signal: true leaves logging alone, false lets the GUI manage a
|
||||
// daemon-driven gui-client.log. The flag default is empty (not "console") so
|
||||
// "no flag" and an explicit "--log-file console" stay distinguishable; empty
|
||||
// falls back to console for InitLog.
|
||||
func parseFlagsAndInitLog() (string, bool) {
|
||||
daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path or tcp://host:port")
|
||||
logFiles := &stringList{}
|
||||
flag.Var(logFiles, "log-file", "Log destination. Repeat to log to multiple targets at once, e.g. `--log-file console --log-file Y:/netbird-ui.log`. Each value is one of: console, syslog, or a file path. File destinations are rotated by lumberjack (same as the daemon). Defaults to console. Passing any value disables the daemon-debug-driven gui-client.log.")
|
||||
logLevel := flag.String("log-level", "info", "Log level: trace|debug|info|warn|error.")
|
||||
postUpdate := flag.Bool("post-update", false, "Set by installer/updater relaunches; suppresses first-run defaults such as enabling autostart.")
|
||||
flag.Parse()
|
||||
|
||||
userSetLogFile := len(logFiles.values) > 0
|
||||
@@ -248,7 +246,7 @@ func parseFlagsAndInitLog() (string, bool, bool) {
|
||||
if err := util.InitLog(*logLevel, targets...); err != nil {
|
||||
log.Fatalf("init log: %v", err)
|
||||
}
|
||||
return *daemonAddr, userSetLogFile, *postUpdate
|
||||
return *daemonAddr, userSetLogFile
|
||||
}
|
||||
|
||||
// newApplication constructs the Wails application. onSecondInstance fires when
|
||||
|
||||
@@ -76,8 +76,9 @@ type Emitter interface {
|
||||
type Store struct {
|
||||
path string
|
||||
|
||||
mu sync.RWMutex
|
||||
current UIPreferences
|
||||
mu sync.RWMutex
|
||||
current UIPreferences
|
||||
existedAtLoad bool
|
||||
|
||||
subsMu sync.Mutex
|
||||
subs []chan UIPreferences
|
||||
@@ -231,13 +232,29 @@ func (s *Store) Subscribe() (<-chan UIPreferences, func()) {
|
||||
return ch, unsubscribe
|
||||
}
|
||||
|
||||
// ExistedAtLoad reports whether the backing preferences file was present on
|
||||
// disk when the store loaded. It distinguishes a user who ran a prior GUI
|
||||
// version from a brand-new OS user with no preferences yet.
|
||||
func (s *Store) ExistedAtLoad() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.existedAtLoad
|
||||
}
|
||||
|
||||
// load reads the file into current. A missing file is not an error (the
|
||||
// in-memory default stands); malformed contents return an error.
|
||||
func (s *Store) load() error {
|
||||
if _, err := os.Stat(s.path); errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
if _, err := os.Stat(s.path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("stat preferences: %w", err)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.existedAtLoad = true
|
||||
s.mu.Unlock()
|
||||
|
||||
var loaded UIPreferences
|
||||
if _, err := util.ReadJson(s.path, &loaded); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user