Maint: rework publicip package
- Use loopstate package - Loop interface composition - Return concrete struct from constructors - Split into more files - Add publicip/state package
This commit is contained in:
@@ -346,7 +346,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
|
||||
go unboundLooper.Run(dnsCtx, dnsDone)
|
||||
otherGroupHandler.Add(dnsHandler)
|
||||
|
||||
publicIPLooper := publicip.NewLooper(httpClient,
|
||||
publicIPLooper := publicip.NewLoop(httpClient,
|
||||
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
|
||||
allSettings.PublicIP, puid, pgid)
|
||||
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
||||
@@ -488,7 +488,7 @@ func routeReadyEvents(ctx context.Context, done chan<- struct{}, buildInfo model
|
||||
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
|
||||
|
||||
// Runs the Public IP getter job once
|
||||
_, _ = publicIPLooper.SetStatus(ctx, constants.Running)
|
||||
_, _ = publicIPLooper.ApplyStatus(ctx, constants.Running)
|
||||
if versionInformation && first {
|
||||
first = false
|
||||
message, err := versionpkg.GetMessage(ctx, buildInfo, httpClient)
|
||||
|
||||
6
internal/publicip/alias.go
Normal file
6
internal/publicip/alias.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package publicip
|
||||
|
||||
import "github.com/qdm12/gluetun/internal/publicip/state"
|
||||
|
||||
type Getter = state.PublicIPGetter
|
||||
type SettingsGetterSetter = state.SettingsGetterSetter
|
||||
72
internal/publicip/fetch.go
Normal file
72
internal/publicip/fetch.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Package publicip defines interfaces to get your public IP address.
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
FetchPublicIP(ctx context.Context) (ip net.IP, err error)
|
||||
}
|
||||
|
||||
type Fetch struct {
|
||||
client *http.Client
|
||||
randIntn func(n int) int
|
||||
}
|
||||
|
||||
func NewFetch(client *http.Client) *Fetch {
|
||||
return &Fetch{
|
||||
client: client,
|
||||
randIntn: rand.Intn,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrParseIP = errors.New("cannot parse IP address")
|
||||
|
||||
func (f *Fetch) FetchPublicIP(ctx context.Context) (ip net.IP, err error) {
|
||||
urls := []string{
|
||||
"https://ifconfig.me/ip",
|
||||
"http://ip1.dynupdate.no-ip.com:8245",
|
||||
"http://ip1.dynupdate.no-ip.com",
|
||||
"https://api.ipify.org",
|
||||
"https://diagnostic.opendns.com/myip",
|
||||
"https://domains.google.com/checkip",
|
||||
"https://ifconfig.io/ip",
|
||||
"https://ipinfo.io/ip",
|
||||
}
|
||||
url := urls[f.randIntn(len(urls))]
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := f.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w from %s: %s", ErrBadStatusCode, url, response.Status)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrCannotReadBody, err)
|
||||
}
|
||||
|
||||
s := strings.ReplaceAll(string(content), "\n", "")
|
||||
ip = net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrParseIP, s)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
22
internal/publicip/helpers.go
Normal file
22
internal/publicip/helpers.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (l *Loop) logAndWait(ctx context.Context, err error) {
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
l.logger.Info("retrying in " + l.backoffTime.String())
|
||||
timer := time.NewTimer(l.backoffTime)
|
||||
l.backoffTime *= 2
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,39 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/loopstate"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
"github.com/qdm12/gluetun/internal/publicip/state"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
var _ Looper = (*Loop)(nil)
|
||||
|
||||
type Looper interface {
|
||||
Run(ctx context.Context, done chan<- struct{})
|
||||
RunRestartTicker(ctx context.Context, done chan<- struct{})
|
||||
GetStatus() (status models.LoopStatus)
|
||||
SetStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error)
|
||||
GetSettings() (settings configuration.PublicIP)
|
||||
SetSettings(settings configuration.PublicIP) (outcome string)
|
||||
GetPublicIP() (publicIP net.IP)
|
||||
Runner
|
||||
RestartTickerRunner
|
||||
loopstate.Getter
|
||||
loopstate.Applier
|
||||
SettingsGetterSetter
|
||||
Getter
|
||||
}
|
||||
|
||||
type looper struct {
|
||||
state state
|
||||
type Loop struct {
|
||||
statusManager loopstate.Manager
|
||||
state state.Manager
|
||||
// Objects
|
||||
getter IPGetter
|
||||
client *http.Client
|
||||
logger logging.Logger
|
||||
fetcher Fetcher
|
||||
client *http.Client
|
||||
logger logging.Logger
|
||||
// Fixed settings
|
||||
puid int
|
||||
pgid int
|
||||
// Internal channels and locks
|
||||
loopLock sync.Mutex
|
||||
start chan struct{}
|
||||
running chan models.LoopStatus
|
||||
stop chan struct{}
|
||||
@@ -43,22 +41,28 @@ type looper struct {
|
||||
updateTicker chan struct{}
|
||||
backoffTime time.Duration
|
||||
// Mock functions
|
||||
timeNow func() time.Time
|
||||
timeSince func(time.Time) time.Duration
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
const defaultBackoffTime = 5 * time.Second
|
||||
|
||||
func NewLooper(client *http.Client, logger logging.Logger,
|
||||
settings configuration.PublicIP, puid, pgid int) Looper {
|
||||
return &looper{
|
||||
state: state{
|
||||
status: constants.Stopped,
|
||||
settings: settings,
|
||||
},
|
||||
func NewLoop(client *http.Client, logger logging.Logger,
|
||||
settings configuration.PublicIP, puid, pgid int) *Loop {
|
||||
start := make(chan struct{})
|
||||
running := make(chan models.LoopStatus)
|
||||
stop := make(chan struct{})
|
||||
stopped := make(chan struct{})
|
||||
updateTicker := make(chan struct{})
|
||||
|
||||
statusManager := loopstate.New(constants.Stopped, start, running, stop, stopped)
|
||||
state := state.New(statusManager, settings, updateTicker)
|
||||
|
||||
return &Loop{
|
||||
statusManager: statusManager,
|
||||
state: state,
|
||||
// Objects
|
||||
client: client,
|
||||
getter: NewIPGetter(client),
|
||||
fetcher: NewFetch(client),
|
||||
logger: logger,
|
||||
puid: puid,
|
||||
pgid: pgid,
|
||||
@@ -69,151 +73,5 @@ func NewLooper(client *http.Client, logger logging.Logger,
|
||||
updateTicker: make(chan struct{}),
|
||||
backoffTime: defaultBackoffTime,
|
||||
timeNow: time.Now,
|
||||
timeSince: time.Since,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *looper) logAndWait(ctx context.Context, err error) {
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
l.logger.Info("retrying in " + l.backoffTime.String())
|
||||
timer := time.NewTimer(l.backoffTime)
|
||||
l.backoffTime *= 2
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *looper) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
crashed := false
|
||||
|
||||
select {
|
||||
case <-l.start:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
getCtx, getCancel := context.WithCancel(ctx)
|
||||
defer getCancel()
|
||||
|
||||
ipCh := make(chan net.IP)
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
ip, err := l.getter.Get(getCtx)
|
||||
if err != nil {
|
||||
if getCtx.Err() == nil {
|
||||
errorCh <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
ipCh <- ip
|
||||
}()
|
||||
|
||||
if !crashed {
|
||||
l.running <- constants.Running
|
||||
crashed = false
|
||||
} else {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.state.setStatusWithLock(constants.Running)
|
||||
}
|
||||
|
||||
stayHere := true
|
||||
for stayHere {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
getCancel()
|
||||
close(errorCh)
|
||||
filepath := l.GetSettings().IPFilepath
|
||||
l.logger.Info("Removing ip file " + filepath)
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
return
|
||||
case <-l.start:
|
||||
getCancel()
|
||||
stayHere = false
|
||||
case <-l.stop:
|
||||
l.logger.Info("stopping")
|
||||
getCancel()
|
||||
<-errorCh
|
||||
l.stopped <- struct{}{}
|
||||
case ip := <-ipCh:
|
||||
getCancel()
|
||||
l.state.setPublicIP(ip)
|
||||
|
||||
message := "Public IP address is " + ip.String()
|
||||
result, err := Info(ctx, l.client, ip)
|
||||
if err != nil {
|
||||
l.logger.Warn(err.Error())
|
||||
} else {
|
||||
message += " (" + result.Country + ", " + result.Region + ", " + result.City + ")"
|
||||
}
|
||||
l.logger.Info(message)
|
||||
|
||||
err = persistPublicIP(l.state.settings.IPFilepath,
|
||||
ip.String(), l.puid, l.pgid)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
l.state.setStatusWithLock(constants.Completed)
|
||||
case err := <-errorCh:
|
||||
getCancel()
|
||||
close(ipCh)
|
||||
l.state.setStatusWithLock(constants.Crashed)
|
||||
l.logAndWait(ctx, err)
|
||||
crashed = true
|
||||
stayHere = false
|
||||
}
|
||||
}
|
||||
close(errorCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *looper) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
timer := time.NewTimer(time.Hour)
|
||||
timer.Stop() // 1 hour, cannot be a race condition
|
||||
timerIsStopped := true
|
||||
if period := l.GetSettings().Period; period > 0 {
|
||||
timerIsStopped = false
|
||||
timer.Reset(period)
|
||||
}
|
||||
lastTick := time.Unix(0, 0)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timerIsStopped && !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
lastTick = l.timeNow()
|
||||
l.start <- struct{}{}
|
||||
timer.Reset(l.GetSettings().Period)
|
||||
case <-l.updateTicker:
|
||||
if !timerIsStopped && !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timerIsStopped = true
|
||||
period := l.GetSettings().Period
|
||||
if period == 0 {
|
||||
continue
|
||||
}
|
||||
var waited time.Duration
|
||||
if lastTick.UnixNano() > 0 {
|
||||
waited = l.timeSince(lastTick)
|
||||
}
|
||||
leftToWait := period - waited
|
||||
timer.Reset(leftToWait)
|
||||
timerIsStopped = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,7 @@
|
||||
// Package publicip defines interfaces to get your public IP address.
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
import "net"
|
||||
|
||||
type IPGetter interface {
|
||||
Get(ctx context.Context) (ip net.IP, err error)
|
||||
}
|
||||
|
||||
type ipGetter struct {
|
||||
client *http.Client
|
||||
randIntn func(n int) int
|
||||
}
|
||||
|
||||
func NewIPGetter(client *http.Client) IPGetter {
|
||||
return &ipGetter{
|
||||
client: client,
|
||||
randIntn: rand.Intn,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrParseIP = errors.New("cannot parse IP address")
|
||||
|
||||
func (i *ipGetter) Get(ctx context.Context) (ip net.IP, err error) {
|
||||
urls := []string{
|
||||
"https://ifconfig.me/ip",
|
||||
"http://ip1.dynupdate.no-ip.com:8245",
|
||||
"http://ip1.dynupdate.no-ip.com",
|
||||
"https://api.ipify.org",
|
||||
"https://diagnostic.opendns.com/myip",
|
||||
"https://domains.google.com/checkip",
|
||||
"https://ifconfig.io/ip",
|
||||
"https://ipinfo.io/ip",
|
||||
}
|
||||
url := urls[i.randIntn(len(urls))]
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := i.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w from %s: %s", ErrBadStatusCode, url, response.Status)
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrCannotReadBody, err)
|
||||
}
|
||||
|
||||
s := strings.ReplaceAll(string(content), "\n", "")
|
||||
ip = net.ParseIP(s)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrParseIP, s)
|
||||
}
|
||||
return ip, nil
|
||||
func (l *Loop) GetPublicIP() (publicIP net.IP) {
|
||||
return l.state.GetPublicIP()
|
||||
}
|
||||
|
||||
101
internal/publicip/runner.go
Normal file
101
internal/publicip/runner.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, done chan<- struct{})
|
||||
}
|
||||
|
||||
func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
crashed := false
|
||||
|
||||
select {
|
||||
case <-l.start:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
getCtx, getCancel := context.WithCancel(ctx)
|
||||
defer getCancel()
|
||||
|
||||
ipCh := make(chan net.IP)
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
ip, err := l.fetcher.FetchPublicIP(getCtx)
|
||||
if err != nil {
|
||||
if getCtx.Err() == nil {
|
||||
errorCh <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
ipCh <- ip
|
||||
}()
|
||||
|
||||
if !crashed {
|
||||
l.running <- constants.Running
|
||||
crashed = false
|
||||
} else {
|
||||
l.backoffTime = defaultBackoffTime
|
||||
l.statusManager.SetStatus(constants.Running)
|
||||
}
|
||||
|
||||
stayHere := true
|
||||
for stayHere {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
getCancel()
|
||||
close(errorCh)
|
||||
filepath := l.state.GetSettings().IPFilepath
|
||||
l.logger.Info("Removing ip file " + filepath)
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
return
|
||||
case <-l.start:
|
||||
getCancel()
|
||||
stayHere = false
|
||||
case <-l.stop:
|
||||
l.logger.Info("stopping")
|
||||
getCancel()
|
||||
<-errorCh
|
||||
l.stopped <- struct{}{}
|
||||
case ip := <-ipCh:
|
||||
getCancel()
|
||||
l.state.SetPublicIP(ip)
|
||||
|
||||
message := "Public IP address is " + ip.String()
|
||||
result, err := Info(ctx, l.client, ip)
|
||||
if err != nil {
|
||||
l.logger.Warn(err.Error())
|
||||
} else {
|
||||
message += " (" + result.Country + ", " + result.Region + ", " + result.City + ")"
|
||||
}
|
||||
l.logger.Info(message)
|
||||
|
||||
filepath := l.state.GetSettings().IPFilepath
|
||||
err = persistPublicIP(filepath, ip.String(), l.puid, l.pgid)
|
||||
if err != nil {
|
||||
l.logger.Error(err.Error())
|
||||
}
|
||||
l.statusManager.SetStatus(constants.Completed)
|
||||
case err := <-errorCh:
|
||||
getCancel()
|
||||
close(ipCh)
|
||||
l.statusManager.SetStatus(constants.Crashed)
|
||||
l.logAndWait(ctx, err)
|
||||
crashed = true
|
||||
stayHere = false
|
||||
}
|
||||
}
|
||||
close(errorCh)
|
||||
}
|
||||
}
|
||||
16
internal/publicip/settings.go
Normal file
16
internal/publicip/settings.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration"
|
||||
)
|
||||
|
||||
func (l *Loop) GetSettings() (settings configuration.PublicIP) {
|
||||
return l.state.GetSettings()
|
||||
}
|
||||
|
||||
func (l *Loop) SetSettings(ctx context.Context, settings configuration.PublicIP) (
|
||||
outcome string) {
|
||||
return l.state.SetSettings(ctx, settings)
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration"
|
||||
"github.com/qdm12/gluetun/internal/constants"
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
type state struct {
|
||||
status models.LoopStatus
|
||||
settings configuration.PublicIP
|
||||
ip net.IP
|
||||
statusMu sync.RWMutex
|
||||
settingsMu sync.RWMutex
|
||||
ipMu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *state) setStatusWithLock(status models.LoopStatus) {
|
||||
s.statusMu.Lock()
|
||||
defer s.statusMu.Unlock()
|
||||
s.status = status
|
||||
}
|
||||
|
||||
func (l *looper) GetStatus() (status models.LoopStatus) {
|
||||
l.state.statusMu.RLock()
|
||||
defer l.state.statusMu.RUnlock()
|
||||
return l.state.status
|
||||
}
|
||||
|
||||
var ErrInvalidStatus = errors.New("invalid status")
|
||||
|
||||
func (l *looper) SetStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error) {
|
||||
l.state.statusMu.Lock()
|
||||
defer l.state.statusMu.Unlock()
|
||||
existingStatus := l.state.status
|
||||
|
||||
switch status {
|
||||
case constants.Running:
|
||||
switch existingStatus {
|
||||
case constants.Starting, constants.Running, constants.Stopping, constants.Crashed:
|
||||
return fmt.Sprintf("already %s", existingStatus), nil
|
||||
}
|
||||
l.loopLock.Lock()
|
||||
defer l.loopLock.Unlock()
|
||||
l.state.status = constants.Starting
|
||||
l.state.statusMu.Unlock()
|
||||
l.start <- struct{}{}
|
||||
|
||||
newStatus := constants.Starting // for canceled context
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case newStatus = <-l.running:
|
||||
}
|
||||
l.state.statusMu.Lock()
|
||||
l.state.status = newStatus
|
||||
return newStatus.String(), nil
|
||||
case constants.Stopped:
|
||||
switch existingStatus {
|
||||
case constants.Stopped, constants.Stopping, constants.Starting, constants.Crashed:
|
||||
return fmt.Sprintf("already %s", existingStatus), nil
|
||||
}
|
||||
l.loopLock.Lock()
|
||||
defer l.loopLock.Unlock()
|
||||
l.state.status = constants.Stopping
|
||||
l.state.statusMu.Unlock()
|
||||
l.stop <- struct{}{}
|
||||
|
||||
newStatus := constants.Stopping // for canceled context
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-l.stopped:
|
||||
newStatus = constants.Stopped
|
||||
}
|
||||
l.state.statusMu.Lock()
|
||||
l.state.status = newStatus
|
||||
return status.String(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("%w: %s: it can only be one of: %s, %s",
|
||||
ErrInvalidStatus, status, constants.Running, constants.Stopped)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *looper) GetSettings() (settings configuration.PublicIP) {
|
||||
l.state.settingsMu.RLock()
|
||||
defer l.state.settingsMu.RUnlock()
|
||||
return l.state.settings
|
||||
}
|
||||
|
||||
func (l *looper) SetSettings(settings configuration.PublicIP) (
|
||||
outcome string) {
|
||||
l.state.settingsMu.Lock()
|
||||
defer l.state.settingsMu.Unlock()
|
||||
settingsUnchanged := reflect.DeepEqual(settings, l.state.settings)
|
||||
if settingsUnchanged {
|
||||
return "settings left unchanged"
|
||||
}
|
||||
periodChanged := l.state.settings.Period != settings.Period
|
||||
l.state.settings = settings
|
||||
if periodChanged {
|
||||
l.updateTicker <- struct{}{}
|
||||
// TODO blocking
|
||||
}
|
||||
return "settings updated"
|
||||
}
|
||||
|
||||
func (l *looper) GetPublicIP() (publicIP net.IP) {
|
||||
l.state.ipMu.RLock()
|
||||
defer l.state.ipMu.RUnlock()
|
||||
publicIP = make(net.IP, len(l.state.ip))
|
||||
copy(publicIP, l.state.ip)
|
||||
return publicIP
|
||||
}
|
||||
|
||||
func (s *state) setPublicIP(publicIP net.IP) {
|
||||
s.ipMu.Lock()
|
||||
defer s.ipMu.Unlock()
|
||||
s.ip = make(net.IP, len(publicIP))
|
||||
copy(s.ip, publicIP)
|
||||
}
|
||||
29
internal/publicip/state/publicip.go
Normal file
29
internal/publicip/state/publicip.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type PublicIPGetSetter interface {
|
||||
PublicIPGetter
|
||||
SetPublicIP(publicIP net.IP)
|
||||
}
|
||||
|
||||
type PublicIPGetter interface {
|
||||
GetPublicIP() (publicIP net.IP)
|
||||
}
|
||||
|
||||
func (s *State) GetPublicIP() (publicIP net.IP) {
|
||||
s.publicIPMu.RLock()
|
||||
defer s.publicIPMu.RUnlock()
|
||||
publicIP = make(net.IP, len(s.publicIP))
|
||||
copy(publicIP, s.publicIP)
|
||||
return publicIP
|
||||
}
|
||||
|
||||
func (s *State) SetPublicIP(publicIP net.IP) {
|
||||
s.settingsMu.Lock()
|
||||
defer s.settingsMu.Unlock()
|
||||
s.publicIP = make(net.IP, len(publicIP))
|
||||
copy(s.publicIP, publicIP)
|
||||
}
|
||||
39
internal/publicip/state/settings.go
Normal file
39
internal/publicip/state/settings.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration"
|
||||
)
|
||||
|
||||
type SettingsGetterSetter interface {
|
||||
GetSettings() (settings configuration.PublicIP)
|
||||
SetSettings(ctx context.Context,
|
||||
settings configuration.PublicIP) (outcome string)
|
||||
}
|
||||
|
||||
func (s *State) GetSettings() (settings configuration.PublicIP) {
|
||||
s.settingsMu.RLock()
|
||||
defer s.settingsMu.RUnlock()
|
||||
return s.settings
|
||||
}
|
||||
|
||||
func (s *State) SetSettings(ctx context.Context, settings configuration.PublicIP) (
|
||||
outcome string) {
|
||||
s.settingsMu.Lock()
|
||||
defer s.settingsMu.Unlock()
|
||||
|
||||
settingsUnchanged := reflect.DeepEqual(s.settings, settings)
|
||||
if settingsUnchanged {
|
||||
return "settings left unchanged"
|
||||
}
|
||||
|
||||
periodChanged := s.settings.Period != settings.Period
|
||||
s.settings = settings
|
||||
if periodChanged {
|
||||
s.updateTicker <- struct{}{}
|
||||
// TODO blocking
|
||||
}
|
||||
return "settings updated"
|
||||
}
|
||||
38
internal/publicip/state/state.go
Normal file
38
internal/publicip/state/state.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/configuration"
|
||||
"github.com/qdm12/gluetun/internal/loopstate"
|
||||
)
|
||||
|
||||
var _ Manager = (*State)(nil)
|
||||
|
||||
type Manager interface {
|
||||
SettingsGetterSetter
|
||||
PublicIPGetSetter
|
||||
}
|
||||
|
||||
func New(statusApplier loopstate.Applier,
|
||||
settings configuration.PublicIP,
|
||||
updateTicker chan<- struct{}) *State {
|
||||
return &State{
|
||||
statusApplier: statusApplier,
|
||||
settings: settings,
|
||||
updateTicker: updateTicker,
|
||||
}
|
||||
}
|
||||
|
||||
type State struct {
|
||||
statusApplier loopstate.Applier
|
||||
|
||||
settings configuration.PublicIP
|
||||
settingsMu sync.RWMutex
|
||||
|
||||
publicIP net.IP
|
||||
publicIPMu sync.RWMutex
|
||||
|
||||
updateTicker chan<- struct{}
|
||||
}
|
||||
16
internal/publicip/status.go
Normal file
16
internal/publicip/status.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qdm12/gluetun/internal/models"
|
||||
)
|
||||
|
||||
func (l *Loop) GetStatus() (status models.LoopStatus) {
|
||||
return l.statusManager.GetStatus()
|
||||
}
|
||||
|
||||
func (l *Loop) ApplyStatus(ctx context.Context, status models.LoopStatus) (
|
||||
outcome string, err error) {
|
||||
return l.statusManager.ApplyStatus(ctx, status)
|
||||
}
|
||||
51
internal/publicip/ticker.go
Normal file
51
internal/publicip/ticker.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RestartTickerRunner interface {
|
||||
RunRestartTicker(ctx context.Context, done chan<- struct{})
|
||||
}
|
||||
|
||||
func (l *Loop) RunRestartTicker(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
timer := time.NewTimer(time.Hour)
|
||||
timer.Stop() // 1 hour, cannot be a race condition
|
||||
timerIsStopped := true
|
||||
if period := l.state.GetSettings().Period; period > 0 {
|
||||
timerIsStopped = false
|
||||
timer.Reset(period)
|
||||
}
|
||||
lastTick := time.Unix(0, 0)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timerIsStopped && !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
lastTick = l.timeNow()
|
||||
l.start <- struct{}{}
|
||||
timer.Reset(l.state.GetSettings().Period)
|
||||
case <-l.updateTicker:
|
||||
if !timerIsStopped && !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
timerIsStopped = true
|
||||
period := l.state.GetSettings().Period
|
||||
if period == 0 {
|
||||
continue
|
||||
}
|
||||
var waited time.Duration
|
||||
if lastTick.UnixNano() > 0 {
|
||||
waited = l.timeNow().Sub(lastTick)
|
||||
}
|
||||
leftToWait := period - waited
|
||||
timer.Reset(leftToWait)
|
||||
timerIsStopped = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user