@marcel91200 / caddy Mirror · read-only / commits / 6e2fabb2
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

cmd: Add --watch flag to start & run commands (closes #1806)

Because, just for fun.

Matthew Holt committed Mar 22, 2020 at 22:58 UTC 6e2fabb2a46812dee42842c13439e1a0238aa40b
3 files changed +110 -14
cmd/commandfuncs.go
+15 -3
@@ -42,6 +42,7 @@ import (
42 func cmdStart(fl Flags) (int, error) {
43 startCmdConfigFlag := fl.String("config")
44 startCmdConfigAdapterFlag := fl.String("adapter")
45 + startCmdWatchFlag := fl.Bool("watch")
46
47 // open a listener to which the child process will connect when
48 // it is ready to confirm that it has successfully started
@@ -68,6 +69,9 @@ func cmdStart(fl Flags) (int, error) {
69 if startCmdConfigAdapterFlag != "" {
70 cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
71 }
72 + if startCmdWatchFlag {
73 + cmd.Args = append(cmd.Args, "--watch")
74 + }
75 stdinpipe, err := cmd.StdinPipe()
76 if err != nil {
77 return caddy.ExitCodeFailedStartup,
@@ -145,6 +149,7 @@ func cmdRun(fl Flags) (int, error) {
149 runCmdConfigAdapterFlag := fl.String("adapter")
150 runCmdResumeFlag := fl.Bool("resume")
151 runCmdPrintEnvFlag := fl.Bool("environ")
152 + runCmdWatchFlag := fl.Bool("watch")
153 runCmdPingbackFlag := fl.String("pingback")
154
155 // if we are supposed to print the environment, do that first
@@ -171,8 +176,9 @@ func cmdRun(fl Flags) (int, error) {
176 }
177 }
178 // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
179 + var configFile string
180 if !runCmdResumeFlag {
175 - config, _, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
181 + config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
182 if err != nil {
183 return caddy.ExitCodeFailedStartup, err
184 }
@@ -211,6 +217,12 @@ func cmdRun(fl Flags) (int, error) {
217 }
218 }
219
220 + // if enabled, reload config file automatically on changes
221 + // (this better only be used in dev!)
222 + if runCmdWatchFlag {
223 + go watchConfigFile(configFile, runCmdConfigAdapterFlag)
224 + }
225 +
226 // warn if the environment does not provide enough information about the disk
227 hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
228 os.Getenv("XDG_CONFIG_HOME") != "" &&
@@ -266,11 +278,11 @@ func cmdReload(fl Flags) (int, error) {
278 reloadCmdAddrFlag := fl.String("address")
279
280 // get the config in caddy's native format
269 - config, hasConfig, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
281 + config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
282 if err != nil {
283 return caddy.ExitCodeFailedStartup, err
284 }
273 - if !hasConfig {
285 + if configFile == "" {
286 return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
287 }
288
cmd/commands.go
+10 -4
@@ -74,7 +74,7 @@ func init() {
74 RegisterCommand(Command{
75 Name: "start",
76 Func: cmdStart,
77 - Usage: "[--config <path> [[--adapter <name>]]",
77 + Usage: "[--config <path> [--adapter <name>]] [--watch]",
78 Short: "Starts the Caddy process in the background and then returns",
79 Long: `
80 Starts the Caddy process, optionally bootstrapped with an initial config file.
@@ -87,6 +87,7 @@ using 'caddy run' instead to keep it in the foreground.`,
87 fs := flag.NewFlagSet("start", flag.ExitOnError)
88 fs.String("config", "", "Configuration file")
89 fs.String("adapter", "", "Name of config adapter to apply")
90 + fs.Bool("watch", false, "Reload changed config file automatically")
91 return fs
92 }(),
93 })
@@ -94,7 +95,7 @@ using 'caddy run' instead to keep it in the foreground.`,
95 RegisterCommand(Command{
96 Name: "run",
97 Func: cmdRun,
97 - Usage: "[--config <path> [--adapter <name>]] [--environ]",
98 + Usage: "[--config <path> [--adapter <name>]] [--environ] [--watch]",
99 Short: `Starts the Caddy process and blocks indefinitely`,
100 Long: `
101 Starts the Caddy process, optionally bootstrapped with an initial config file,
@@ -119,13 +120,18 @@ be printed before starting. This is the same as the environ command but does
120 not quit after printing, and can be useful for troubleshooting.
121
122 The --resume flag will override the --config flag if there is a config auto-
122 -save file. It is not an error if --resume is used and no autosave file exists.`,
123 +save file. It is not an error if --resume is used and no autosave file exists.
124 +
125 +If --watch is specified, the config file will be loaded automatically after
126 +changes. ⚠️ This is dangerous in production! Only use this option in a local
127 +development environment.`,
128 Flags: func() *flag.FlagSet {
129 fs := flag.NewFlagSet("run", flag.ExitOnError)
130 fs.String("config", "", "Configuration file")
131 fs.String("adapter", "", "Name of config adapter to apply")
127 - fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
132 fs.Bool("environ", false, "Print environment")
133 + fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
134 + fs.Bool("watch", false, "Watch config file for changes and reload it automatically")
135 fs.String("pingback", "", "Echo confirmation bytes to this address on success")
136 return fs
137 }(),
cmd/main.go
+85 -7
@@ -99,10 +99,10 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
99 // there is no config available. It prints any warnings to stderr,
100 // and returns the resulting JSON config bytes along with
101 // whether a config file was loaded or not.
102 -func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
102 +func loadConfig(configFile, adapterName string) ([]byte, string, error) {
103 // specifying an adapter without a config file is ambiguous
104 if adapterName != "" && configFile == "" {
105 - return nil, false, fmt.Errorf("cannot adapt config without config file (use --config)")
105 + return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
106 }
107
108 // load initial config and adapter
@@ -112,7 +112,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
112 if configFile != "" {
113 config, err = ioutil.ReadFile(configFile)
114 if err != nil {
115 - return nil, false, fmt.Errorf("reading config file: %v", err)
115 + return nil, "", fmt.Errorf("reading config file: %v", err)
116 }
117 caddy.Log().Info("using provided configuration",
118 zap.String("config_file", configFile),
@@ -129,7 +129,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
129 cfgAdapter = nil
130 } else if err != nil {
131 // default Caddyfile exists, but error reading it
132 - return nil, false, fmt.Errorf("reading default Caddyfile: %v", err)
132 + return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
133 } else {
134 // success reading default Caddyfile
135 configFile = "Caddyfile"
@@ -151,7 +151,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
151 if adapterName != "" {
152 cfgAdapter = caddyconfig.GetAdapter(adapterName)
153 if cfgAdapter == nil {
154 - return nil, false, fmt.Errorf("unrecognized config adapter: %s", adapterName)
154 + return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
155 }
156 }
157
@@ -161,7 +161,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
161 "filename": configFile,
162 })
163 if err != nil {
164 - return nil, false, fmt.Errorf("adapting config using %s: %v", adapterName, err)
164 + return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
165 }
166 for _, warn := range warnings {
167 msg := warn.Message
@@ -173,7 +173,85 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
173 config = adaptedConfig
174 }
175
176 - return config, configFile != "", nil
176 + return config, configFile, nil
177 +}
178 +
179 +// watchConfigFile watches the config file at filename for changes
180 +// and reloads the config if the file was updated. This function
181 +// blocks indefinitely; it only quits if the poller has errors for
182 +// long enough time. The filename passed in must be the actual
183 +// config file used, not one to be discovered.
184 +func watchConfigFile(filename, adapterName string) {
185 + // make our logger; since config reloads can change the
186 + // default logger, we need to get it dynamically each time
187 + logger := func() *zap.Logger {
188 + return caddy.Log().
189 + Named("watcher").
190 + With(zap.String("config_file", filename))
191 + }
192 +
193 + // get the initial timestamp on the config file
194 + info, err := os.Stat(filename)
195 + if err != nil {
196 + logger().Error("cannot watch config file", zap.Error(err))
197 + return
198 + }
199 + lastModified := info.ModTime()
200 +
201 + logger().Info("watching config file for changes")
202 +
203 + // if the file disappears or something, we can
204 + // stop polling if the error lasts long enough
205 + var lastErr time.Time
206 + finalError := func(err error) bool {
207 + if lastErr.IsZero() {
208 + lastErr = time.Now()
209 + return false
210 + }
211 + if time.Since(lastErr) > 30*time.Second {
212 + logger().Error("giving up watching config file; too many errors",
213 + zap.Error(err))
214 + return true
215 + }
216 + return false
217 + }
218 +
219 + // begin poller
220 + for range time.Tick(1 * time.Second) {
221 + // get the file info
222 + info, err := os.Stat(filename)
223 + if err != nil {
224 + if finalError(err) {
225 + return
226 + }
227 + continue
228 + }
229 + lastErr = time.Time{} // no error, so clear any memory of one
230 +
231 + // if it hasn't changed, nothing to do
232 + if !info.ModTime().After(lastModified) {
233 + continue
234 + }
235 +
236 + logger().Info("config file changed; reloading")
237 +
238 + // remember this timestamp
239 + lastModified = info.ModTime()
240 +
241 + // load the contents of the file
242 + config, _, err := loadConfig(filename, adapterName)
243 + if err != nil {
244 + logger().Error("unable to load latest config", zap.Error(err))
245 + continue
246 + }
247 +
248 + // apply the updated config
249 + err = caddy.Load(config, false)
250 + if err != nil {
251 + logger().Error("applying latest config", zap.Error(err))
252 + continue
253 + }
254 + }
255 }
256
257 // Flags wraps a FlagSet so that typed values