aboutsummaryrefslogtreecommitdiff
path: root/core/restart.go
diff options
context:
space:
mode:
Diffstat (limited to 'core/restart.go')
-rw-r--r--core/restart.go166
1 files changed, 166 insertions, 0 deletions
diff --git a/core/restart.go b/core/restart.go
new file mode 100644
index 000000000..82567d35c
--- /dev/null
+++ b/core/restart.go
@@ -0,0 +1,166 @@
+// +build !windows
+
+package core
+
+import (
+ "bytes"
+ "encoding/gob"
+ "errors"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+ "path"
+ "sync/atomic"
+
+ "github.com/miekg/coredns/core/https"
+)
+
+func init() {
+ gob.Register(CaddyfileInput{})
+}
+
+// Restart restarts the entire application; gracefully with zero
+// downtime if on a POSIX-compatible system, or forcefully if on
+// Windows but with imperceptibly-short downtime.
+//
+// The restarted application will use newCaddyfile as its input
+// configuration. If newCaddyfile is nil, the current (existing)
+// Caddyfile configuration will be used.
+//
+// Note: The process must exist in the same place on the disk in
+// order for this to work. Thus, multiple graceful restarts don't
+// work if executing with `go run`, since the binary is cleaned up
+// when `go run` sees the initial parent process exit.
+func Restart(newCaddyfile Input) error {
+ log.Println("[INFO] Restarting")
+
+ if newCaddyfile == nil {
+ caddyfileMu.Lock()
+ newCaddyfile = caddyfile
+ caddyfileMu.Unlock()
+ }
+
+ // Get certificates for any new hosts in the new Caddyfile without causing downtime
+ err := getCertsForNewCaddyfile(newCaddyfile)
+ if err != nil {
+ return errors.New("TLS preload: " + err.Error())
+ }
+
+ if len(os.Args) == 0 { // this should never happen, but...
+ os.Args = []string{""}
+ }
+
+ // Tell the child that it's a restart
+ os.Setenv("CADDY_RESTART", "true")
+
+ // Prepare our payload to the child process
+ cdyfileGob := caddyfileGob{
+ ListenerFds: make(map[string]uintptr),
+ Caddyfile: newCaddyfile,
+ OnDemandTLSCertsIssued: atomic.LoadInt32(https.OnDemandIssuedCount),
+ }
+
+ // Prepare a pipe to the fork's stdin so it can get the Caddyfile
+ rpipe, wpipe, err := os.Pipe()
+ if err != nil {
+ return err
+ }
+
+ // Prepare a pipe that the child process will use to communicate
+ // its success with us by sending > 0 bytes
+ sigrpipe, sigwpipe, err := os.Pipe()
+ if err != nil {
+ return err
+ }
+
+ // Pass along relevant file descriptors to child process; ordering
+ // is very important since we rely on these being in certain positions.
+ extraFiles := []*os.File{sigwpipe} // fd 3
+
+ // Add file descriptors of all the sockets
+ serversMu.Lock()
+ for i, s := range servers {
+ extraFiles = append(extraFiles, s.ListenerFd())
+ cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners
+ }
+ serversMu.Unlock()
+
+ // Set up the command
+ cmd := exec.Command(os.Args[0], os.Args[1:]...)
+ cmd.Stdin = rpipe // fd 0
+ cmd.Stdout = os.Stdout // fd 1
+ cmd.Stderr = os.Stderr // fd 2
+ cmd.ExtraFiles = extraFiles
+
+ // Spawn the child process
+ err = cmd.Start()
+ if err != nil {
+ return err
+ }
+
+ // Immediately close our dup'ed fds and the write end of our signal pipe
+ for _, f := range extraFiles {
+ f.Close()
+ }
+
+ // Feed Caddyfile to the child
+ err = gob.NewEncoder(wpipe).Encode(cdyfileGob)
+ if err != nil {
+ return err
+ }
+ wpipe.Close()
+
+ // Determine whether child startup succeeded
+ answer, readErr := ioutil.ReadAll(sigrpipe)
+ if answer == nil || len(answer) == 0 {
+ cmdErr := cmd.Wait() // get exit status
+ log.Printf("[ERROR] Restart: child failed to initialize (%v) - changes not applied", cmdErr)
+ if readErr != nil {
+ log.Printf("[ERROR] Restart: additionally, error communicating with child process: %v", readErr)
+ }
+ return errIncompleteRestart
+ }
+
+ // Looks like child is successful; we can exit gracefully.
+ return Stop()
+}
+
+func getCertsForNewCaddyfile(newCaddyfile Input) error {
+ // parse the new caddyfile only up to (and including) TLS
+ // so we can know what we need to get certs for.
+ configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
+ if err != nil {
+ return errors.New("loading Caddyfile: " + err.Error())
+ }
+
+ // first mark the configs that are qualified for managed TLS
+ https.MarkQualified(configs)
+
+ // since we group by bind address to obtain certs, we must call
+ // EnableTLS to make sure the port is set properly first
+ // (can ignore error since we aren't actually using the certs)
+ https.EnableTLS(configs, false)
+
+ // find out if we can let the acme package start its own challenge listener
+ // on port 80
+ var proxyACME bool
+ serversMu.Lock()
+ for _, s := range servers {
+ _, port, _ := net.SplitHostPort(s.Addr)
+ if port == "80" {
+ proxyACME = true
+ break
+ }
+ }
+ serversMu.Unlock()
+
+ // place certs on the disk
+ err = https.ObtainCerts(configs, false, proxyACME)
+ if err != nil {
+ return errors.New("obtaining certs: " + err.Error())
+ }
+
+ return nil
+}