aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pkg/api/api0/api.go7
-rw-r--r--pkg/api/api0/client.go129
-rw-r--r--pkg/api/api0/metrics.go16
-rw-r--r--pkg/atlas/server.go52
-rw-r--r--pkg/eax/eax.go145
-rw-r--r--pkg/eax/updatemgr.go180
6 files changed, 7 insertions, 522 deletions
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go
index 355deef..2fc1347 100644
--- a/pkg/api/api0/api.go
+++ b/pkg/api/api0/api.go
@@ -27,7 +27,6 @@ import (
"github.com/klauspost/compress/gzip"
"github.com/pg9182/ip2x"
- "github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/metricsx"
"github.com/r2northstar/atlas/pkg/nspkt"
"github.com/rs/zerolog/hlog"
@@ -48,12 +47,6 @@ type Handler struct {
// NSPkt handles connectionless packets. It must be non-nil.
NSPkt *nspkt.Listener
- // UsernameSource configures the source to use for usernames.
- UsernameSource UsernameSource
-
- // EAXClient makes requests to the EAX API.
- EAXClient *eax.Client
-
// CleanBadWords is used to filter bad words from server names and
// descriptions. If not provided, words will not be filtered.
CleanBadWords func(s string) string
diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go
index b718e17..1cb5cf3 100644
--- a/pkg/api/api0/client.go
+++ b/pkg/api/api0/client.go
@@ -12,34 +12,11 @@ import (
"time"
"github.com/r2northstar/atlas/pkg/api/api0/api0gameserver"
- "github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/pdata"
"github.com/r2northstar/atlas/pkg/stryder"
"github.com/rs/zerolog/hlog"
)
-// UsernameSource determines where to get player in-game usernames from.
-type UsernameSource string
-
-const (
- // Don't get usernames.
- UsernameSourceNone UsernameSource = ""
-
- // Get the username from EAX.
- UsernameSourceEAX UsernameSource = "eax"
-
- // Get the username from Stryder (available since October 2, 2023). Note
- // that this source only returns usernames for valid tokens.
- UsernameSourceStryder UsernameSource = "stryder"
-
- // Get the username from Stryder, but fall back to EAX on missing/failure.
- UsernameSourceStryderEAX UsernameSource = "stryder-eax"
-
- // Get the username from Stryder, but also check EAX and warn if it's
- // different.
- UsernameSourceStryderEAXDebug UsernameSource = "stryder-eax-debug"
-)
-
type MainMenuPromos struct {
NewInfo MainMenuPromosNew `json:"newInfo"`
LargeButton MainMenuPromosButtonLarge `json:"largeButton"`
@@ -214,14 +191,7 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request)
}
}
}
-
- select {
- case <-r.Context().Done(): // check if the request was canceled to avoid making unnecessary requests
- return
- default:
- }
-
- username := h.lookupUsername(r, uid, stryderRes)
+ username, _ := h.lookupUsername(r, uid, stryderRes)
select {
case <-r.Context().Done(): // check if the request was canceled to avoid making unnecessary requests
@@ -298,99 +268,10 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request)
})
}
-// lookupUsername gets the username for uid according to the configured
-// UsernameSource, returning an empty string if not found or on error.
-func (h *Handler) lookupUsername(r *http.Request, uid uint64, stryderRes []byte) (username string) {
- switch h.UsernameSource {
- case UsernameSourceNone:
- break
- case UsernameSourceEAX:
- username, _ = h.lookupUsernameEAX(r, uid)
- case UsernameSourceStryder:
- username, _ = h.lookupUsernameStryder(r, uid, stryderRes)
- case UsernameSourceStryderEAX:
- username, _ = h.lookupUsernameStryder(r, uid, stryderRes)
- if username == "" {
- if eaxUsername, ok := h.lookupUsernameEAX(r, uid); ok {
- username = eaxUsername
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("eax_username", eaxUsername).
- Msgf("failed to get username from stryder, but got it from eax")
- }
- }
- case UsernameSourceStryderEAXDebug:
- username, _ = h.lookupUsernameStryder(r, uid, stryderRes)
- if eaxUsername, ok := h.lookupUsernameEAX(r, uid); ok {
- if eaxUsername != username {
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("stryder_username", username).
- Str("eax_username", eaxUsername).
- Msgf("got username from stryder and eax, but they don't match; using the stryder one")
- }
- } else {
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("stryder_username", username).
- Msgf("got username from stryder, but failed to get username from eax")
- }
- default:
- hlog.FromRequest(r).Error().
- Msgf("unknown username source %q", h.UsernameSource)
- }
- return
-}
-
-// lookupUsernameEAX gets the username for uid from the EAX API, returning an
-// empty string if a username does not exist for the uid, and false on error.
-func (h *Handler) lookupUsernameEAX(r *http.Request, uid uint64) (username string, ok bool) {
- select {
- case <-r.Context().Done(): // check if the request was canceled to avoid polluting the metrics
- return
- default:
- }
- if h.EAXClient == nil {
- hlog.FromRequest(r).Error().
- Str("username_source", "eax").
- Msgf("no eax client available for username lookup")
- return
- }
- eaxStart := time.Now()
- if p, err := h.EAXClient.PlayerIDByPD(r.Context(), uid); err == nil {
- if p != nil {
- username = p.DisplayName
- h.m().client_originauth_eax_username_lookup_calls_total.success.Inc()
- } else {
- hlog.FromRequest(r).Warn().
- Err(err).
- Uint64("uid", uid).
- Str("username_source", "eax").
- Msgf("no eax username found for uid")
- h.m().client_originauth_eax_username_lookup_calls_total.notfound.Inc()
- }
- ok = true
- } else if errors.Is(err, eax.ErrVersionRequired) || errors.Is(err, eax.ErrAutoUpdateBackoff) {
- hlog.FromRequest(r).Error().
- Err(err).
- Str("username_source", "eax").
- Msgf("eax update check failure")
- h.m().client_originauth_eax_username_lookup_calls_total.fail_update_check.Inc()
- } else if !errors.Is(err, context.Canceled) {
- hlog.FromRequest(r).Error().
- Err(err).
- Str("username_source", "eax").
- Msgf("failed to get eax player info")
- h.m().client_originauth_eax_username_lookup_calls_total.fail_other_error.Inc()
- }
- h.m().client_originauth_eax_username_lookup_duration_seconds.UpdateDuration(eaxStart)
- return
-}
-
-// lookupUsernameStryder gets the username for uid from the Stryder response,
-// returning an empty string if the username is empty, or false if the
-// username is not present in the response or the response is invalid.
-func (h *Handler) lookupUsernameStryder(r *http.Request, uid uint64, res []byte) (username string, ok bool) {
+// lookupUsername gets the username for uid from the Stryder response, returning
+// an empty string if the username is empty, or false if the username is not
+// present in the response or the response is invalid.
+func (h *Handler) lookupUsername(r *http.Request, uid uint64, res []byte) (username string, ok bool) {
select {
case <-r.Context().Done(): // check if the request was canceled to avoid polluting the metrics
return
diff --git a/pkg/api/api0/metrics.go b/pkg/api/api0/metrics.go
index 5c8c462..161c1da 100644
--- a/pkg/api/api0/metrics.go
+++ b/pkg/api/api0/metrics.go
@@ -70,15 +70,8 @@ type apiMetrics struct {
fail_other_error *metrics.Counter
http_method_not_allowed *metrics.Counter
}
- client_originauth_requests_map *metricsx.GeoCounter2
- client_originauth_stryder_auth_duration_seconds *metrics.Histogram
- client_originauth_eax_username_lookup_duration_seconds *metrics.Histogram
- client_originauth_eax_username_lookup_calls_total struct {
- success *metrics.Counter
- notfound *metrics.Counter
- fail_update_check *metrics.Counter
- fail_other_error *metrics.Counter
- }
+ client_originauth_requests_map *metricsx.GeoCounter2
+ client_originauth_stryder_auth_duration_seconds *metrics.Histogram
client_originauth_stryder_username_lookup_calls_total struct {
success *metrics.Counter
notfound *metrics.Counter
@@ -263,11 +256,6 @@ func (h *Handler) m() *apiMetrics {
mo.client_originauth_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="http_method_not_allowed"}`)
mo.client_originauth_requests_map = metricsx.NewGeoCounter2(`atlas_api0_client_originauth_requests_map`)
mo.client_originauth_stryder_auth_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_stryder_auth_duration_seconds`)
- mo.client_originauth_eax_username_lookup_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_eax_username_lookup_duration_seconds`)
- mo.client_originauth_eax_username_lookup_calls_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="success"}`)
- mo.client_originauth_eax_username_lookup_calls_total.notfound = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="notfound"}`)
- mo.client_originauth_eax_username_lookup_calls_total.fail_update_check = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="fail_update_check"}`)
- mo.client_originauth_eax_username_lookup_calls_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="fail_other_error"}`)
mo.client_originauth_stryder_username_lookup_calls_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_stryder_username_lookup_calls_total{result="success"}`)
mo.client_originauth_stryder_username_lookup_calls_total.notfound = mo.set.NewCounter(`atlas_api0_client_originauth_stryder_username_lookup_calls_total{result="notfound"}`)
mo.client_originauth_stryder_username_lookup_calls_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_stryder_username_lookup_calls_total{result="fail_other_error"}`)
diff --git a/pkg/atlas/server.go b/pkg/atlas/server.go
index 7ad8fce..7399e8e 100644
--- a/pkg/atlas/server.go
+++ b/pkg/atlas/server.go
@@ -25,7 +25,6 @@ import (
"github.com/r2northstar/atlas/db/pdatadb"
"github.com/r2northstar/atlas/pkg/api/api0"
"github.com/r2northstar/atlas/pkg/cloudflare"
- "github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/memstore"
"github.com/r2northstar/atlas/pkg/nspkt"
"github.com/r2northstar/atlas/pkg/regionmap"
@@ -298,16 +297,6 @@ func NewServer(c *Config) (*Server, error) {
Add(hlog.RequestIDHandler("rid", "")).
Then(http.HandlerFunc(s.serveRest))
- if exc, err := configureEAX(c, s.Logger.With().Str("component", "eax").Logger()); err == nil {
- s.API0.EAXClient = exc
- } else {
- return nil, fmt.Errorf("initialize eax: %w", err)
- }
- if x, err := configureUsernameSource(c); err == nil {
- s.API0.UsernameSource = x
- } else {
- return nil, fmt.Errorf("initialize username lookup: %w", err)
- }
if astore, err := configureAccountStorage(c); err == nil {
s.API0.AccountStorage = astore
} else {
@@ -491,28 +480,6 @@ func configureLogging(c *Config) (l zerolog.Logger, reopen func(), err error) {
return
}
-func configureEAX(c *Config, l zerolog.Logger) (*eax.Client, error) {
- mgr := &eax.UpdateMgr{
- AutoUpdateBackoff: expbackoff,
- AutoUpdateHook: func(ver string, err error) {
- if err != nil {
- l.Err(err).Str("eax_client_version", ver).Msg("eax update error, using old version")
- } else {
- l.Info().Str("eax_client_version", ver).Msg("updated eax client version")
- }
- },
- }
- if v := c.EAXUpdateVersion; v != "" {
- mgr.SetVersion(v)
- } else {
- mgr.AutoUpdateInterval = c.EAXUpdateInterval
- mgr.AutoUpdateBucket = c.EAXUpdateBucket
- }
- return &eax.Client{
- UpdateMgr: mgr,
- }, nil
-}
-
func expbackoff(_ error, last time.Time, count int) bool {
var hmax, hmaxat, hrate float64 = 24, 8, 2.3
// ~5m, ~10m, ~23m, ~52m, ~2h, ~4.6h, ~10.5h, 24h
@@ -526,25 +493,6 @@ func expbackoff(_ error, last time.Time, count int) bool {
return time.Since(last).Hours() >= next
}
-func configureUsernameSource(c *Config) (api0.UsernameSource, error) {
- switch typ := c.UsernameSource; typ {
- case "none":
- return api0.UsernameSourceNone, nil
- case "eax":
- return api0.UsernameSourceEAX, nil
- case "stryder":
- return api0.UsernameSourceStryder, nil
- case "stryder-eax":
- return api0.UsernameSourceStryderEAX, nil
- case "stryder-eax-debug":
- return api0.UsernameSourceStryderEAXDebug, nil
- case "":
- return api0.UsernameSourceNone, nil
- default:
- return "", fmt.Errorf("unknown source %q", typ)
- }
-}
-
func configureAccountStorage(c *Config) (api0.AccountStorage, error) {
switch typ, arg, _ := strings.Cut(c.API0_Storage_Accounts, ":"); typ {
case "memory":
diff --git a/pkg/eax/eax.go b/pkg/eax/eax.go
deleted file mode 100644
index 34f5702..0000000
--- a/pkg/eax/eax.go
+++ /dev/null
@@ -1,145 +0,0 @@
-// Package eax queries the EA App API.
-package eax
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "mime"
- "net/http"
- "net/url"
- "strconv"
-)
-
-type Client struct {
- // The [net/http.Client] to use for requests. If not provided,
- // [net/http.DefaultClient] is used.
- Client *http.Client
-
- // The UpdateMgr for requests which require version information.
- UpdateMgr *UpdateMgr
-}
-
-var ErrVersionRequired = errors.New("client version is required for this endpoint")
-
-// PlayerID contains basic identifiers and names for a player.
-type PlayerID struct {
- PD uint64 // origin ID
- PSD uint64 // ?
- DisplayName string // in-game name
- Nickname string // social name?
-}
-
-// PlayerByPd gets basic information about an Origin ID. If the player does not
-// exist, nil will be returned.
-func (c *Client) PlayerIDByPD(ctx context.Context, pd uint64) (*PlayerID, error) {
- var obj struct {
- PlayerByPD *struct {
- PD string `json:"pd"`
- PSD string `json:"psd"`
- DisplayName string `json:"displayName"`
- Nickname string `json:"nickname"`
- } `json:"playerByPd"`
- }
- if err := c.gql1(ctx, true, `query { playerByPd (pd: `+strconv.FormatUint(pd, 10)+`) { pd psd displayName nickname } }`, &obj); err != nil {
- return nil, err
- }
- if obj.PlayerByPD == nil {
- return nil, nil
- }
- res := &PlayerID{
- DisplayName: obj.PlayerByPD.DisplayName,
- Nickname: obj.PlayerByPD.Nickname,
- }
- if s := obj.PlayerByPD.PD; s != "" {
- if v, err := strconv.ParseUint(s, 10, 64); err == nil {
- res.PD = v
- } else {
- return res, fmt.Errorf("parse pd %q: %w", s, err)
- }
- }
- if s := obj.PlayerByPD.PSD; s != "" {
- if v, err := strconv.ParseUint(s, 10, 64); err == nil {
- res.PD = v
- } else {
- return res, fmt.Errorf("parse psd %q: %w", s, err)
- }
- }
- return res, nil
-}
-
-// gql1 performs a basic GraphQL query.
-func (c *Client) gql1(ctx context.Context, ver bool, query string, out any) error {
- req, err := c.req(ctx, ver, http.MethodGet, "https://service-aggregation-layer.juno.ea.com/graphql?query="+url.QueryEscape(query), nil)
- if err != nil {
- return err
- }
-
- resp, err := c.do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if mt, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")); mt != "application/json" {
- if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("response status %d (%s) with content-type %q", resp.StatusCode, resp.Status, mt)
- }
- return fmt.Errorf("unexpected content-type %q", mt)
- }
-
- buf, err := io.ReadAll(resp.Body)
- if err != nil {
- return err
- }
-
- var obj struct {
- Data json.RawMessage `json:"data"`
- Errors []struct {
- Message string `json:"message"`
- } `json:"errors"`
- }
- if err := json.Unmarshal(buf, &obj); err != nil {
- if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("response status %d (%s) with invalid json", resp.StatusCode, resp.Status)
- }
- return fmt.Errorf("invalid json resp: %w", err)
- }
- if len(obj.Errors) != 0 {
- return fmt.Errorf("got %d errors, including %q", len(obj.Errors), obj.Errors[0].Message)
- }
- if err := json.Unmarshal([]byte(obj.Data), out); err != nil {
- return fmt.Errorf("invalid json data: %w", err)
- }
- return nil
-}
-
-func (c *Client) do(r *http.Request) (*http.Response, error) {
- if c.Client == nil {
- return http.DefaultClient.Do(r)
- }
- return c.Client.Do(r)
-}
-
-func (c *Client) req(ctx context.Context, ver bool, method, url string, body io.Reader) (*http.Request, error) {
- req, err := http.NewRequestWithContext(ctx, method, url, body)
- if err == nil {
- if ver {
- if c.UpdateMgr == nil {
- return nil, ErrVersionRequired
- }
- ver, _, err := c.UpdateMgr.Update(false)
- if err != nil {
- return nil, fmt.Errorf("%w: failed to update version: %v", ErrVersionRequired, err)
- }
- if ver == "" {
- return nil, ErrVersionRequired
- }
- req.Header.Set("User-Agent", "EADesktop/"+ver)
- }
- req.Header.Set("x-client-id", "EAX-JUNO-CLIENT")
- }
- return req, err
-}
diff --git a/pkg/eax/updatemgr.go b/pkg/eax/updatemgr.go
deleted file mode 100644
index d1a6a59..0000000
--- a/pkg/eax/updatemgr.go
+++ /dev/null
@@ -1,180 +0,0 @@
-package eax
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "strconv"
- "sync"
- "time"
-)
-
-// UpdateMgr manages EAX client version information.
-type UpdateMgr struct {
- // HTTP client to use. If not provided, [net/http.DefaultClient] will be
- // used.
- Client *http.Client
-
- // Timeout is the timeout for refreshing tokens. If zero, a reasonable
- // default is used. If negative, there is no timeout.
- Timeout time.Duration
-
- // Interval to update at. If zero, will not auto-update.
- AutoUpdateInterval time.Duration
-
- // Auto-update staged roll-out bucket.
- AutoUpdateBucket int
-
- // Auto-update backoff, if provided, checks if another auto-update is
- // allowed after a failure. If it returns false, ErrAutoUpdateBackoff will be
- // returned from the function triggering the auto-update.
- AutoUpdateBackoff func(err error, time time.Time, count int) bool
-
- // AutoUpdateHook is called for every auto-update attempt with the new (or
- // current if error) version, and any error which occurred.
- AutoUpdateHook func(v string, err error)
-
- verInit sync.Once
- verPf bool // ensures only one auto-update runs at a time
- verCv *sync.Cond // allows other goroutines to wait for that update to complete
- verErr error // last auto-update error
- verErrTime time.Time // last auto-update error time
- verErrCount int // consecutive auto-update errors
- ver string // current version
- verTime time.Time // last update check time
-}
-
-var ErrAutoUpdateBackoff = errors.New("not updating eax client version due to backoff")
-
-func (u *UpdateMgr) init() {
- u.verInit.Do(func() {
- u.verCv = sync.NewCond(new(sync.Mutex))
- })
-}
-
-// SetVersion sets the current version.
-func (u *UpdateMgr) SetVersion(v string) {
- u.init()
- u.verCv.L.Lock()
- for u.verPf {
- u.verCv.Wait()
- }
- u.ver = v
- u.verErr = nil
- u.verTime = time.Now()
- u.verCv.L.Unlock()
-}
-
-// Update gets the latest version, following u.AutoUpdateInterval if provided,
-// unless the version is not set or force is true. If another update is in
-// progress, it waits for the result of it. True is returned (on success or
-// failure) if this call performed a update. This function may block for up to
-// Timeout.
-func (u *UpdateMgr) Update(force bool) (string, bool, error) {
- u.init()
- if u.verCv.L.Lock(); u.verPf {
- for u.verPf {
- u.verCv.Wait()
- }
- defer u.verCv.L.Unlock()
- return u.ver, false, u.verErr
- } else {
- if force || u.ver == "" || (u.AutoUpdateInterval != 0 && time.Since(u.verTime) > u.AutoUpdateInterval) {
- u.verPf = true
- u.verCv.L.Unlock()
- defer func() {
- u.verCv.L.Lock()
- u.verCv.Broadcast()
- u.verPf = false
- u.verCv.L.Unlock()
- }()
- } else {
- defer u.verCv.L.Unlock()
- return u.ver, false, u.verErr
- }
- }
- if u.verErr != nil && u.AutoUpdateBackoff != nil {
- if !u.AutoUpdateBackoff(u.verErr, u.verErrTime, u.verErrCount) {
- return u.ver, true, fmt.Errorf("%w (%d attempts, last error: %v)", ErrAutoUpdateBackoff, u.verErrCount, u.verErr)
- }
- }
- u.verErr = func() error {
- var ctx context.Context
- var cancel context.CancelFunc
- if u.Timeout > 0 {
- ctx, cancel = context.WithTimeout(context.Background(), u.Timeout)
- } else if u.Timeout == 0 {
- ctx, cancel = context.WithTimeout(context.Background(), time.Second*15)
- } else {
- ctx, cancel = context.WithCancel(context.Background())
- }
- defer cancel()
-
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://autopatch.juno.ea.com/autopatch/upgrade/buckets/"+strconv.Itoa(u.AutoUpdateBucket), nil)
- if err != nil {
- return err
- }
- if u.ver != "" {
- req.Header.Set("User-Agent", "EADesktop/"+u.ver)
- } else {
- req.Header.Set("User-Agent", "")
- }
-
- cl := u.Client
- if cl == nil {
- cl = http.DefaultClient
- }
-
- resp, err := cl.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- var obj struct {
- Minimum struct {
- Version string `json:"version"`
- ActivationDate string `json:"activationDate"`
- } `json:"minimum"`
- Recommended struct {
- Version string `json:"version"`
- } `json:"recommended"`
- }
- if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("response status %d (%s)", resp.StatusCode, resp.Status)
- }
- if err := json.NewDecoder(resp.Body).Decode(&obj); err != nil {
- return fmt.Errorf("parse autopatch response: %w", err)
- }
-
- var version string
- if v := obj.Minimum.Version; v != "" {
- version = v
- }
- if v := obj.Recommended.Version; v != "" {
- version = v
- }
- if version == "" {
- return fmt.Errorf("parse autopatch response: missing minimum or recommended version")
- }
- u.ver = version
- u.verTime = time.Now()
- return nil
- }()
- if u.verErrCount != 0 {
- u.verErr = fmt.Errorf("%w (attempt %d)", u.verErr, u.verErrCount)
- }
- if u.verErr != nil {
- u.verErrCount++
- u.verErrTime = time.Now()
- } else {
- u.verErrCount = 0
- u.verErrTime = time.Time{}
- }
- if u.AutoUpdateHook != nil {
- go u.AutoUpdateHook(u.ver, u.verErr)
- }
- return u.ver, true, u.verErr
-}