mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 22:23:48 +03:00
cmd: Add --watch flag to start & run commands (closes #1806)
Because, just for fun.
This commit is contained in:
parent
8cc60e6896
commit
6e2fabb2a4
3 changed files with 110 additions and 14 deletions
|
@ -42,6 +42,7 @@ import (
|
||||||
func cmdStart(fl Flags) (int, error) {
|
func cmdStart(fl Flags) (int, error) {
|
||||||
startCmdConfigFlag := fl.String("config")
|
startCmdConfigFlag := fl.String("config")
|
||||||
startCmdConfigAdapterFlag := fl.String("adapter")
|
startCmdConfigAdapterFlag := fl.String("adapter")
|
||||||
|
startCmdWatchFlag := fl.Bool("watch")
|
||||||
|
|
||||||
// open a listener to which the child process will connect when
|
// open a listener to which the child process will connect when
|
||||||
// it is ready to confirm that it has successfully started
|
// it is ready to confirm that it has successfully started
|
||||||
|
@ -68,6 +69,9 @@ func cmdStart(fl Flags) (int, error) {
|
||||||
if startCmdConfigAdapterFlag != "" {
|
if startCmdConfigAdapterFlag != "" {
|
||||||
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
|
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
|
||||||
}
|
}
|
||||||
|
if startCmdWatchFlag {
|
||||||
|
cmd.Args = append(cmd.Args, "--watch")
|
||||||
|
}
|
||||||
stdinpipe, err := cmd.StdinPipe()
|
stdinpipe, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
|
@ -145,6 +149,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||||
runCmdConfigAdapterFlag := fl.String("adapter")
|
runCmdConfigAdapterFlag := fl.String("adapter")
|
||||||
runCmdResumeFlag := fl.Bool("resume")
|
runCmdResumeFlag := fl.Bool("resume")
|
||||||
runCmdPrintEnvFlag := fl.Bool("environ")
|
runCmdPrintEnvFlag := fl.Bool("environ")
|
||||||
|
runCmdWatchFlag := fl.Bool("watch")
|
||||||
runCmdPingbackFlag := fl.String("pingback")
|
runCmdPingbackFlag := fl.String("pingback")
|
||||||
|
|
||||||
// if we are supposed to print the environment, do that first
|
// if we are supposed to print the environment, do that first
|
||||||
|
@ -171,8 +176,9 @@ func cmdRun(fl Flags) (int, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||||
|
var configFile string
|
||||||
if !runCmdResumeFlag {
|
if !runCmdResumeFlag {
|
||||||
config, _, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
|
@ -211,6 +217,12 @@ func cmdRun(fl Flags) (int, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if enabled, reload config file automatically on changes
|
||||||
|
// (this better only be used in dev!)
|
||||||
|
if runCmdWatchFlag {
|
||||||
|
go watchConfigFile(configFile, runCmdConfigAdapterFlag)
|
||||||
|
}
|
||||||
|
|
||||||
// warn if the environment does not provide enough information about the disk
|
// warn if the environment does not provide enough information about the disk
|
||||||
hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
|
hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
|
||||||
os.Getenv("XDG_CONFIG_HOME") != "" &&
|
os.Getenv("XDG_CONFIG_HOME") != "" &&
|
||||||
|
@ -266,11 +278,11 @@ func cmdReload(fl Flags) (int, error) {
|
||||||
reloadCmdAddrFlag := fl.String("address")
|
reloadCmdAddrFlag := fl.String("address")
|
||||||
|
|
||||||
// get the config in caddy's native format
|
// get the config in caddy's native format
|
||||||
config, hasConfig, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
|
config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
if !hasConfig {
|
if configFile == "" {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ func init() {
|
||||||
RegisterCommand(Command{
|
RegisterCommand(Command{
|
||||||
Name: "start",
|
Name: "start",
|
||||||
Func: cmdStart,
|
Func: cmdStart,
|
||||||
Usage: "[--config <path> [[--adapter <name>]]",
|
Usage: "[--config <path> [--adapter <name>]] [--watch]",
|
||||||
Short: "Starts the Caddy process in the background and then returns",
|
Short: "Starts the Caddy process in the background and then returns",
|
||||||
Long: `
|
Long: `
|
||||||
Starts the Caddy process, optionally bootstrapped with an initial config file.
|
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.`,
|
||||||
fs := flag.NewFlagSet("start", flag.ExitOnError)
|
fs := flag.NewFlagSet("start", flag.ExitOnError)
|
||||||
fs.String("config", "", "Configuration file")
|
fs.String("config", "", "Configuration file")
|
||||||
fs.String("adapter", "", "Name of config adapter to apply")
|
fs.String("adapter", "", "Name of config adapter to apply")
|
||||||
|
fs.Bool("watch", false, "Reload changed config file automatically")
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
})
|
})
|
||||||
|
@ -94,7 +95,7 @@ using 'caddy run' instead to keep it in the foreground.`,
|
||||||
RegisterCommand(Command{
|
RegisterCommand(Command{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Func: cmdRun,
|
Func: cmdRun,
|
||||||
Usage: "[--config <path> [--adapter <name>]] [--environ]",
|
Usage: "[--config <path> [--adapter <name>]] [--environ] [--watch]",
|
||||||
Short: `Starts the Caddy process and blocks indefinitely`,
|
Short: `Starts the Caddy process and blocks indefinitely`,
|
||||||
Long: `
|
Long: `
|
||||||
Starts the Caddy process, optionally bootstrapped with an initial config file,
|
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
|
||||||
not quit after printing, and can be useful for troubleshooting.
|
not quit after printing, and can be useful for troubleshooting.
|
||||||
|
|
||||||
The --resume flag will override the --config flag if there is a config auto-
|
The --resume flag will override the --config flag if there is a config auto-
|
||||||
save file. It is not an error if --resume is used and no autosave file exists.`,
|
save file. It is not an error if --resume is used and no autosave file exists.
|
||||||
|
|
||||||
|
If --watch is specified, the config file will be loaded automatically after
|
||||||
|
changes. ⚠️ This is dangerous in production! Only use this option in a local
|
||||||
|
development environment.`,
|
||||||
Flags: func() *flag.FlagSet {
|
Flags: func() *flag.FlagSet {
|
||||||
fs := flag.NewFlagSet("run", flag.ExitOnError)
|
fs := flag.NewFlagSet("run", flag.ExitOnError)
|
||||||
fs.String("config", "", "Configuration file")
|
fs.String("config", "", "Configuration file")
|
||||||
fs.String("adapter", "", "Name of config adapter to apply")
|
fs.String("adapter", "", "Name of config adapter to apply")
|
||||||
fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
|
|
||||||
fs.Bool("environ", false, "Print environment")
|
fs.Bool("environ", false, "Print environment")
|
||||||
|
fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
|
||||||
|
fs.Bool("watch", false, "Watch config file for changes and reload it automatically")
|
||||||
fs.String("pingback", "", "Echo confirmation bytes to this address on success")
|
fs.String("pingback", "", "Echo confirmation bytes to this address on success")
|
||||||
return fs
|
return fs
|
||||||
}(),
|
}(),
|
||||||
|
|
92
cmd/main.go
92
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,
|
// there is no config available. It prints any warnings to stderr,
|
||||||
// and returns the resulting JSON config bytes along with
|
// and returns the resulting JSON config bytes along with
|
||||||
// whether a config file was loaded or not.
|
// 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
|
// specifying an adapter without a config file is ambiguous
|
||||||
if adapterName != "" && configFile == "" {
|
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
|
// load initial config and adapter
|
||||||
|
@ -112,7 +112,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
|
||||||
if configFile != "" {
|
if configFile != "" {
|
||||||
config, err = ioutil.ReadFile(configFile)
|
config, err = ioutil.ReadFile(configFile)
|
||||||
if err != nil {
|
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",
|
caddy.Log().Info("using provided configuration",
|
||||||
zap.String("config_file", configFile),
|
zap.String("config_file", configFile),
|
||||||
|
@ -129,7 +129,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
|
||||||
cfgAdapter = nil
|
cfgAdapter = nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
// default Caddyfile exists, but error reading it
|
// 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 {
|
} else {
|
||||||
// success reading default Caddyfile
|
// success reading default Caddyfile
|
||||||
configFile = "Caddyfile"
|
configFile = "Caddyfile"
|
||||||
|
@ -151,7 +151,7 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
|
||||||
if adapterName != "" {
|
if adapterName != "" {
|
||||||
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
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,
|
"filename": configFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 {
|
for _, warn := range warnings {
|
||||||
msg := warn.Message
|
msg := warn.Message
|
||||||
|
@ -173,7 +173,85 @@ func loadConfig(configFile, adapterName string) ([]byte, bool, error) {
|
||||||
config = adaptedConfig
|
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
|
// Flags wraps a FlagSet so that typed values
|
||||||
|
|
Loading…
Reference in a new issue