summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthew Holt <mholt@users.noreply.github.com>2019-07-12 10:07:11 -0600
committerMatthew Holt <mholt@users.noreply.github.com>2019-07-12 10:07:11 -0600
commitb780f0f49b191a6724b7ec54aa62a97d23977231 (patch)
tree984a9f1f816fd9a092f0bba63a38c3099c43b7b1
parent2141626269201d902b736711a808098e6d175cbb (diff)
Standardize exit codes and improve shutdown handling; update gitignore
-rw-r--r--.gitignore2
-rw-r--r--caddy.go46
-rw-r--r--cmd/commands.go64
-rw-r--r--cmd/main.go9
-rw-r--r--sigtrap.go82
-rw-r--r--sigtrap_nonposix.go19
-rw-r--r--sigtrap_posix.go57
7 files changed, 242 insertions, 37 deletions
diff --git a/.gitignore b/.gitignore
index daa29f7..6193446 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 c6d810d..41355c9 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)
- }
- }
+ unsyncedStop(oldCfg)
- // clean up all old modules
- oldCfg.cancelFunc()
- }
+ 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 ad0339f..00760f4 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 4691b37..16d065b 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 0000000..65f1e96
--- /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 0000000..3b4595a
--- /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 0000000..63e6a31
--- /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")
+ }
+ }
+ }()
+}