Significant improvements, added file watching to restar
t event loop on file change
This commit is contained in:
parent
f55bab7f9d
commit
dda670d6c0
1 changed files with 133 additions and 49 deletions
182
cmd/main.go
182
cmd/main.go
|
|
@ -3,19 +3,20 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflare-go"
|
|
||||||
"github.com/goodieshq/cfdns/pkg/cf"
|
"github.com/goodieshq/cfdns/pkg/cf"
|
||||||
"github.com/goodieshq/cfdns/pkg/config"
|
"github.com/goodieshq/cfdns/pkg/config"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "0.3.0"
|
const VERSION = "0.3.1"
|
||||||
const DEFAULT_CONFIG_FILE = "config/cfdns.yaml"
|
|
||||||
|
|
||||||
type CLIFlags struct {
|
type CLIFlags struct {
|
||||||
ConfigFile string
|
ConfigFile string
|
||||||
|
|
@ -23,72 +24,98 @@ type CLIFlags struct {
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
|
godotenv.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Info().Str("version", VERSION).Msg("starting cfdns...")
|
log.Info().Str("version", VERSION).Msg("Starting CFDNS...")
|
||||||
info := cli()
|
info := cli()
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
// create a context that is cancelled on SIGINT or SIGTERM
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// load the configuration file from JSON or YAML
|
// load the initial configuration from the YAML file
|
||||||
cfg, err := config.LoadConfig(info.ConfigFile)
|
cfg, err := config.LoadConfig(info.ConfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to load config file")
|
log.Fatal().Err(err).Msg("failed to load config file")
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfopts []cloudflare.Option
|
// set the initial logging level based on config
|
||||||
|
|
||||||
// set the log output verbosity level
|
|
||||||
if cfg.Verbose {
|
if cfg.Verbose {
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
} else {
|
} else {
|
||||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfdns, err := cf.NewCFDNS(*cfg, cfopts...)
|
// Create the cfdns instance
|
||||||
|
cfdns, err := cf.NewCFDNS(*cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed to create cfdns instance")
|
log.Fatal().Err(err).Msg("failed to create cfdns instance")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the provided zone ID is valid for the scoped token
|
// create a file watcher for the config file to signal changes
|
||||||
valid, err := cfdns.ZoneIsValid(ctx)
|
|
||||||
if !valid {
|
|
||||||
evt := log.Fatal()
|
|
||||||
if err != nil {
|
|
||||||
evt = evt.Err(err)
|
|
||||||
}
|
|
||||||
evt.Msg("failed to validate the zone")
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher := watchFile(ctx, info.ConfigFile)
|
watcher := watchFile(ctx, info.ConfigFile)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
log.Info().Msg("starting cfdns processing cycle...")
|
// create a timer for the processing frequency
|
||||||
|
timer := time.NewTimer(cfg.Frequency)
|
||||||
|
stop := func() {
|
||||||
|
if !timer.Stop() {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tStart := time.Now()
|
||||||
|
log.Debug().Msg("Starting CFDNS processing cycle.")
|
||||||
cfdns.Process(ctx)
|
cfdns.Process(ctx)
|
||||||
cfdns.Wait()
|
cfdns.Wait()
|
||||||
|
log.Info().Str("duration", Dur(time.Since(tStart))).Msg("Completed CFDNS processing cycle.")
|
||||||
|
|
||||||
|
// wait for the next cycle, config file modification, or shutdown signal
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Info().Msg("shutting down cfdns...")
|
stop()
|
||||||
|
log.Warn().Msg("Shutting down CFDNS...")
|
||||||
cfdns.Close()
|
cfdns.Close()
|
||||||
log.Info().Msg("cfdns stopped")
|
log.Info().Msg("CFDNS stopped. Exiting.")
|
||||||
return
|
return
|
||||||
case <-time.After(cfg.Frequency):
|
case <-timer.C:
|
||||||
case <-watcher:
|
// continue to next processing cycle
|
||||||
|
continue
|
||||||
|
case _, ok := <-watcher:
|
||||||
|
stop()
|
||||||
|
if !ok {
|
||||||
|
watcher = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// check file modification time and reload if changed
|
// check file modification time and reload if changed
|
||||||
log.Info().Msg("configuration file changed, reloading...")
|
log.Info().Msg("Configuration file changed, reloading...")
|
||||||
cfg, err = config.LoadConfig(info.ConfigFile)
|
|
||||||
|
// load the new configuration from file
|
||||||
|
cfgNew, err := config.LoadConfig(info.ConfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("failed to reload config file, keeping existing configuration")
|
log.Error().Err(err).Msg("failed to reload config file, keeping existing configuration")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cfg.Verbose {
|
|
||||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
// set the new configuration
|
||||||
} else {
|
if err := cfdns.SetConfig(cfgNew); err != nil {
|
||||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
log.Error().Err(err).Msg("failed to apply new configuration, keeping existing configuration")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
cfdns.SetConfig(cfg)
|
|
||||||
|
// update logging level based on new config
|
||||||
|
if cfg.Verbose {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
} else {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
}
|
||||||
|
log.Info().Msg("Configuration reloaded successfully.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,18 +127,43 @@ func cli() *CLIFlags {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if cliFlags.ConfigFile == "" {
|
if cliFlags.ConfigFile == "" {
|
||||||
cliFlags.ConfigFile = DEFAULT_CONFIG_FILE
|
log.Fatal().Msg("configuration file is required, use -config/-c to specify the file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cliFlags
|
return &cliFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLastModTime(filename string) time.Time {
|
type FileSig struct {
|
||||||
|
ModTime time.Time
|
||||||
|
Size int64
|
||||||
|
Inode uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs1 *FileSig) Changed(fs2 *FileSig) bool {
|
||||||
|
if fs1 == nil || fs2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fs2.ModTime.After(fs1.ModTime) ||
|
||||||
|
fs2.Size != fs1.Size ||
|
||||||
|
fs2.Inode != fs1.Inode
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileSig(filename string) *FileSig {
|
||||||
info, err := os.Stat(filename)
|
info, err := os.Stat(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var inode uint64
|
||||||
|
if st, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||||
|
inode = st.Ino
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FileSig{
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
Size: info.Size(),
|
||||||
|
Inode: inode,
|
||||||
}
|
}
|
||||||
return info.ModTime()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchFile(ctx context.Context, filename string) <-chan struct{} {
|
func watchFile(ctx context.Context, filename string) <-chan struct{} {
|
||||||
|
|
@ -119,30 +171,35 @@ func watchFile(ctx context.Context, filename string) <-chan struct{} {
|
||||||
interval := 1 * time.Second
|
interval := 1 * time.Second
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
modTime := getLastModTime(filename)
|
defer close(ch)
|
||||||
|
|
||||||
|
fileSig := getFileSig(filename)
|
||||||
|
|
||||||
|
t := time.NewTicker(interval)
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-time.After(interval):
|
case <-t.C:
|
||||||
// get the last modification time of the file
|
// get the metadata signature of the file
|
||||||
newModTime := getLastModTime(filename)
|
fileSigNew := getFileSig(filename)
|
||||||
|
|
||||||
// if we couldn't get the mod time, skip this iteration
|
// if we couldn't get the mod time, skip this iteration
|
||||||
if newModTime.IsZero() {
|
if fileSigNew == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is the first time checking, just set the modTime
|
// if this is the first time checking, just set the modTime
|
||||||
if modTime.IsZero() {
|
if fileSig == nil {
|
||||||
modTime = newModTime
|
fileSig = fileSigNew
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the modification time has changed, notify via channel
|
if fileSig.Changed(fileSigNew) {
|
||||||
if newModTime.After(modTime) {
|
// update the stored signature and notify
|
||||||
modTime = newModTime
|
fileSig = fileSigNew
|
||||||
<-time.After(50 * time.Millisecond)
|
|
||||||
select {
|
select {
|
||||||
case ch <- struct{}{}:
|
case ch <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
|
|
@ -153,3 +210,30 @@ func watchFile(ctx context.Context, filename string) <-chan struct{} {
|
||||||
}()
|
}()
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Dur(d time.Duration) string {
|
||||||
|
const precision = 2
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case d < time.Microsecond:
|
||||||
|
return fmt.Sprintf("%dns", d.Nanoseconds())
|
||||||
|
case d < time.Millisecond:
|
||||||
|
f := "%." + fmt.Sprintf("%d", precision) + "fµs"
|
||||||
|
return fmt.Sprintf(f, float64(d.Nanoseconds())/1000)
|
||||||
|
case d < time.Second:
|
||||||
|
f := "%." + fmt.Sprintf("%d", precision) + "fms"
|
||||||
|
return fmt.Sprintf(f, float64(d.Microseconds())/1000)
|
||||||
|
case d < time.Minute:
|
||||||
|
f := "%." + fmt.Sprintf("%d", precision) + "fs"
|
||||||
|
return fmt.Sprintf(f, d.Seconds())
|
||||||
|
case d < time.Hour:
|
||||||
|
m := int(d.Minutes())
|
||||||
|
s := int(d.Seconds()) % 60
|
||||||
|
return fmt.Sprintf("%dm%ds", m, s)
|
||||||
|
default:
|
||||||
|
h := int(d.Hours())
|
||||||
|
m := int(d.Minutes()) % 60
|
||||||
|
s := int(d.Seconds()) % 60
|
||||||
|
return fmt.Sprintf("%dh%dm%ds", h, m, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue