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)
|
go unboundLooper.Run(dnsCtx, dnsDone)
|
||||||
otherGroupHandler.Add(dnsHandler)
|
otherGroupHandler.Add(dnsHandler)
|
||||||
|
|
||||||
publicIPLooper := publicip.NewLooper(httpClient,
|
publicIPLooper := publicip.NewLoop(httpClient,
|
||||||
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
|
logger.NewChild(logging.Settings{Prefix: "ip getter: "}),
|
||||||
allSettings.PublicIP, puid, pgid)
|
allSettings.PublicIP, puid, pgid)
|
||||||
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
pubIPHandler, pubIPCtx, pubIPDone := goshutdown.NewGoRoutineHandler(
|
||||||
@@ -488,7 +488,7 @@ func routeReadyEvents(ctx context.Context, done chan<- struct{}, buildInfo model
|
|||||||
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
|
restartTickerContext, restartTickerCancel = context.WithCancel(ctx)
|
||||||
|
|
||||||
// Runs the Public IP getter job once
|
// Runs the Public IP getter job once
|
||||||
_, _ = publicIPLooper.SetStatus(ctx, constants.Running)
|
_, _ = publicIPLooper.ApplyStatus(ctx, constants.Running)
|
||||||
if versionInformation && first {
|
if versionInformation && first {
|
||||||
first = false
|
first = false
|
||||||
message, err := versionpkg.GetMessage(ctx, buildInfo, httpClient)
|
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
|
package publicip
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/qdm12/gluetun/internal/configuration"
|
"github.com/qdm12/gluetun/internal/configuration"
|
||||||
"github.com/qdm12/gluetun/internal/constants"
|
"github.com/qdm12/gluetun/internal/constants"
|
||||||
|
"github.com/qdm12/gluetun/internal/loopstate"
|
||||||
"github.com/qdm12/gluetun/internal/models"
|
"github.com/qdm12/gluetun/internal/models"
|
||||||
|
"github.com/qdm12/gluetun/internal/publicip/state"
|
||||||
"github.com/qdm12/golibs/logging"
|
"github.com/qdm12/golibs/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ Looper = (*Loop)(nil)
|
||||||
|
|
||||||
type Looper interface {
|
type Looper interface {
|
||||||
Run(ctx context.Context, done chan<- struct{})
|
Runner
|
||||||
RunRestartTicker(ctx context.Context, done chan<- struct{})
|
RestartTickerRunner
|
||||||
GetStatus() (status models.LoopStatus)
|
loopstate.Getter
|
||||||
SetStatus(ctx context.Context, status models.LoopStatus) (
|
loopstate.Applier
|
||||||
outcome string, err error)
|
SettingsGetterSetter
|
||||||
GetSettings() (settings configuration.PublicIP)
|
Getter
|
||||||
SetSettings(settings configuration.PublicIP) (outcome string)
|
|
||||||
GetPublicIP() (publicIP net.IP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type looper struct {
|
type Loop struct {
|
||||||
state state
|
statusManager loopstate.Manager
|
||||||
|
state state.Manager
|
||||||
// Objects
|
// Objects
|
||||||
getter IPGetter
|
fetcher Fetcher
|
||||||
client *http.Client
|
client *http.Client
|
||||||
logger logging.Logger
|
logger logging.Logger
|
||||||
// Fixed settings
|
// Fixed settings
|
||||||
puid int
|
puid int
|
||||||
pgid int
|
pgid int
|
||||||
// Internal channels and locks
|
// Internal channels and locks
|
||||||
loopLock sync.Mutex
|
|
||||||
start chan struct{}
|
start chan struct{}
|
||||||
running chan models.LoopStatus
|
running chan models.LoopStatus
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
@@ -43,22 +41,28 @@ type looper struct {
|
|||||||
updateTicker chan struct{}
|
updateTicker chan struct{}
|
||||||
backoffTime time.Duration
|
backoffTime time.Duration
|
||||||
// Mock functions
|
// Mock functions
|
||||||
timeNow func() time.Time
|
timeNow func() time.Time
|
||||||
timeSince func(time.Time) time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultBackoffTime = 5 * time.Second
|
const defaultBackoffTime = 5 * time.Second
|
||||||
|
|
||||||
func NewLooper(client *http.Client, logger logging.Logger,
|
func NewLoop(client *http.Client, logger logging.Logger,
|
||||||
settings configuration.PublicIP, puid, pgid int) Looper {
|
settings configuration.PublicIP, puid, pgid int) *Loop {
|
||||||
return &looper{
|
start := make(chan struct{})
|
||||||
state: state{
|
running := make(chan models.LoopStatus)
|
||||||
status: constants.Stopped,
|
stop := make(chan struct{})
|
||||||
settings: settings,
|
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
|
// Objects
|
||||||
client: client,
|
client: client,
|
||||||
getter: NewIPGetter(client),
|
fetcher: NewFetch(client),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
puid: puid,
|
puid: puid,
|
||||||
pgid: pgid,
|
pgid: pgid,
|
||||||
@@ -69,151 +73,5 @@ func NewLooper(client *http.Client, logger logging.Logger,
|
|||||||
updateTicker: make(chan struct{}),
|
updateTicker: make(chan struct{}),
|
||||||
backoffTime: defaultBackoffTime,
|
backoffTime: defaultBackoffTime,
|
||||||
timeNow: time.Now,
|
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
|
package publicip
|
||||||
|
|
||||||
import (
|
import "net"
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IPGetter interface {
|
func (l *Loop) GetPublicIP() (publicIP net.IP) {
|
||||||
Get(ctx context.Context) (ip net.IP, err error)
|
return l.state.GetPublicIP()
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
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