cmd: Improve stop command by trying API before signaling process

This allows graceful shutdown on all platforms
This commit is contained in:
Matthew Holt 2019-11-15 15:45:18 -07:00
parent 0ca109db4a
commit 6cdb2392d7
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
4 changed files with 87 additions and 39 deletions

View file

@ -35,6 +35,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/keybase/go-ps" "github.com/keybase/go-ps"
"github.com/mholt/certmagic" "github.com/mholt/certmagic"
"go.uber.org/zap"
) )
func cmdStart(fl Flags) (int, error) { func cmdStart(fl Flags) (int, error) {
@ -193,19 +194,43 @@ func cmdRun(fl Flags) (int, error) {
select {} select {}
} }
func cmdStop(_ Flags) (int, error) { func cmdStop(fl Flags) (int, error) {
stopCmdAddrFlag := fl.String("address")
adminAddr := caddy.DefaultAdminListen
if stopCmdAddrFlag != "" {
adminAddr = stopCmdAddrFlag
}
stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr)
req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
}
req.Header.Set("Origin", adminAddr)
err = apiRequest(req)
if err != nil {
// if the caddy instance doesn't have an API listener set up,
// or we are unable to reach it for some reason, try signaling it
caddy.Log().Warn("unable to use API to stop instance; will try to signal the process",
zap.String("endpoint", stopEndpoint),
zap.Error(err),
)
processList, err := ps.Processes() processList, err := ps.Processes()
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
} }
thisProcName := getProcessName() thisProcName := getProcessName()
var found bool var found bool
for _, p := range processList { for _, p := range processList {
// the process we're looking for should have the same name but different PID // the process we're looking for should have the same name as us but different PID
if p.Executable() == thisProcName && p.Pid() != os.Getpid() { if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
found = true found = true
fmt.Printf("pid=%d\n", p.Pid()) fmt.Printf("pid=%d\n", p.Pid())
if err := gracefullyStopProcess(p.Pid()); err != nil { if err := gracefullyStopProcess(p.Pid()); err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@ -214,7 +239,10 @@ func cmdStop(_ Flags) (int, error) {
if !found { if !found {
return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running") return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
} }
fmt.Println(" success") fmt.Println(" success")
}
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
@ -251,25 +279,19 @@ func cmdReload(fl Flags) (int, error) {
if adminAddr == "" { if adminAddr == "" {
adminAddr = caddy.DefaultAdminListen adminAddr = caddy.DefaultAdminListen
} }
adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr) loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
// send the configuration to the instance // prepare the request to update the configuration
resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config)) req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config))
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
fmt.Errorf("sending configuration to instance: %v", err)
} }
defer resp.Body.Close() req.Header.Set("Content-Type", "application/json")
req.Header.Set("Origin", adminAddr)
// if it didn't work, let the user know err = apiRequest(req)
if resp.StatusCode >= 400 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
}
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
} }
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
@ -522,3 +544,22 @@ commands:
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func apiRequest(req *http.Request) error {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("performing request: %v", err)
}
defer resp.Body.Close()
// if it didn't work, let the user know
if resp.StatusCode >= 400 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
if err != nil {
return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
}
return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
}
return nil
}

View file

@ -134,11 +134,18 @@ not quit after printing, and can be useful for troubleshooting.`,
Long: ` Long: `
Stops the background Caddy process as gracefully as possible. Stops the background Caddy process as gracefully as possible.
On Windows, this stop is forceful and Caddy will not have an opportunity to It will first try to use the admin API's /stop endpoint; the address of
clean up any active locks; for a graceful shutdown on Windows, use Ctrl+C this request can be customized using the --address flag if it is not the
or the /stop API endpoint. default.
Note: this will stop any process named the same as the executable (os.Args[0]).`, If that fails for any reason, it will attempt to signal the first process
it can find named the same as this one (os.Args[0]). On Windows, such
a stop is forceful because Windows does not have signals.`,
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("stop", flag.ExitOnError)
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
return fs
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{

View file

@ -24,7 +24,7 @@ import (
) )
func gracefullyStopProcess(pid int) error { func gracefullyStopProcess(pid int) error {
fmt.Printf("Graceful stop...\n") fmt.Print("Graceful stop... ")
err := syscall.Kill(pid, syscall.SIGINT) err := syscall.Kill(pid, syscall.SIGINT)
if err != nil { if err != nil {
return fmt.Errorf("kill: %v", err) return fmt.Errorf("kill: %v", err)

View file

@ -23,7 +23,7 @@ import (
) )
func gracefullyStopProcess(pid int) error { func gracefullyStopProcess(pid int) error {
fmt.Printf("Forceful Stop...\n") fmt.Print("Forceful stop... ")
// process on windows will not stop unless forced with /f // process on windows will not stop unless forced with /f
cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/f") cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/f")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {