package publicip import ( "context" "fmt" "net/http" "net/netip" "sync" "time" "github.com/qdm12/gluetun/internal/configuration/settings" "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/publicip/api" ) type Loop struct { // State settings settings.PublicIP settingsMutex sync.RWMutex ipData models.PublicIP ipDataMutex sync.RWMutex fetcher *api.ResilientFetcher // Fixed injected objects httpClient *http.Client logger Logger // Fixed parameters puid int pgid int // Internal channels and locks // runCtx is used to detect when the loop has exited // when performing an update runCtx context.Context //nolint:containedctx runCancel context.CancelFunc runTrigger chan<- context.Context runResult <-chan error updateTrigger chan<- settings.PublicIP updatedResult <-chan error runDone <-chan struct{} // Mock functions timeNow func() time.Time } func NewLoop(settings settings.PublicIP, puid, pgid int, httpClient *http.Client, logger Logger, ) (loop *Loop, err error) { fetchers, err := api.New(makeNameTokenPairs(settings.APIs), httpClient) if err != nil { return nil, fmt.Errorf("creating fetchers: %w", err) } return &Loop{ settings: settings, httpClient: httpClient, fetcher: api.NewResilient(fetchers, logger), logger: logger, puid: puid, pgid: pgid, timeNow: time.Now, }, nil } func makeNameTokenPairs(apis []settings.PublicIPAPI) (nameTokenPairs []api.NameToken) { nameTokenPairs = make([]api.NameToken, len(apis)) for i := range apis { nameTokenPairs[i] = api.NameToken{ Name: apis[i].Name, Token: apis[i].Token, } } return nameTokenPairs } func (l *Loop) String() string { return "public ip loop" } func (l *Loop) Start(_ context.Context) (_ <-chan error, err error) { l.runCtx, l.runCancel = context.WithCancel(context.Background()) runDone := make(chan struct{}) l.runDone = runDone runTrigger := make(chan context.Context) l.runTrigger = runTrigger runResult := make(chan error) l.runResult = runResult updateTrigger := make(chan settings.PublicIP) l.updateTrigger = updateTrigger updatedResult := make(chan error) l.updatedResult = updatedResult go l.run(l.runCtx, runDone, runTrigger, runResult, updateTrigger, updatedResult) return nil, nil //nolint:nilnil } func (l *Loop) run(runCtx context.Context, runDone chan<- struct{}, runTrigger <-chan context.Context, runResult chan<- error, updateTrigger <-chan settings.PublicIP, updatedResult chan<- error, ) { defer close(runDone) for { var singleRunCtx context.Context var singleRunResult chan<- error select { case <-runCtx.Done(): return case singleRunCtx = <-runTrigger: // Note singleRunCtx is canceled if runCtx is canceled. singleRunResult = runResult case partialUpdate := <-updateTrigger: var err error err = l.update(partialUpdate) updatedResult <- err continue } if !*l.settings.Enabled { singleRunResult <- nil continue } result, err := l.fetcher.FetchInfo(singleRunCtx, netip.Addr{}) if err != nil { err = fmt.Errorf("fetching information: %w", err) singleRunResult <- err continue } message := "Public IP address is " + result.IP.String() message += " (" + result.Country + ", " + result.Region + ", " + result.City + " - source: " + l.fetcher.String() + ")" l.logger.Info(message) l.ipDataMutex.Lock() l.ipData = result l.ipDataMutex.Unlock() filepath := *l.settings.IPFilepath err = persistPublicIP(filepath, result.IP.String(), l.puid, l.pgid) if err != nil { err = fmt.Errorf("persisting public ip address: %w", err) } singleRunResult <- err } } func (l *Loop) RunOnce(ctx context.Context) (err error) { singleRunCtx, singleRunCancel := context.WithCancel(ctx) select { case l.runTrigger <- singleRunCtx: case <-ctx.Done(): // in case writing to run trigger is blocking singleRunCancel() return ctx.Err() } select { case err = <-l.runResult: singleRunCancel() return err case <-l.runCtx.Done(): singleRunCancel() <-l.runResult return l.runCtx.Err() } } func (l *Loop) UpdateWith(partialUpdate settings.PublicIP) (err error) { select { case l.updateTrigger <- partialUpdate: select { case err = <-l.updatedResult: return err case <-l.runCtx.Done(): return l.runCtx.Err() } case <-l.runCtx.Done(): // loop has been stopped, no update can be done return l.runCtx.Err() } } func (l *Loop) Stop() (err error) { l.runCancel() <-l.runDone return l.ClearData() } func (l *Loop) Fetcher() (fetcher *api.ResilientFetcher) { return l.fetcher }