// +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 }