feat: Настройки › Автообновление - clients compare their version with the server's every N and update themselves; the server looks at GitLab every N and, when a newer release is out, offers it to an administrator at login (after the PIN) with «Обновить» and «Отказаться»; the words of a made picture's result are the model's only
This commit is contained in:
1 parent
916a49958b
commit
3d2f886f04
22 files changed
+1074
-7
No files matched your search
@@ -0,0 +1,95 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
||||
)
|
||||
|
||||
// Auto-update, the agent's part: the server tells (with every poll, on its
|
||||
// config and on the WebSocket) its own version and how often to compare -
|
||||
// in minutes, 0 for never. When the server's version is newer, the agent
|
||||
// takes the new one from it, as the panel's «Обновить» does.
|
||||
|
||||
// autoUpdate is what the agent was told, and when it last looked.
|
||||
type autoUpdate struct {
|
||||
mu sync.Mutex
|
||||
minutes int
|
||||
serverVer string
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func (a *Agent) applyUpdateInfo(minutes int, serverVersion string) {
|
||||
a.auto.mu.Lock()
|
||||
defer a.auto.mu.Unlock()
|
||||
if minutes < 0 {
|
||||
minutes = 0
|
||||
}
|
||||
a.auto.minutes = minutes
|
||||
if serverVersion != "" {
|
||||
a.auto.serverVer = serverVersion
|
||||
}
|
||||
}
|
||||
|
||||
// applyUpdateHeaders takes the settings from the headers of an answer; an
|
||||
// answer without them (an older server) changes nothing.
|
||||
func (a *Agent) applyUpdateHeaders(h http.Header) {
|
||||
v := h.Get(proto.AutoUpdateHeader)
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
m, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
m = 0
|
||||
}
|
||||
a.applyUpdateInfo(m, h.Get(proto.ServerVersionHeader))
|
||||
}
|
||||
|
||||
func (a *Agent) updateInfo() (int, string) {
|
||||
a.auto.mu.Lock()
|
||||
defer a.auto.mu.Unlock()
|
||||
return a.auto.minutes, a.auto.serverVer
|
||||
}
|
||||
|
||||
func (a *Agent) autoUpdateOn() bool { m, _ := a.updateInfo(); return m > 0 }
|
||||
|
||||
// autoUpdateTick: one look at the clock. Out of the interval since the last
|
||||
// look, it looks; the server's version newer than its own, it updates.
|
||||
// Reports whether it did.
|
||||
func (a *Agent) autoUpdateTick(now time.Time, update func() proto.TaskResult) bool {
|
||||
a.auto.mu.Lock()
|
||||
if a.auto.minutes <= 0 || (!a.auto.last.IsZero() && now.Sub(a.auto.last) < time.Duration(a.auto.minutes)*time.Minute) {
|
||||
a.auto.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
a.auto.last = now
|
||||
server := a.auto.serverVer
|
||||
a.auto.mu.Unlock()
|
||||
if !proto.VersionNewer(server, Version) {
|
||||
return false
|
||||
}
|
||||
log.Printf("auto-update: server has %s, this is %s - updating", server, Version)
|
||||
if res := update(); !res.OK {
|
||||
log.Printf("auto-update: %s", res.Error)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// autoUpdateLoop watches the clock until ctx ends.
|
||||
func (a *Agent) autoUpdateLoop(ctx context.Context) {
|
||||
t := time.NewTicker(30 * time.Second)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case now := <-t.C:
|
||||
a.autoUpdateTick(now, a.doUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
||||
)
|
||||
|
||||
// updates counts what an agent was made to update with.
|
||||
type updates struct{ n int }
|
||||
|
||||
func (u *updates) do() proto.TaskResult { u.n++; return proto.TaskResult{OK: true} }
|
||||
|
||||
// The agent is told, by headers or by the WebSocket's message, how often to
|
||||
// compare versions; nothing told, nothing done.
|
||||
func TestAutoUpdateIsTold(t *testing.T) {
|
||||
a := New(Config{})
|
||||
if a.autoUpdateOn() {
|
||||
t.Fatal("on with nothing told")
|
||||
}
|
||||
h := http.Header{}
|
||||
h.Set(proto.AutoUpdateHeader, "90")
|
||||
h.Set(proto.ServerVersionHeader, "9.9.9")
|
||||
a.applyUpdateHeaders(h)
|
||||
if m, v := a.updateInfo(); m != 90 || v != "9.9.9" {
|
||||
t.Fatalf("by headers: %d %q", m, v)
|
||||
}
|
||||
a.applyUpdateInfo(0, "9.9.9")
|
||||
if a.autoUpdateOn() {
|
||||
t.Fatal("still on after being told 0")
|
||||
}
|
||||
a.applyUpdateInfo(30, "9.9.9")
|
||||
if m, _ := a.updateInfo(); m != 30 {
|
||||
t.Fatalf("by the message: %d", m)
|
||||
}
|
||||
// Rubbish is not taken for a number.
|
||||
h.Set(proto.AutoUpdateHeader, "soon")
|
||||
a.applyUpdateHeaders(h)
|
||||
if m, _ := a.updateInfo(); m != 0 {
|
||||
t.Fatalf("rubbish: %d", m)
|
||||
}
|
||||
}
|
||||
|
||||
// When the interval is out the agent looks; it updates only to a newer
|
||||
// version of the server's, and tries again the next time if it did not work.
|
||||
func TestAutoUpdateTick(t *testing.T) {
|
||||
a := New(Config{})
|
||||
u := &updates{}
|
||||
now := time.Now()
|
||||
a.applyUpdateInfo(60, "9.9.9")
|
||||
|
||||
if !a.autoUpdateTick(now, u.do) || u.n != 1 {
|
||||
t.Fatalf("a newer server: %d updates", u.n)
|
||||
}
|
||||
if a.autoUpdateTick(now.Add(30*time.Minute), u.do) || u.n != 1 {
|
||||
t.Fatalf("before the interval is out: %d updates", u.n)
|
||||
}
|
||||
if !a.autoUpdateTick(now.Add(61*time.Minute), u.do) || u.n != 2 {
|
||||
t.Fatalf("at the next time: %d updates", u.n)
|
||||
}
|
||||
|
||||
// The same, an older one, a build that is not a version: nothing.
|
||||
for _, v := range []string{Version, "0.0.1", "dev", ""} {
|
||||
b := New(Config{})
|
||||
b.applyUpdateInfo(60, v)
|
||||
if b.autoUpdateTick(now, u.do) {
|
||||
t.Fatalf("updated to %q", v)
|
||||
}
|
||||
}
|
||||
// Off: nothing, whatever the server has.
|
||||
c := New(Config{})
|
||||
c.applyUpdateInfo(0, "9.9.9")
|
||||
if c.autoUpdateTick(now, u.do) || u.n != 2 {
|
||||
t.Fatalf("off, yet updated: %d", u.n)
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,8 @@ type Agent struct {
|
||||
tunMu sync.Mutex
|
||||
tunAddr string
|
||||
tunCancel context.CancelFunc
|
||||
|
||||
auto autoUpdate // what the server told of auto-update
|
||||
}
|
||||
|
||||
// New builds an agent. It does not perform any network I/O.
|
||||
@@ -115,6 +117,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
go a.watchMain(ctx)
|
||||
go a.autoUpdateLoop(ctx)
|
||||
log.Printf("agent %s connected to %s", a.state.AgentID, a.host())
|
||||
|
||||
// Direct forward tunnel (low-latency TCP mux + UDP). Its address comes
|
||||
@@ -321,6 +324,7 @@ func (a *Agent) pollOnce(ctx, reqCtx context.Context) error {
|
||||
a.applyTunnelHeader(ctx, resp.Header)
|
||||
a.learnAddrs(resp.Header.Get(proto.MainHeader), resp.Header.Get(proto.FallbackHeader))
|
||||
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
||||
a.applyUpdateHeaders(resp.Header)
|
||||
a.dispatch(ctx, msg)
|
||||
return nil
|
||||
}
|
||||
@@ -356,6 +360,7 @@ func (a *Agent) fetchConfig(ctx context.Context) bool {
|
||||
defer resp.Body.Close()
|
||||
a.applyTransport(resp.Header.Get(proto.TransportHeader))
|
||||
a.learnAddrs(resp.Header.Get(proto.MainHeader), resp.Header.Get(proto.FallbackHeader))
|
||||
a.applyUpdateHeaders(resp.Header)
|
||||
return a.applyTunnelHeader(ctx, resp.Header)
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ func (a *Agent) runWS(ctx context.Context) error {
|
||||
a.applyTunnelAddr(ctx, m.Tunnel)
|
||||
a.learnAddrs(m.Main, m.Fallback)
|
||||
a.applyTransport(m.Transport)
|
||||
a.applyUpdateInfo(m.AutoUpdate, m.ServerVersion)
|
||||
if a.mode() == proto.TransportLP {
|
||||
return errors.New("set to long polling")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user