From b780f0f49b191a6724b7ec54aa62a97d23977231 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 12 Jul 2019 10:07:11 -0600 Subject: [PATCH] Standardize exit codes and improve shutdown handling; update gitignore --- .gitignore | 2 ++ caddy.go | 48 ++++++++++++++++++++------ cmd/commands.go | 64 +++++++++++++++++++++-------------- cmd/main.go | 9 +++-- sigtrap.go | 82 +++++++++++++++++++++++++++++++++++++++++++++ sigtrap_nonposix.go | 19 +++++++++++ sigtrap_posix.go | 57 +++++++++++++++++++++++++++++++ 7 files changed, 243 insertions(+), 38 deletions(-) create mode 100644 sigtrap.go create mode 100644 sigtrap_nonposix.go create mode 100644 sigtrap_posix.go diff --git a/.gitignore b/.gitignore index daa29f7d..61934469 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ _gitignore/ +*.log +Caddyfile # artifacts from pprof tooling *.prof diff --git a/caddy.go b/caddy.go index c6d810d5..41355c93 100644 --- a/caddy.go +++ b/caddy.go @@ -150,21 +150,46 @@ func Run(newCfg *Config) error { currentCfg = newCfg // Stop, Cleanup each old app - if oldCfg != nil { - for name, a := range oldCfg.apps { - err := a.Stop() - if err != nil { - log.Printf("[ERROR] stop %s: %v", name, err) - } - } - - // clean up all old modules - oldCfg.cancelFunc() - } + 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 { + err := a.Stop() + if err != nil { + log.Printf("[ERROR] stop %s: %v", name, err) + } + } + + // clean up all old modules + oldCfg.cancelFunc() +} + // Duration is a JSON-string-unmarshable duration type. type Duration time.Duration @@ -199,6 +224,7 @@ func GoModule() *debug.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" // CtxKey is a value type for use with context.WithValue. diff --git a/cmd/commands.go b/cmd/commands.go index ad0339f7..00760f4b 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -44,7 +44,8 @@ func cmdStart() (int, error) { // it is ready to confirm that it has successfully started ln, err := net.Listen("tcp", "127.0.0.1:0") 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() @@ -63,7 +64,8 @@ func cmdStart() (int, error) { } stdinpipe, err := cmd.StdinPipe() 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.Stderr = os.Stderr @@ -72,7 +74,7 @@ func cmdStart() (int, error) { expect := make([]byte, 32) _, err = rand.Read(expect) 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 @@ -87,7 +89,7 @@ func cmdStart() (int, error) { // start the process err = cmd.Start() 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 @@ -125,10 +127,11 @@ func cmdStart() (int, error) { case <-success: fmt.Println("Successfully started Caddy") 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) { @@ -144,7 +147,8 @@ func cmdRun() (int, error) { var err error config, err = ioutil.ReadFile(*runCmdConfigFlag) 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 err := caddy.StartAdmin(config) 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() @@ -165,16 +170,19 @@ func cmdRun() (int, error) { if *runCmdPingbackFlag != "" { confirmationBytes, err := ioutil.ReadAll(os.Stdin) 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) 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() _, err = conn.Write(confirmationBytes) 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) { processList, err := ps.Processes() 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]) var found bool @@ -195,15 +203,15 @@ func cmdStop() (int, error) { fmt.Printf("pid=%d\n", p.Pid()) fmt.Printf("Graceful stop...") if err := gracefullyStopProcess(p.Pid()); err != nil { - return 1, err + return caddy.ExitCodeFailedStartup, err } } } if !found { - return 1, fmt.Errorf("Caddy is not running") + return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running") } fmt.Println(" success") - return 0, nil + return caddy.ExitCodeSuccess, nil } func cmdReload() (int, error) { @@ -214,13 +222,15 @@ func cmdReload() (int, error) { // a configuration is required 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 config, err := ioutil.ReadFile(*reloadCmdConfigFlag) 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 @@ -231,7 +241,8 @@ func cmdReload() (int, error) { } err = json.Unmarshal(config, &tmpStruct) 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 } @@ -243,7 +254,8 @@ func cmdReload() (int, error) { // send the configuration to the instance resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config)) 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() @@ -251,12 +263,14 @@ func cmdReload() (int, error) { if resp.StatusCode >= 400 { respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10)) 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) { @@ -267,19 +281,19 @@ func cmdVersion() (int, error) { } else { fmt.Println(goModule.Version) } - return 0, nil + return caddy.ExitCodeSuccess, nil } func cmdListModules() (int, error) { for _, m := range caddy.Modules() { fmt.Println(m) } - return 0, nil + return caddy.ExitCodeSuccess, nil } func cmdEnviron() (int, error) { for _, v := range os.Environ() { fmt.Println(v) } - return 0, nil + return caddy.ExitCodeSuccess, nil } diff --git a/cmd/main.go b/cmd/main.go index 4691b37a..16d065b8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,10 +23,15 @@ import ( "log" "net" "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() { + caddy.TrapSignals() + if len(os.Args) <= 1 { fmt.Println(usageString()) return @@ -35,7 +40,7 @@ func Main() { subcommand, ok := commands[os.Args[1]] if !ok { 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 { diff --git a/sigtrap.go b/sigtrap.go new file mode 100644 index 00000000..65f1e964 --- /dev/null +++ b/sigtrap.go @@ -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 +) diff --git a/sigtrap_nonposix.go b/sigtrap_nonposix.go new file mode 100644 index 00000000..3b4595a6 --- /dev/null +++ b/sigtrap_nonposix.go @@ -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() {} diff --git a/sigtrap_posix.go b/sigtrap_posix.go new file mode 100644 index 00000000..63e6a31a --- /dev/null +++ b/sigtrap_posix.go @@ -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") + } + } + }() +}