Standardize exit codes and improve shutdown handling; update gitignore

This commit is contained in:
Matthew Holt 2019-07-12 10:07:11 -06:00
parent 2141626269
commit b780f0f49b
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
7 changed files with 243 additions and 38 deletions

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
_gitignore/ _gitignore/
*.log
Caddyfile
# artifacts from pprof tooling # artifacts from pprof tooling
*.prof *.prof

View file

@ -150,7 +150,35 @@ func Run(newCfg *Config) error {
currentCfg = newCfg currentCfg = newCfg
// Stop, Cleanup each old app // Stop, Cleanup each old app
if oldCfg != nil { unsyncedStop(oldCfg)
return nil
}
// Stop stops running the current configuration.
// It is the antithesis of Run(). This function
// will log any errors that occur during the
// stopping of individual apps and continue to
// stop the others.
func Stop() error {
currentCfgMu.Lock()
defer currentCfgMu.Unlock()
unsyncedStop(currentCfg)
currentCfg = nil
return nil
}
// unsyncedStop stops oldCfg from running, but if
// applicable, you need to acquire locks yourself.
// It is a no-op if oldCfg is nil. If any app
// returns an error when stopping, it is logged
// and the function continues with the next app.
func unsyncedStop(oldCfg *Config) {
if oldCfg == nil {
return
}
// stop each app
for name, a := range oldCfg.apps { for name, a := range oldCfg.apps {
err := a.Stop() err := a.Stop()
if err != nil { if err != nil {
@ -162,9 +190,6 @@ func Run(newCfg *Config) error {
oldCfg.cancelFunc() oldCfg.cancelFunc()
} }
return nil
}
// Duration is a JSON-string-unmarshable duration type. // Duration is a JSON-string-unmarshable duration type.
type Duration time.Duration type Duration time.Duration
@ -199,6 +224,7 @@ func GoModule() *debug.Module {
} }
// goModule is the name of this Go module. // goModule is the name of this Go module.
// TODO: we should be able to find this at runtime, see https://github.com/golang/go/issues/29228
const goModule = "github.com/caddyserver/caddy/v2" const goModule = "github.com/caddyserver/caddy/v2"
// CtxKey is a value type for use with context.WithValue. // CtxKey is a value type for use with context.WithValue.

View file

@ -44,7 +44,8 @@ func cmdStart() (int, error) {
// it is ready to confirm that it has successfully started // it is ready to confirm that it has successfully started
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
return 1, fmt.Errorf("opening listener for success confirmation: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("opening listener for success confirmation: %v", err)
} }
defer ln.Close() defer ln.Close()
@ -63,7 +64,8 @@ func cmdStart() (int, error) {
} }
stdinpipe, err := cmd.StdinPipe() stdinpipe, err := cmd.StdinPipe()
if err != nil { if err != nil {
return 1, fmt.Errorf("creating stdin pipe: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("creating stdin pipe: %v", err)
} }
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -72,7 +74,7 @@ func cmdStart() (int, error) {
expect := make([]byte, 32) expect := make([]byte, 32)
_, err = rand.Read(expect) _, err = rand.Read(expect)
if err != nil { if err != nil {
return 1, fmt.Errorf("generating random confirmation bytes: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
} }
// begin writing the confirmation bytes to the child's // begin writing the confirmation bytes to the child's
@ -87,7 +89,7 @@ func cmdStart() (int, error) {
// start the process // start the process
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return 1, fmt.Errorf("starting caddy process: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
} }
// there are two ways we know we're done: either // there are two ways we know we're done: either
@ -125,10 +127,11 @@ func cmdStart() (int, error) {
case <-success: case <-success:
fmt.Println("Successfully started Caddy") fmt.Println("Successfully started Caddy")
case err := <-exit: case err := <-exit:
return 1, fmt.Errorf("caddy process exited with error: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy process exited with error: %v", err)
} }
return 0, nil return caddy.ExitCodeSuccess, nil
} }
func cmdRun() (int, error) { func cmdRun() (int, error) {
@ -144,7 +147,8 @@ func cmdRun() (int, error) {
var err error var err error
config, err = ioutil.ReadFile(*runCmdConfigFlag) config, err = ioutil.ReadFile(*runCmdConfigFlag)
if err != nil { if err != nil {
return 1, fmt.Errorf("reading config file: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading config file: %v", err)
} }
} }
@ -156,7 +160,8 @@ func cmdRun() (int, error) {
// start the admin endpoint along with any initial config // start the admin endpoint along with any initial config
err := caddy.StartAdmin(config) err := caddy.StartAdmin(config)
if err != nil { if err != nil {
return 1, fmt.Errorf("starting caddy administration endpoint: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("starting caddy administration endpoint: %v", err)
} }
defer caddy.StopAdmin() defer caddy.StopAdmin()
@ -165,16 +170,19 @@ func cmdRun() (int, error) {
if *runCmdPingbackFlag != "" { if *runCmdPingbackFlag != "" {
confirmationBytes, err := ioutil.ReadAll(os.Stdin) confirmationBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil { if err != nil {
return 1, fmt.Errorf("reading confirmation bytes from stdin: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
} }
conn, err := net.Dial("tcp", *runCmdPingbackFlag) conn, err := net.Dial("tcp", *runCmdPingbackFlag)
if err != nil { if err != nil {
return 1, fmt.Errorf("dialing confirmation address: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("dialing confirmation address: %v", err)
} }
defer conn.Close() defer conn.Close()
_, err = conn.Write(confirmationBytes) _, err = conn.Write(confirmationBytes)
if err != nil { if err != nil {
return 1, fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err)
} }
} }
@ -184,7 +192,7 @@ func cmdRun() (int, error) {
func cmdStop() (int, error) { func cmdStop() (int, error) {
processList, err := ps.Processes() processList, err := ps.Processes()
if err != nil { if err != nil {
return 1, fmt.Errorf("listing processes: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
} }
thisProcName := filepath.Base(os.Args[0]) thisProcName := filepath.Base(os.Args[0])
var found bool var found bool
@ -195,15 +203,15 @@ func cmdStop() (int, error) {
fmt.Printf("pid=%d\n", p.Pid()) fmt.Printf("pid=%d\n", p.Pid())
fmt.Printf("Graceful stop...") fmt.Printf("Graceful stop...")
if err := gracefullyStopProcess(p.Pid()); err != nil { if err := gracefullyStopProcess(p.Pid()); err != nil {
return 1, err return caddy.ExitCodeFailedStartup, err
} }
} }
} }
if !found { if !found {
return 1, fmt.Errorf("Caddy is not running") return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
} }
fmt.Println(" success") fmt.Println(" success")
return 0, nil return caddy.ExitCodeSuccess, nil
} }
func cmdReload() (int, error) { func cmdReload() (int, error) {
@ -214,13 +222,15 @@ func cmdReload() (int, error) {
// a configuration is required // a configuration is required
if *reloadCmdConfigFlag == "" { if *reloadCmdConfigFlag == "" {
return 1, fmt.Errorf("no configuration to load (use --config)") return caddy.ExitCodeFailedStartup,
fmt.Errorf("no configuration to load (use --config)")
} }
// load the configuration file // load the configuration file
config, err := ioutil.ReadFile(*reloadCmdConfigFlag) config, err := ioutil.ReadFile(*reloadCmdConfigFlag)
if err != nil { if err != nil {
return 1, fmt.Errorf("reading config file: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading config file: %v", err)
} }
// get the address of the admin listener and craft endpoint URL // get the address of the admin listener and craft endpoint URL
@ -231,7 +241,8 @@ func cmdReload() (int, error) {
} }
err = json.Unmarshal(config, &tmpStruct) err = json.Unmarshal(config, &tmpStruct)
if err != nil { if err != nil {
return 1, fmt.Errorf("unmarshaling admin listener address from config: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("unmarshaling admin listener address from config: %v", err)
} }
adminAddr = tmpStruct.Admin.Listen adminAddr = tmpStruct.Admin.Listen
} }
@ -243,7 +254,8 @@ func cmdReload() (int, error) {
// send the configuration to the instance // send the configuration to the instance
resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config)) resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
if err != nil { if err != nil {
return 1, fmt.Errorf("sending configuration to instance: %v", err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("sending configuration to instance: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -251,12 +263,14 @@ func cmdReload() (int, error) {
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10)) respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
if err != nil { if err != nil {
return 1, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) return caddy.ExitCodeFailedStartup,
fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
} }
return 1, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
} }
return 0, nil return caddy.ExitCodeSuccess, nil
} }
func cmdVersion() (int, error) { func cmdVersion() (int, error) {
@ -267,19 +281,19 @@ func cmdVersion() (int, error) {
} else { } else {
fmt.Println(goModule.Version) fmt.Println(goModule.Version)
} }
return 0, nil return caddy.ExitCodeSuccess, nil
} }
func cmdListModules() (int, error) { func cmdListModules() (int, error) {
for _, m := range caddy.Modules() { for _, m := range caddy.Modules() {
fmt.Println(m) fmt.Println(m)
} }
return 0, nil return caddy.ExitCodeSuccess, nil
} }
func cmdEnviron() (int, error) { func cmdEnviron() (int, error) {
for _, v := range os.Environ() { for _, v := range os.Environ() {
fmt.Println(v) fmt.Println(v)
} }
return 0, nil return caddy.ExitCodeSuccess, nil
} }

View file

@ -23,10 +23,15 @@ import (
"log" "log"
"net" "net"
"os" "os"
"github.com/caddyserver/caddy/v2"
) )
// Main executes the main function of the caddy command. // Main implements the main function of the caddy command.
// Call this if Caddy is to be the main() if your program.
func Main() { func Main() {
caddy.TrapSignals()
if len(os.Args) <= 1 { if len(os.Args) <= 1 {
fmt.Println(usageString()) fmt.Println(usageString())
return return
@ -35,7 +40,7 @@ func Main() {
subcommand, ok := commands[os.Args[1]] subcommand, ok := commands[os.Args[1]]
if !ok { if !ok {
fmt.Printf("%q is not a valid command\n", os.Args[1]) fmt.Printf("%q is not a valid command\n", os.Args[1])
os.Exit(2) os.Exit(caddy.ExitCodeFailedStartup)
} }
if exitCode, err := subcommand(); err != nil { if exitCode, err := subcommand(); err != nil {

82
sigtrap.go Normal file
View file

@ -0,0 +1,82 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddy
import (
"log"
"os"
"os/signal"
"github.com/mholt/certmagic"
)
// TrapSignals create signal/interrupt handlers as best it can for the
// current OS. This is a rather invasive function to call in a Go program
// that captures signals already, so in that case it would be better to
// implement these handlers yourself.
func TrapSignals() {
trapSignalsCrossPlatform()
trapSignalsPosix()
}
// trapSignalsCrossPlatform captures SIGINT or interrupt (depending
// on the OS), which initiates a graceful shutdown. A second SIGINT
// or interrupt will forcefully exit the process immediately.
func trapSignalsCrossPlatform() {
go func() {
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, os.Interrupt)
for i := 0; true; i++ {
<-shutdown
if i > 0 {
log.Println("[INFO] SIGINT: Force quit")
os.Exit(ExitCodeForceQuit)
}
log.Println("[INFO] SIGINT: Shutting down")
go gracefulStop("SIGINT")
}
}()
}
// gracefulStop exits the process as gracefully as possible.
func gracefulStop(sigName string) {
exitCode := ExitCodeSuccess
// first stop all the apps
err := Stop()
if err != nil {
log.Printf("[ERROR] %s stop: %v", sigName, err)
exitCode = ExitCodeFailedQuit
}
// always, always, always try to clean up locks
certmagic.CleanUpOwnLocks()
log.Printf("[INFO] %s: Shutdown done", sigName)
os.Exit(exitCode)
}
// Exit codes. Generally, you will want to avoid
// automatically restarting the process if the
// exit code is 1.
const (
ExitCodeSuccess = iota
ExitCodeFailedStartup
ExitCodeForceQuit
ExitCodeFailedQuit
)

19
sigtrap_nonposix.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build windows plan9 nacl js
package caddy
func trapSignalsPosix() {}

57
sigtrap_posix.go Normal file
View file

@ -0,0 +1,57 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build !windows,!plan9,!nacl,!js
package caddy
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/mholt/certmagic"
)
// trapSignalsPosix captures POSIX-only signals.
func trapSignalsPosix() {
go func() {
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
for sig := range sigchan {
switch sig {
case syscall.SIGQUIT:
log.Println("[INFO] SIGQUIT: Quitting process immediately")
certmagic.CleanUpOwnLocks() // try to clean up locks anyway, it's important
os.Exit(ExitCodeForceQuit)
case syscall.SIGTERM:
log.Println("[INFO] SIGTERM: Shutting down apps then terminating")
gracefulStop("SIGTERM")
case syscall.SIGUSR1:
log.Println("[INFO] SIGUSR1: Not implemented")
case syscall.SIGUSR2:
log.Println("[INFO] SIGUSR2: Not implemented")
case syscall.SIGHUP:
// ignore; this signal is sometimes sent outside of the user's control
log.Println("[INFO] SIGHUP: Not implemented")
}
}
}()
}