aboutsummaryrefslogtreecommitdiff
path: root/pkg/eax/updatemgr.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/eax/updatemgr.go')
-rw-r--r--pkg/eax/updatemgr.go180
1 files changed, 180 insertions, 0 deletions
diff --git a/pkg/eax/updatemgr.go b/pkg/eax/updatemgr.go
new file mode 100644
index 0000000..d1a6a59
--- /dev/null
+++ b/pkg/eax/updatemgr.go
@@ -0,0 +1,180 @@
+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
+}