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 {