diff --git a/config/config.go b/config/config.go index 768428b7..6a899b62 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/thrasher-/gocryptotrader/connchecker" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/forexprovider" @@ -104,18 +105,19 @@ type CurrencyPairFormatConfig struct { // prestart management of Portfolio, Communications, Webserver and Enabled // Exchanges type Config struct { - Name string `json:"name"` - EncryptConfig int `json:"encryptConfig"` - GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` - Logging log.Logging `json:"logging"` - Profiler ProfilerConfig `json:"profiler"` - NTPClient NTPClientConfig `json:"ntpclient"` - Currency CurrencyConfig `json:"currencyConfig"` - Communications CommunicationsConfig `json:"communications"` - Portfolio portfolio.Base `json:"portfolioAddresses"` - Webserver WebserverConfig `json:"webserver"` - Exchanges []ExchangeConfig `json:"exchanges"` - BankAccounts []BankAccount `json:"bankAccounts"` + Name string `json:"name"` + EncryptConfig int `json:"encryptConfig"` + GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"` + Logging log.Logging `json:"logging"` + Profiler ProfilerConfig `json:"profiler"` + NTPClient NTPClientConfig `json:"ntpclient"` + Currency CurrencyConfig `json:"currencyConfig"` + Communications CommunicationsConfig `json:"communications"` + Portfolio portfolio.Base `json:"portfolioAddresses"` + Webserver WebserverConfig `json:"webserver"` + Exchanges []ExchangeConfig `json:"exchanges"` + BankAccounts []BankAccount `json:"bankAccounts"` + ConnectionMonitor ConnectionMonitorConfig `json:"connectionMonitor"` // Deprecated config settings, will be removed at a future date CurrencyPairFormat *CurrencyPairFormatConfig `json:"currencyPairFormat,omitempty"` @@ -124,6 +126,14 @@ type Config struct { SMS *SMSGlobalConfig `json:"smsGlobal,omitempty"` } +// ConnectionMonitorConfig defines the connection monitor variables to ensure +// that there is internet connectivity +type ConnectionMonitorConfig struct { + DNSList []string `json:"preferredDNSList"` + PublicDomainList []string `json:"preferredDomainList"` + CheckInterval time.Duration `json:"checkInterval"` +} + // ProfilerConfig defines the profiler configuration to enable pprof type ProfilerConfig struct { Enabled bool `json:"enabled"` @@ -1158,6 +1168,24 @@ func (c *Config) DisableNTPCheck(input io.Reader) (string, error) { return "", errors.New("something went wrong NTPCheck should never make it this far") } +// CheckConnectionMonitorConfig checks and if zero value assigns default values +func (c *Config) CheckConnectionMonitorConfig() { + m.Lock() + defer m.Unlock() + + if c.ConnectionMonitor.CheckInterval == 0 { + c.ConnectionMonitor.CheckInterval = connchecker.DefaultCheckInterval + } + + if len(c.ConnectionMonitor.DNSList) == 0 { + c.ConnectionMonitor.DNSList = connchecker.DefaultDNSList + } + + if len(c.ConnectionMonitor.PublicDomainList) == 0 { + c.ConnectionMonitor.PublicDomainList = connchecker.DefaultDomainList + } +} + // GetFilePath returns the desired config file or the default config file name // based on if the application is being run under test or normal mode. func GetFilePath(file string) (string, error) { @@ -1351,6 +1379,7 @@ func (c *Config) CheckConfig() error { return fmt.Errorf(ErrCheckingConfigValues, err) } + c.CheckConnectionMonitorConfig() c.CheckCommunicationsConfig() if c.Webserver.Enabled { diff --git a/connchecker/connchecker.go b/connchecker/connchecker.go new file mode 100644 index 00000000..03b725f7 --- /dev/null +++ b/connchecker/connchecker.go @@ -0,0 +1,121 @@ +package connchecker + +import ( + "net" + "sync" + "time" + + log "github.com/thrasher-/gocryptotrader/logger" +) + +// DefaultCheckInterval is a const that defines the amount of time between +// checking if the connection is lost +const DefaultCheckInterval = time.Second + +// Default check lists +var ( + DefaultDNSList = []string{"8.8.8.8", "8.8.4.4", "1.1.1.1", "1.0.0.1"} + DefaultDomainList = []string{"www.google.com", "www.cloudflare.com", "www.facebook.com"} +) + +// New returns a new connection checker, if no values set it will default it out +func New(dnsList, domainList []string, checkInterval time.Duration) *Checker { + c := &Checker{} + if len(dnsList) == 0 { + c.DNSList = DefaultDNSList + } else { + c.DNSList = dnsList + } + + if len(domainList) == 0 { + c.DomainList = DefaultDomainList + } else { + c.DomainList = domainList + } + + if checkInterval == 0 { + c.CheckInterval = DefaultCheckInterval + } else { + c.CheckInterval = checkInterval + } + + go c.Monitor() + return c +} + +// Checker defines a struct to determine connectivity to the interwebs +type Checker struct { + DNSList []string + DomainList []string + CheckInterval time.Duration + shutdown chan struct{} + wg sync.WaitGroup + connected bool + sync.Mutex +} + +// Shutdown cleanly shutsdown monitor routine +func (c *Checker) Shutdown() { + c.shutdown <- struct{}{} + c.wg.Wait() +} + +// Monitor determines internet connectivity via a DNS lookup +func (c *Checker) Monitor() { + c.wg.Add(1) + tick := time.NewTicker(time.Second) + defer func() { tick.Stop(); c.wg.Done() }() + c.connectionTest() + for { + select { + case <-tick.C: + c.connectionTest() + case <-c.shutdown: + return + } + } +} + +// ConnectionTest determines if a connection to the internet is available by +// iterating over a set list of dns ip and popular domains +func (c *Checker) connectionTest() { + for i := range c.DNSList { + _, err := net.LookupAddr(c.DNSList[i]) + if err == nil { + c.Lock() + if !c.connected { + log.Warnf("Internet connectivity re-established") + c.connected = true + } + c.Unlock() + return + } + } + + for i := range c.DomainList { + _, err := net.LookupHost(c.DomainList[i]) + if err == nil { + c.Lock() + if !c.connected { + log.Warnf("Internet connectivity re-established") + c.connected = true + } + c.Unlock() + return + } + } + + c.Lock() + if c.connected { + log.Warnf("Internet connectivity lost") + c.connected = false + } + c.Unlock() +} + +// IsConnected returns if there is internet connectivity +func (c *Checker) IsConnected() bool { + c.Lock() + defer c.Unlock() + return c.connected +} diff --git a/main.go b/main.go index 6049143d..e9a7db6c 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,14 @@ import ( "os/signal" "runtime" "strconv" + "sync" "syscall" "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/communications" "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/connchecker" "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/coinmarketcap" exchange "github.com/thrasher-/gocryptotrader/exchanges" @@ -25,14 +27,16 @@ import ( // Bot contains configuration, portfolio, exchange & ticker data and is the // overarching type across this code base. type Bot struct { - config *config.Config - portfolio *portfolio.Base - exchanges []exchange.IBotExchange - comms *communications.Communications - shutdown chan bool - dryRun bool - configFile string - dataDir string + config *config.Config + portfolio *portfolio.Base + exchanges []exchange.IBotExchange + comms *communications.Communications + shutdown chan bool + dryRun bool + configFile string + dataDir string + connectivity *connchecker.Checker + sync.Mutex } const banner = ` @@ -129,6 +133,11 @@ func main() { } } + // Sets up internet connectivity monitor + bot.connectivity = connchecker.New(bot.config.ConnectionMonitor.DNSList, + bot.config.ConnectionMonitor.PublicDomainList, + bot.config.ConnectionMonitor.CheckInterval) + AdjustGoMaxProcs() log.Debugf("Bot '%s' started.\n", bot.config.Name) log.Debugf("Bot dry run mode: %v.\n", common.IsEnabled(bot.dryRun)) @@ -243,7 +252,7 @@ func HandleInterrupt() { go func() { sig := <-c log.Debugf("Captured %v, shutdown requested.", sig) - bot.shutdown <- true + close(bot.shutdown) }() }