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