package app import ( "errors" "fmt" "log" "os" "time" ) func getVPNLocationsSnapshot(force bool) vpnLocationsSnapshot { vpnLocationCache.ensureLoaded() vpnLocationCache.maybeStartRefresh(force) return vpnLocationCache.snapshot(time.Now()) } func (s *vpnLocationsStore) ensureLoaded() { s.mu.Lock() if s.loaded { s.mu.Unlock() return } s.loaded = true s.mu.Unlock() locs, updatedAt, err := loadVPNLocationsCache(vpnLocationsCachePath) if err != nil { if !errors.Is(err, os.ErrNotExist) { log.Printf("vpn locations cache load warning: %v", err) } return } s.mu.Lock() if len(locs) > 0 { s.locations = cloneVPNLocations(locs) } if !updatedAt.IsZero() { s.swr.setUpdatedAt(updatedAt) } s.mu.Unlock() } func (s *vpnLocationsStore) maybeStartRefresh(force bool) { now := time.Now() s.mu.Lock() if !s.swr.beginRefresh(now, force, len(s.locations) > 0) { s.mu.Unlock() return } s.mu.Unlock() go s.refresh() } func (s *vpnLocationsStore) snapshot(now time.Time) vpnLocationsSnapshot { s.mu.Lock() defer s.mu.Unlock() meta := s.swr.snapshot(now) out := vpnLocationsSnapshot{ Locations: cloneVPNLocations(s.locations), UpdatedAt: meta.UpdatedAt, Stale: meta.Stale, RefreshInProgress: meta.RefreshInProgress, LastError: meta.LastError, NextRetryAt: meta.NextRetryAt, } return out } func (s *vpnLocationsStore) refresh() { start := time.Now() stdout, _, exitCode, err := runCommandTimeout(vpnLocationsCommandTimeout, adgvpnCLI, "list-locations") if err != nil || exitCode != 0 { s.finishError(fmt.Sprintf("list-locations failed: err=%v exit=%d", err, exitCode), time.Now()) log.Printf("vpn locations refresh failed in %s: exit=%d err=%v", time.Since(start), exitCode, err) return } locs, err := parseVPNLocationsOutput(stdout) if err != nil { s.finishError(fmt.Sprintf("list-locations parse error: %v", err), time.Now()) log.Printf("vpn locations parse failed in %s: %v", time.Since(start), err) return } now := time.Now() changed, snapshot := s.finishSuccess(locs, now) if err := saveVPNLocationsCache(vpnLocationsCachePath, snapshot.Locations, now); err != nil { log.Printf("vpn locations cache save warning: %v", err) } events.push("vpn_locations_changed", map[string]any{ "ok": true, "count": len(snapshot.Locations), "changed": changed, "updated_at": snapshot.UpdatedAt, }) log.Printf("vpn locations refresh ok in %s: count=%d changed=%v", time.Since(start), len(snapshot.Locations), changed) } func (s *vpnLocationsStore) finishError(msg string, now time.Time) { s.mu.Lock() defer s.mu.Unlock() s.swr.finishError(msg, now) meta := s.swr.snapshot(now) events.push("vpn_locations_changed", map[string]any{ "ok": false, "error": meta.LastError, "next_retry_at": meta.NextRetryAt, "cached_count": len(s.locations), }) } func (s *vpnLocationsStore) finishSuccess(locs []vpnLocationItem, now time.Time) (bool, vpnLocationsSnapshot) { s.mu.Lock() defer s.mu.Unlock() changed := !equalVPNLocations(s.locations, locs) s.locations = cloneVPNLocations(locs) s.swr.finishSuccess(now) meta := s.swr.snapshot(now) snap := vpnLocationsSnapshot{ Locations: cloneVPNLocations(s.locations), UpdatedAt: meta.UpdatedAt, Stale: meta.Stale, RefreshInProgress: meta.RefreshInProgress, LastError: meta.LastError, NextRetryAt: meta.NextRetryAt, } return changed, snap }