@marcel91200 / caddy Mirror · read-only / commits / 6cdb2392
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: Improve stop command by trying API before signaling process

This allows graceful shutdown on all platforms

Matthew Holt committed Nov 15, 2019 at 15:45 UTC 6cdb2392d73992099955f9ce859748dea97cf4df
4 files changed +87 -39
cmd/commandfuncs.go
+74 -33
@@ -35,6 +35,7 @@ import (
35 "github.com/caddyserver/caddy/v2/caddyconfig"
36 "github.com/keybase/go-ps"
37 "github.com/mholt/certmagic"
38 + "go.uber.org/zap"
39 )
40
41 func cmdStart(fl Flags) (int, error) {
@@ -193,28 +194,55 @@ func cmdRun(fl Flags) (int, error) {
194 select {}
195 }
196
196 -func cmdStop(_ Flags) (int, error) {
197 - processList, err := ps.Processes()
197 +func cmdStop(fl Flags) (int, error) {
198 + stopCmdAddrFlag := fl.String("address")
199 +
200 + adminAddr := caddy.DefaultAdminListen
201 + if stopCmdAddrFlag != "" {
202 + adminAddr = stopCmdAddrFlag
203 + }
204 + stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr)
205 +
206 + req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil)
207 + if err != nil {
208 + return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
209 + }
210 + req.Header.Set("Origin", adminAddr)
211 +
212 + err = apiRequest(req)
213 if err != nil {
199 - return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
200 - }
201 - thisProcName := getProcessName()
202 - var found bool
203 - for _, p := range processList {
204 - // the process we're looking for should have the same name but different PID
205 - if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
206 - found = true
207 - fmt.Printf("pid=%d\n", p.Pid())
208 -
209 - if err := gracefullyStopProcess(p.Pid()); err != nil {
210 - return caddy.ExitCodeFailedStartup, err
214 + // if the caddy instance doesn't have an API listener set up,
215 + // or we are unable to reach it for some reason, try signaling it
216 +
217 + caddy.Log().Warn("unable to use API to stop instance; will try to signal the process",
218 + zap.String("endpoint", stopEndpoint),
219 + zap.Error(err),
220 + )
221 +
222 + processList, err := ps.Processes()
223 + if err != nil {
224 + return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
225 + }
226 + thisProcName := getProcessName()
227 +
228 + var found bool
229 + for _, p := range processList {
230 + // the process we're looking for should have the same name as us but different PID
231 + if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
232 + found = true
233 + fmt.Printf("pid=%d\n", p.Pid())
234 + if err := gracefullyStopProcess(p.Pid()); err != nil {
235 + return caddy.ExitCodeFailedStartup, err
236 + }
237 }
238 }
239 + if !found {
240 + return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
241 + }
242 +
243 + fmt.Println(" success")
244 }
214 - if !found {
215 - return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
216 - }
217 - fmt.Println(" success")
245 +
246 return caddy.ExitCodeSuccess, nil
247 }
248
@@ -251,25 +279,19 @@ func cmdReload(fl Flags) (int, error) {
279 if adminAddr == "" {
280 adminAddr = caddy.DefaultAdminListen
281 }
254 - adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
282 + loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
283
256 - // send the configuration to the instance
257 - resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
284 + // prepare the request to update the configuration
285 + req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config))
286 if err != nil {
259 - return caddy.ExitCodeFailedStartup,
260 - fmt.Errorf("sending configuration to instance: %v", err)
287 + return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
288 }
262 - defer resp.Body.Close()
289 + req.Header.Set("Content-Type", "application/json")
290 + req.Header.Set("Origin", adminAddr)
291
264 - // if it didn't work, let the user know
265 - if resp.StatusCode >= 400 {
266 - respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
267 - if err != nil {
268 - return caddy.ExitCodeFailedStartup,
269 - fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
270 - }
271 - return caddy.ExitCodeFailedStartup,
272 - fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
292 + err = apiRequest(req)
293 + if err != nil {
294 + return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
295 }
296
297 return caddy.ExitCodeSuccess, nil
@@ -522,3 +544,22 @@ commands:
544
545 return caddy.ExitCodeSuccess, nil
546 }
547 +
548 +func apiRequest(req *http.Request) error {
549 + resp, err := http.DefaultClient.Do(req)
550 + if err != nil {
551 + return fmt.Errorf("performing request: %v", err)
552 + }
553 + defer resp.Body.Close()
554 +
555 + // if it didn't work, let the user know
556 + if resp.StatusCode >= 400 {
557 + respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
558 + if err != nil {
559 + return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
560 + }
561 + return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
562 + }
563 +
564 + return nil
565 +}
cmd/commands.go
+11 -4
@@ -134,11 +134,18 @@ not quit after printing, and can be useful for troubleshooting.`,
134 Long: `
135 Stops the background Caddy process as gracefully as possible.
136
137 -On Windows, this stop is forceful and Caddy will not have an opportunity to
138 -clean up any active locks; for a graceful shutdown on Windows, use Ctrl+C
139 -or the /stop API endpoint.
137 +It will first try to use the admin API's /stop endpoint; the address of
138 +this request can be customized using the --address flag if it is not the
139 +default.
140
141 -Note: this will stop any process named the same as the executable (os.Args[0]).`,
141 +If that fails for any reason, it will attempt to signal the first process
142 +it can find named the same as this one (os.Args[0]). On Windows, such
143 +a stop is forceful because Windows does not have signals.`,
144 + Flags: func() *flag.FlagSet {
145 + fs := flag.NewFlagSet("stop", flag.ExitOnError)
146 + fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
147 + return fs
148 + }(),
149 })
150
151 RegisterCommand(Command{
cmd/proc_posix.go
+1 -1
@@ -24,7 +24,7 @@ import (
24 )
25
26 func gracefullyStopProcess(pid int) error {
27 - fmt.Printf("Graceful stop...\n")
27 + fmt.Print("Graceful stop... ")
28 err := syscall.Kill(pid, syscall.SIGINT)
29 if err != nil {
30 return fmt.Errorf("kill: %v", err)
cmd/proc_windows.go
+1 -1
@@ -23,7 +23,7 @@ import (
23 )
24
25 func gracefullyStopProcess(pid int) error {
26 - fmt.Printf("Forceful Stop...\n")
26 + fmt.Print("Forceful stop... ")
27 // process on windows will not stop unless forced with /f
28 cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid), "/f")
29 if err := cmd.Run(); err != nil {