From 6e2fabb2a46812dee42842c13439e1a0238aa40b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sun, 22 Mar 2020 22:58:24 -0600 Subject: cmd: Add --watch flag to start & run commands (closes #1806) Because, just for fun. --- cmd/main.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 7 deletions(-) (limited to 'cmd/main.go') diff --git a/cmd/main.go b/cmd/main.go index 1a3397e..4dbbe38 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -99,10 +99,10 @@ func handlePingbackConn(conn net.Conn, expect []byte) error { // there is no config available. It prints any warnings to stderr, // and returns the resulting JSON config bytes along with // whether a config file was loaded or not. -func loadConfig(configFile, adapterName string) ([]byte, bool, error) { +func loadConfig(configFile, adapterName string) ([]byte, string, error) { // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { - return nil, false, fmt.Errorf("cannot adapt config without config file (use --config)") + return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") } // load initial config and adapter @@ -112,7 +112,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) { if configFile != "" { config, err = ioutil.ReadFile(configFile) if err != nil { - return nil, false, fmt.Errorf("reading config file: %v", err) + return nil, "", fmt.Errorf("reading config file: %v", err) } caddy.Log().Info("using provided configuration", zap.String("config_file", configFile), @@ -129,7 +129,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) { cfgAdapter = nil } else if err != nil { // default Caddyfile exists, but error reading it - return nil, false, fmt.Errorf("reading default Caddyfile: %v", err) + return nil, "", fmt.Errorf("reading default Caddyfile: %v", err) } else { // success reading default Caddyfile configFile = "Caddyfile" @@ -151,7 +151,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) { if adapterName != "" { cfgAdapter = caddyconfig.GetAdapter(adapterName) if cfgAdapter == nil { - return nil, false, fmt.Errorf("unrecognized config adapter: %s", adapterName) + return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName) } } @@ -161,7 +161,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) { "filename": configFile, }) if err != nil { - return nil, false, fmt.Errorf("adapting config using %s: %v", adapterName, err) + return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err) } for _, warn := range warnings { msg := warn.Message @@ -173,7 +173,85 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) { config = adaptedConfig } - return config, configFile != "", nil + return config, configFile, nil +} + +// watchConfigFile watches the config file at filename for changes +// and reloads the config if the file was updated. This function +// blocks indefinitely; it only quits if the poller has errors for +// long enough time. The filename passed in must be the actual +// config file used, not one to be discovered. +func watchConfigFile(filename, adapterName string) { + // make our logger; since config reloads can change the + // default logger, we need to get it dynamically each time + logger := func() *zap.Logger { + return caddy.Log(). + Named("watcher"). + With(zap.String("config_file", filename)) + } + + // get the initial timestamp on the config file + info, err := os.Stat(filename) + if err != nil { + logger().Error("cannot watch config file", zap.Error(err)) + return + } + lastModified := info.ModTime() + + logger().Info("watching config file for changes") + + // if the file disappears or something, we can + // stop polling if the error lasts long enough + var lastErr time.Time + finalError := func(err error) bool { + if lastErr.IsZero() { + lastErr = time.Now() + return false + } + if time.Since(lastErr) > 30*time.Second { + logger().Error("giving up watching config file; too many errors", + zap.Error(err)) + return true + } + return false + } + + // begin poller + for range time.Tick(1 * time.Second) { + // get the file info + info, err := os.Stat(filename) + if err != nil { + if finalError(err) { + return + } + continue + } + lastErr = time.Time{} // no error, so clear any memory of one + + // if it hasn't changed, nothing to do + if !info.ModTime().After(lastModified) { + continue + } + + logger().Info("config file changed; reloading") + + // remember this timestamp + lastModified = info.ModTime() + + // load the contents of the file + config, _, err := loadConfig(filename, adapterName) + if err != nil { + logger().Error("unable to load latest config", zap.Error(err)) + continue + } + + // apply the updated config + err = caddy.Load(config, false) + if err != nil { + logger().Error("applying latest config", zap.Error(err)) + continue + } + } } // Flags wraps a FlagSet so that typed values -- cgit v1.2.3