From 4ebff9a13065757988dcec314a9989e9b48a0ed7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 26 Oct 2015 13:34:31 -0600 Subject: [PATCH] core: Major refactor for graceful restarts; numerous fixes Merged config and app packages into one called caddy. Abstracted away caddy startup functionality making it easier to embed Caddy in any Go application and use it as a library. Graceful restart (should) now ensure child starts properly. Now piping a gob bundle to child process so that the child can match up inherited listeners to server address. Much cleanup still to do. --- app/app.go | 172 ------- caddy/assets/path.go | 29 ++ caddy/caddy.go | 470 ++++++++++++++++++ {config => caddy}/config.go | 17 +- {config => caddy}/config_test.go | 2 +- {config => caddy}/directives.go | 6 +- {config => caddy}/letsencrypt/crypto.go | 0 {config => caddy}/letsencrypt/crypto_test.go | 0 {config => caddy}/letsencrypt/letsencrypt.go | 6 + .../letsencrypt/letsencrypt_test.go | 0 {config => caddy}/letsencrypt/renew.go | 17 +- {config => caddy}/letsencrypt/storage.go | 4 +- {config => caddy}/letsencrypt/storage_test.go | 0 {config => caddy}/letsencrypt/user.go | 0 {config => caddy}/letsencrypt/user_test.go | 0 {config => caddy}/parse/dispenser.go | 0 {config => caddy}/parse/dispenser_test.go | 0 {config => caddy}/parse/import_test1.txt | 0 {config => caddy}/parse/import_test2.txt | 0 {config => caddy}/parse/lexer.go | 0 {config => caddy}/parse/lexer_test.go | 0 {config => caddy}/parse/parse.go | 0 {config => caddy}/parse/parse_test.go | 0 {config => caddy}/parse/parsing.go | 0 {config => caddy}/parse/parsing_test.go | 0 {config => caddy}/setup/basicauth.go | 0 {config => caddy}/setup/basicauth_test.go | 0 {config => caddy}/setup/bindhost.go | 0 {config => caddy}/setup/browse.go | 0 {config => caddy}/setup/controller.go | 2 +- {config => caddy}/setup/errors.go | 0 {config => caddy}/setup/errors_test.go | 0 {config => caddy}/setup/ext.go | 0 {config => caddy}/setup/ext_test.go | 0 {config => caddy}/setup/fastcgi.go | 0 {config => caddy}/setup/fastcgi_test.go | 0 {config => caddy}/setup/gzip.go | 0 {config => caddy}/setup/gzip_test.go | 0 {config => caddy}/setup/headers.go | 0 {config => caddy}/setup/headers_test.go | 0 {config => caddy}/setup/internal.go | 0 {config => caddy}/setup/internal_test.go | 0 {config => caddy}/setup/log.go | 0 {config => caddy}/setup/log_test.go | 0 {config => caddy}/setup/markdown.go | 0 {config => caddy}/setup/markdown_test.go | 0 {config => caddy}/setup/mime.go | 0 {config => caddy}/setup/mime_test.go | 0 {config => caddy}/setup/proxy.go | 0 {config => caddy}/setup/redir.go | 0 {config => caddy}/setup/rewrite.go | 0 {config => caddy}/setup/rewrite_test.go | 0 {config => caddy}/setup/roller.go | 0 {config => caddy}/setup/root.go | 0 {config => caddy}/setup/root_test.go | 0 {config => caddy}/setup/startupshutdown.go | 0 {config => caddy}/setup/templates.go | 0 {config => caddy}/setup/templates_test.go | 0 .../setup/testdata/blog/first_post.md | 0 {config => caddy}/setup/testdata/header.html | 0 .../setup/testdata/tpl_with_include.html | 0 {config => caddy}/setup/tls.go | 0 {config => caddy}/setup/tls_test.go | 0 {config => caddy}/setup/websocket.go | 0 {config => caddy}/setup/websocket_test.go | 0 main.go | 237 ++++----- middleware/proxy/upstream.go | 2 +- server/graceful.go | 8 +- server/server.go | 10 +- 69 files changed, 630 insertions(+), 352 deletions(-) delete mode 100644 app/app.go create mode 100644 caddy/assets/path.go create mode 100644 caddy/caddy.go rename {config => caddy}/config.go (96%) rename {config => caddy}/config_test.go (99%) rename {config => caddy}/directives.go (96%) rename {config => caddy}/letsencrypt/crypto.go (100%) rename {config => caddy}/letsencrypt/crypto_test.go (100%) rename {config => caddy}/letsencrypt/letsencrypt.go (97%) rename {config => caddy}/letsencrypt/letsencrypt_test.go (100%) rename {config => caddy}/letsencrypt/renew.go (88%) rename {config => caddy}/letsencrypt/storage.go (95%) rename {config => caddy}/letsencrypt/storage_test.go (100%) rename {config => caddy}/letsencrypt/user.go (100%) rename {config => caddy}/letsencrypt/user_test.go (100%) rename {config => caddy}/parse/dispenser.go (100%) rename {config => caddy}/parse/dispenser_test.go (100%) rename {config => caddy}/parse/import_test1.txt (100%) rename {config => caddy}/parse/import_test2.txt (100%) rename {config => caddy}/parse/lexer.go (100%) rename {config => caddy}/parse/lexer_test.go (100%) rename {config => caddy}/parse/parse.go (100%) rename {config => caddy}/parse/parse_test.go (100%) rename {config => caddy}/parse/parsing.go (100%) rename {config => caddy}/parse/parsing_test.go (100%) rename {config => caddy}/setup/basicauth.go (100%) rename {config => caddy}/setup/basicauth_test.go (100%) rename {config => caddy}/setup/bindhost.go (100%) rename {config => caddy}/setup/browse.go (100%) rename {config => caddy}/setup/controller.go (98%) rename {config => caddy}/setup/errors.go (100%) rename {config => caddy}/setup/errors_test.go (100%) rename {config => caddy}/setup/ext.go (100%) rename {config => caddy}/setup/ext_test.go (100%) rename {config => caddy}/setup/fastcgi.go (100%) rename {config => caddy}/setup/fastcgi_test.go (100%) rename {config => caddy}/setup/gzip.go (100%) rename {config => caddy}/setup/gzip_test.go (100%) rename {config => caddy}/setup/headers.go (100%) rename {config => caddy}/setup/headers_test.go (100%) rename {config => caddy}/setup/internal.go (100%) rename {config => caddy}/setup/internal_test.go (100%) rename {config => caddy}/setup/log.go (100%) rename {config => caddy}/setup/log_test.go (100%) rename {config => caddy}/setup/markdown.go (100%) rename {config => caddy}/setup/markdown_test.go (100%) rename {config => caddy}/setup/mime.go (100%) rename {config => caddy}/setup/mime_test.go (100%) rename {config => caddy}/setup/proxy.go (100%) rename {config => caddy}/setup/redir.go (100%) rename {config => caddy}/setup/rewrite.go (100%) rename {config => caddy}/setup/rewrite_test.go (100%) rename {config => caddy}/setup/roller.go (100%) rename {config => caddy}/setup/root.go (100%) rename {config => caddy}/setup/root_test.go (100%) rename {config => caddy}/setup/startupshutdown.go (100%) rename {config => caddy}/setup/templates.go (100%) rename {config => caddy}/setup/templates_test.go (100%) rename {config => caddy}/setup/testdata/blog/first_post.md (100%) rename {config => caddy}/setup/testdata/header.html (100%) rename {config => caddy}/setup/testdata/tpl_with_include.html (100%) rename {config => caddy}/setup/tls.go (100%) rename {config => caddy}/setup/tls_test.go (100%) rename {config => caddy}/setup/websocket.go (100%) rename {config => caddy}/setup/websocket_test.go (100%) diff --git a/app/app.go b/app/app.go deleted file mode 100644 index 3c66e6129..000000000 --- a/app/app.go +++ /dev/null @@ -1,172 +0,0 @@ -// Package app holds application-global state to make it accessible -// by other packages in the application. -// -// This package differs from config in that the things in app aren't -// really related to server configuration. -package app - -import ( - "errors" - "log" - "os" - "os/signal" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "syscall" - - "github.com/mholt/caddy/server" -) - -const ( - // Name is the program name - Name = "Caddy" - - // Version is the program version - Version = "0.7.6" -) - -var ( - // Servers is a list of all the currently-listening servers - Servers []*server.Server - - // ServersMutex protects the Servers slice during changes - ServersMutex sync.Mutex - - // Wg is used to wait for all servers to shut down - Wg sync.WaitGroup - - // HTTP2 indicates whether HTTP2 is enabled or not - HTTP2 bool // TODO: temporary flag until http2 is standard - - // Quiet mode hides non-error initialization output - Quiet bool -) - -func init() { - go func() { - // Wait for signal - interrupt := make(chan os.Signal, 1) - signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGTERM? Or that should not run callbacks... - <-interrupt - - // Run shutdown callbacks - var exitCode int - ServersMutex.Lock() - errs := server.ShutdownCallbacks(Servers) - ServersMutex.Unlock() - if len(errs) > 0 { - for _, err := range errs { - log.Println(err) - } - exitCode = 1 - } - os.Exit(exitCode) - }() -} - -// 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 caddyfile as its input -// configuration; it will not look elsewhere for the config -// to use. -func Restart(caddyfile []byte) error { - // TODO: This is POSIX-only right now; also, os.Args[0] is required! - // TODO: Pipe the Caddyfile to stdin of child! - // TODO: Before stopping this process, verify child started successfully (valid Caddyfile, etc) - - // Tell the child that it's a restart - os.Setenv("CADDY_RESTART", "true") - - // Pass along current environment and file descriptors to child. - // We pass along the file descriptors explicitly to ensure proper - // order, since losing the original order will break the child. - fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()} - - // Now add file descriptors of the sockets - ServersMutex.Lock() - for _, s := range Servers { - fds = append(fds, s.ListenerFd()) - } - ServersMutex.Unlock() - - // Fork the process with the current environment and file descriptors - execSpec := &syscall.ProcAttr{ - Env: os.Environ(), - Files: fds, - } - fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) - if err != nil { - log.Println("FORK ERR:", err, fork) - } - - // Child process is listening now; we can stop all our servers here. - ServersMutex.Lock() - for _, s := range Servers { - go s.Stop() // TODO: error checking/reporting - } - ServersMutex.Unlock() - - return err -} - -// SetCPU parses string cpu and sets GOMAXPROCS -// according to its value. It accepts either -// a number (e.g. 3) or a percent (e.g. 50%). -func SetCPU(cpu string) error { - var numCPU int - - availCPU := runtime.NumCPU() - - if strings.HasSuffix(cpu, "%") { - // Percent - var percent float32 - pctStr := cpu[:len(cpu)-1] - pctInt, err := strconv.Atoi(pctStr) - if err != nil || pctInt < 1 || pctInt > 100 { - return errors.New("invalid CPU value: percentage must be between 1-100") - } - percent = float32(pctInt) / 100 - numCPU = int(float32(availCPU) * percent) - } else { - // Number - num, err := strconv.Atoi(cpu) - if err != nil || num < 1 { - return errors.New("invalid CPU value: provide a number or percent greater than 0") - } - numCPU = num - } - - if numCPU > availCPU { - numCPU = availCPU - } - - runtime.GOMAXPROCS(numCPU) - return nil -} - -// DataFolder returns the path to the folder -// where the application may store data. This -// currently resolves to ~/.caddy -func DataFolder() string { - return filepath.Join(userHomeDir(), ".caddy") -} - -// userHomeDir returns the user's home directory according to -// environment variables. -// -// Credit: http://stackoverflow.com/a/7922977/1048862 -func userHomeDir() string { - if runtime.GOOS == "windows" { - home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - if home == "" { - home = os.Getenv("USERPROFILE") - } - return home - } - return os.Getenv("HOME") -} diff --git a/caddy/assets/path.go b/caddy/assets/path.go new file mode 100644 index 000000000..46b883b1c --- /dev/null +++ b/caddy/assets/path.go @@ -0,0 +1,29 @@ +package assets + +import ( + "os" + "path/filepath" + "runtime" +) + +// Path returns the path to the folder +// where the application may store data. This +// currently resolves to ~/.caddy +func Path() string { + return filepath.Join(userHomeDir(), ".caddy") +} + +// userHomeDir returns the user's home directory according to +// environment variables. +// +// Credit: http://stackoverflow.com/a/7922977/1048862 +func userHomeDir() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} diff --git a/caddy/caddy.go b/caddy/caddy.go new file mode 100644 index 000000000..1cc039b64 --- /dev/null +++ b/caddy/caddy.go @@ -0,0 +1,470 @@ +package caddy + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + + "github.com/mholt/caddy/caddy/letsencrypt" + "github.com/mholt/caddy/server" +) + +// Configurable application parameters +var ( + // The name and version of the application. + AppName, AppVersion string + + // If true, initialization will not show any output. + Quiet bool + + // DefaultInput is the default configuration to use when config input is empty or missing. + DefaultInput = CaddyfileInput{ + Contents: []byte(fmt.Sprintf("%s:%s\nroot %s", DefaultHost, DefaultPort, DefaultRoot)), + } + + // HTTP2 indicates whether HTTP2 is enabled or not + HTTP2 bool // TODO: temporary flag until http2 is standard +) + +var ( + // caddyfile is the input configuration text used for this process + caddyfile Input + + // caddyfileMu protects caddyfile during changes + caddyfileMu sync.Mutex + + // incompleteRestartErr occurs if this process is a fork + // of the parent but no Caddyfile was piped in + incompleteRestartErr = errors.New("cannot finish restart successfully") + + // servers is a list of all the currently-listening servers + servers []*server.Server + + // serversMu protects the servers slice during changes + serversMu sync.Mutex + + // wg is used to wait for all servers to shut down + wg sync.WaitGroup + + // loadedGob is used if this is a child process as part of + // a graceful restart; it is used to map listeners to their + // index in the list of inherited file descriptors. This + // variable is not safe for concurrent access. + loadedGob caddyfileGob +) + +const ( + DefaultHost = "0.0.0.0" + DefaultPort = "2015" + DefaultRoot = "." +) + +// caddyfileGob maps bind address to index of the file descriptor +// in the Files array passed to the child process. It also contains +// the caddyfile contents. +type caddyfileGob struct { + ListenerFds map[string]uintptr + Caddyfile []byte +} + +// Start starts Caddy with the given Caddyfile. If cdyfile +// is nil or the process is forked from a parent as part of +// a graceful restart, Caddy will check to see if Caddyfile +// was piped from stdin and use that. +// +// If this process is a fork and no Caddyfile was piped in, +// an error will be returned. If this process is NOT a fork +// and cdyfile is nil, a default configuration will be assumed. +// In any case, an error is returned if Caddy could not be +// started. +func Start(cdyfile Input) error { + var err error + + // Input must never be nil; try to load something + if cdyfile == nil { + cdyfile, err = LoadCaddyfile(nil) + if err != nil { + return err + } + } + + caddyfileMu.Lock() + caddyfile = cdyfile + caddyfileMu.Unlock() + + groupings, err := Load(path.Base(caddyfile.Path()), bytes.NewReader(caddyfile.Body())) + + // Start each server with its one or more configurations + for i, group := range groupings { + s, err := server.New(group.BindAddr.String(), group.Configs) + if err != nil { + log.Fatal(err) + } + s.HTTP2 = HTTP2 // TODO: This setting is temporary + + var ln server.ListenerFile + if isRestart() { + // Look up this server's listener in the map of inherited file descriptors; + // if we don't have one, we must make a new one. + if fdIndex, ok := loadedGob.ListenerFds[s.Addr]; ok { + file := os.NewFile(fdIndex, "") + + fln, err := net.FileListener(file) + if err != nil { + log.Fatal("FILE LISTENER:", err) + } + + ln, ok = fln.(server.ListenerFile) + if !ok { + log.Fatal("Listener was not a ListenerFile") + } + + delete(loadedGob.ListenerFds, s.Addr) // mark it as used + } + } + + wg.Add(1) + go func(s *server.Server, i int, ln server.ListenerFile) { + defer wg.Done() + if ln == nil { + err := s.ListenAndServe() + // "use of closed network connection" is normal if doing graceful shutdown... + if !strings.Contains(err.Error(), "use of closed network connection") { + // But an error at initial startup must be fatal + log.Fatal(err) + } + } else { + err := s.Serve(ln) + if err != nil { + log.Println(err) + } + } + }(s, i, ln) + + serversMu.Lock() + servers = append(servers, s) + serversMu.Unlock() + } + + // Close remaining file descriptors we may have inherited that we don't need + if isRestart() { + for _, fdIndex := range loadedGob.ListenerFds { + file := os.NewFile(fdIndex, "") + fln, err := net.FileListener(file) + if err == nil { + fln.Close() + } + } + } + + // Show initialization output + if !Quiet && !isRestart() { + var checkedFdLimit bool + for _, group := range groupings { + for _, conf := range group.Configs { + // Print address of site + fmt.Println(conf.Address()) + + // Note if non-localhost site resolves to loopback interface + if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { + fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", + conf.Host, group.BindAddr.IP.String()) + } + if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { + checkFdlimit() + checkedFdLimit = true + } + } + } + } + + // Tell parent we're A-OK + if isRestart() { + file := os.NewFile(3, "") + file.Write([]byte("success")) + file.Close() + } + + return nil +} + +// isLocalhost returns true if the string looks explicitly like a localhost address. +func isLocalhost(s string) bool { + return s == "localhost" || s == "::1" || strings.HasPrefix(s, "127.") +} + +// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. +func checkFdlimit() { + const min = 4096 + + // Warn if ulimit is too low for production sites + if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { + out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH + if err == nil { + // Note that an error here need not be reported + lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) + if err == nil && lim < min { + fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) + } + } + } +} + +func Stop() error { + serversMu.Lock() + for _, s := range servers { + s.Stop() // TODO: error checking/reporting? + } + serversMu.Unlock() + return nil +} + +// 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. +func Restart(newCaddyfile Input) error { + if newCaddyfile == nil { + caddyfileMu.Lock() + newCaddyfile = caddyfile + caddyfileMu.Unlock() + } + + if runtime.GOOS == "windows" { + err := Stop() + if err != nil { + return err + } + err = Start(newCaddyfile) + if err != nil { + return err + } + return nil + } + + if len(os.Args) == 0 { // this should never happen, but just in case... + 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.Body(), + } + + // 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 or failure with us, the parent + sigrpipe, sigwpipe, err := os.Pipe() + if err != nil { + return err + } + + // Pass along current environment and file descriptors to child. + // Ordering here is very important: stdin, stdout, stderr, sigpipe, + // and then the listener file descriptors (in order). + fds := []uintptr{rpipe.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), sigwpipe.Fd()} + + // Now add file descriptors of the sockets + serversMu.Lock() + for i, s := range servers { + fds = append(fds, s.ListenerFd()) + cdyfileGob.ListenerFds[s.Addr] = uintptr(4 + i) // 4 fds come before any of the listeners + } + serversMu.Unlock() + + // Fork the process with the current environment and file descriptors + execSpec := &syscall.ProcAttr{ + Env: os.Environ(), + Files: fds, + } + pid, err := syscall.ForkExec(os.Args[0], os.Args, execSpec) + if err != nil { + log.Println("FORK ERR:", err, pid) + } + + // Feed it the Caddyfile + err = gob.NewEncoder(wpipe).Encode(cdyfileGob) + if err != nil { + return err + } + wpipe.Close() + + // Wait for child process to signal success or fail + sigwpipe.Close() // close our copy of the write end of the pipe + answer, err := ioutil.ReadAll(sigrpipe) + if err != nil || len(answer) == 0 { + log.Println("restart: child failed to answer; changes not applied") + return incompleteRestartErr + } + + // Child process is listening now; we can stop all our servers here. + return Stop() +} + +// Wait blocks until all servers are stopped. +func Wait() { + wg.Wait() +} + +// LoadCaddyfile loads a Caddyfile in a way that prioritizes +// reading from stdin pipe; otherwise it calls loader to load +// the Caddyfile. If loader does not return a Caddyfile, the +// default one will be returned. Thus, if there are no other +// errors, this function always returns at least the default +// Caddyfile. +func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { + // If we are a fork, finishing the restart is highest priority; + // piped input is required in this case. + if isRestart() { + err := gob.NewDecoder(os.Stdin).Decode(&loadedGob) + if err != nil { + return nil, err + } + cdyfile = CaddyfileInput{ + Filepath: os.Stdin.Name(), + Contents: loadedGob.Caddyfile, + } + } + + // Otherwise, we first try to get from stdin pipe + if cdyfile == nil { + cdyfile, err = CaddyfileFromPipe(os.Stdin) + if err != nil { + return nil, err + } + } + + // No piped input, so try the user's loader instead + if cdyfile == nil && loader != nil { + cdyfile, err = loader() + } + + // Otherwise revert to default + if cdyfile == nil { + cdyfile = DefaultInput + } + + return +} + +// Caddyfile returns the current Caddyfile +func Caddyfile() Input { + caddyfileMu.Lock() + defer caddyfileMu.Unlock() + return caddyfile +} + +// isRestart returns whether this process is, according +// to env variables, a fork as part of a graceful restart. +func isRestart() bool { + return os.Getenv("CADDY_RESTART") == "true" +} + +// CaddyfileFromPipe loads the Caddyfile input from f if f is +// not interactive input. f is assumed to be a pipe or stream, +// such as os.Stdin. If f is not a pipe, no error is returned +// but the Input value will be nil. An error is only returned +// if there was an error reading the pipe, even if the length +// of what was read is 0. +func CaddyfileFromPipe(f *os.File) (Input, error) { + fi, err := f.Stat() + if err == nil && fi.Mode()&os.ModeCharDevice == 0 { + // Note that a non-nil error is not a problem. Windows + // will not create a stdin if there is no pipe, which + // produces an error when calling Stat(). But Unix will + // make one either way, which is why we also check that + // bitmask. + // BUG: Reading from stdin after this fails (e.g. for the let's encrypt email address) (OS X) + confBody, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + return CaddyfileInput{ + Contents: confBody, + Filepath: f.Name(), + }, nil + } + + // not having input from the pipe is not itself an error, + // just means no input to return. + return nil, nil +} + +// Input represents a Caddyfile; its contents and file path +// (which should include the file name at the end of the path). +// If path does not apply (e.g. piped input) you may use +// any understandable value. The path is mainly used for logging, +// error messages, and debugging. +type Input interface { + // Gets the Caddyfile contents + Body() []byte + + // Gets the path to the origin file + Path() string +} + +// CaddyfileInput represents a Caddyfile as input +// and is simply a convenient way to implement +// the Input interface. +type CaddyfileInput struct { + Filepath string + Contents []byte +} + +// Body returns c.Contents. +func (c CaddyfileInput) Body() []byte { return c.Contents } + +// Path returns c.Filepath. +func (c CaddyfileInput) Path() string { return c.Filepath } + +func init() { + letsencrypt.OnRenew = func() error { return Restart(nil) } + + // Trap signals + go func() { + // Wait for signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, os.Kill) + <-interrupt + + // TODO: A signal just for graceful restart (reload config) - maybe SIGUSR1 + + // Run shutdown callbacks + var exitCode int + serversMu.Lock() + errs := server.ShutdownCallbacks(servers) + serversMu.Unlock() + if len(errs) > 0 { + for _, err := range errs { + log.Println(err) + } + exitCode = 1 + } + os.Exit(exitCode) + }() +} diff --git a/config/config.go b/caddy/config.go similarity index 96% rename from config/config.go rename to caddy/config.go index d7fed8bf2..dac657845 100644 --- a/config/config.go +++ b/caddy/config.go @@ -1,4 +1,4 @@ -package config +package caddy import ( "fmt" @@ -7,19 +7,14 @@ import ( "net" "sync" - "github.com/mholt/caddy/app" - "github.com/mholt/caddy/config/letsencrypt" - "github.com/mholt/caddy/config/parse" - "github.com/mholt/caddy/config/setup" + "github.com/mholt/caddy/caddy/letsencrypt" + "github.com/mholt/caddy/caddy/parse" + "github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/server" ) const ( - DefaultHost = "0.0.0.0" - DefaultPort = "2015" - DefaultRoot = "." - // DefaultConfigFile is the name of the configuration file that is loaded // by default if no other file is specified. DefaultConfigFile = "Caddyfile" @@ -56,8 +51,8 @@ func Load(filename string, input io.Reader) (Group, error) { Root: Root, Middleware: make(map[string][]middleware.Middleware), ConfigFile: filename, - AppName: app.Name, - AppVersion: app.Version, + AppName: AppName, + AppVersion: AppVersion, } // It is crucial that directives are executed in the proper order. diff --git a/config/config_test.go b/caddy/config_test.go similarity index 99% rename from config/config_test.go rename to caddy/config_test.go index 756784191..477da2071 100644 --- a/config/config_test.go +++ b/caddy/config_test.go @@ -1,4 +1,4 @@ -package config +package caddy import ( "testing" diff --git a/config/directives.go b/caddy/directives.go similarity index 96% rename from config/directives.go rename to caddy/directives.go index 354b55959..3ebee7955 100644 --- a/config/directives.go +++ b/caddy/directives.go @@ -1,8 +1,8 @@ -package config +package caddy import ( - "github.com/mholt/caddy/config/parse" - "github.com/mholt/caddy/config/setup" + "github.com/mholt/caddy/caddy/parse" + "github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/middleware" ) diff --git a/config/letsencrypt/crypto.go b/caddy/letsencrypt/crypto.go similarity index 100% rename from config/letsencrypt/crypto.go rename to caddy/letsencrypt/crypto.go diff --git a/config/letsencrypt/crypto_test.go b/caddy/letsencrypt/crypto_test.go similarity index 100% rename from config/letsencrypt/crypto_test.go rename to caddy/letsencrypt/crypto_test.go diff --git a/config/letsencrypt/letsencrypt.go b/caddy/letsencrypt/letsencrypt.go similarity index 97% rename from config/letsencrypt/letsencrypt.go rename to caddy/letsencrypt/letsencrypt.go index 632e80007..a7aef7e83 100644 --- a/config/letsencrypt/letsencrypt.go +++ b/caddy/letsencrypt/letsencrypt.go @@ -18,6 +18,12 @@ import ( "github.com/xenolf/lego/acme" ) +// OnRenew is the function that will be used to restart +// the application or the part of the application that uses +// the certificates maintained by this package. When at least +// one certificate is renewed, this function will be called. +var OnRenew func() error + // Activate sets up TLS for each server config in configs // as needed. It only skips the config if the cert and key // are already provided or if plaintext http is explicitly diff --git a/config/letsencrypt/letsencrypt_test.go b/caddy/letsencrypt/letsencrypt_test.go similarity index 100% rename from config/letsencrypt/letsencrypt_test.go rename to caddy/letsencrypt/letsencrypt_test.go diff --git a/config/letsencrypt/renew.go b/caddy/letsencrypt/renew.go similarity index 88% rename from config/letsencrypt/renew.go rename to caddy/letsencrypt/renew.go index 291df06c4..cd19c24e7 100644 --- a/config/letsencrypt/renew.go +++ b/caddy/letsencrypt/renew.go @@ -17,10 +17,16 @@ import ( func keepCertificatesRenewed(configs []server.Config) { ticker := time.Tick(renewInterval) for range ticker { - if errs := processCertificateRenewal(configs); len(errs) > 0 { + if n, errs := processCertificateRenewal(configs); len(errs) > 0 { for _, err := range errs { log.Printf("[ERROR] cert renewal: %v\n", err) } + if n > 0 && OnRenew != nil { + err := OnRenew() + if err != nil { + log.Printf("[ERROR] onrenew callback: %v\n", err) + } + } } } } @@ -28,9 +34,11 @@ func keepCertificatesRenewed(configs []server.Config) { // checkCertificateRenewal loops through all configured // sites and looks for certificates to renew. Nothing is mutated // through this function. The changes happen directly on disk. -func processCertificateRenewal(configs []server.Config) []error { - var errs []error +// It returns the number of certificates renewed and +func processCertificateRenewal(configs []server.Config) (int, []error) { log.Print("[INFO] Processing certificate renewals...") + var errs []error + var n int for _, cfg := range configs { // Host must be TLS-enabled and have assets managed by LE @@ -95,11 +103,12 @@ func processCertificateRenewal(configs []server.Config) []error { } saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) + n++ } else if daysLeft <= 14 { // Warn on 14 days remaining log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 7 days remain.\n", daysLeft, cfg.Host) } } - return errs + return n, errs } diff --git a/config/letsencrypt/storage.go b/caddy/letsencrypt/storage.go similarity index 95% rename from config/letsencrypt/storage.go rename to caddy/letsencrypt/storage.go index ca4405a8d..6826e930f 100644 --- a/config/letsencrypt/storage.go +++ b/caddy/letsencrypt/storage.go @@ -4,13 +4,13 @@ import ( "path/filepath" "strings" - "github.com/mholt/caddy/app" + "github.com/mholt/caddy/caddy/assets" ) // storage is used to get file paths in a consistent, // cross-platform way for persisting Let's Encrypt assets // on the file system. -var storage = Storage(filepath.Join(app.DataFolder(), "letsencrypt")) +var storage = Storage(filepath.Join(assets.Path(), "letsencrypt")) // Storage is a root directory and facilitates // forming file paths derived from it. diff --git a/config/letsencrypt/storage_test.go b/caddy/letsencrypt/storage_test.go similarity index 100% rename from config/letsencrypt/storage_test.go rename to caddy/letsencrypt/storage_test.go diff --git a/config/letsencrypt/user.go b/caddy/letsencrypt/user.go similarity index 100% rename from config/letsencrypt/user.go rename to caddy/letsencrypt/user.go diff --git a/config/letsencrypt/user_test.go b/caddy/letsencrypt/user_test.go similarity index 100% rename from config/letsencrypt/user_test.go rename to caddy/letsencrypt/user_test.go diff --git a/config/parse/dispenser.go b/caddy/parse/dispenser.go similarity index 100% rename from config/parse/dispenser.go rename to caddy/parse/dispenser.go diff --git a/config/parse/dispenser_test.go b/caddy/parse/dispenser_test.go similarity index 100% rename from config/parse/dispenser_test.go rename to caddy/parse/dispenser_test.go diff --git a/config/parse/import_test1.txt b/caddy/parse/import_test1.txt similarity index 100% rename from config/parse/import_test1.txt rename to caddy/parse/import_test1.txt diff --git a/config/parse/import_test2.txt b/caddy/parse/import_test2.txt similarity index 100% rename from config/parse/import_test2.txt rename to caddy/parse/import_test2.txt diff --git a/config/parse/lexer.go b/caddy/parse/lexer.go similarity index 100% rename from config/parse/lexer.go rename to caddy/parse/lexer.go diff --git a/config/parse/lexer_test.go b/caddy/parse/lexer_test.go similarity index 100% rename from config/parse/lexer_test.go rename to caddy/parse/lexer_test.go diff --git a/config/parse/parse.go b/caddy/parse/parse.go similarity index 100% rename from config/parse/parse.go rename to caddy/parse/parse.go diff --git a/config/parse/parse_test.go b/caddy/parse/parse_test.go similarity index 100% rename from config/parse/parse_test.go rename to caddy/parse/parse_test.go diff --git a/config/parse/parsing.go b/caddy/parse/parsing.go similarity index 100% rename from config/parse/parsing.go rename to caddy/parse/parsing.go diff --git a/config/parse/parsing_test.go b/caddy/parse/parsing_test.go similarity index 100% rename from config/parse/parsing_test.go rename to caddy/parse/parsing_test.go diff --git a/config/setup/basicauth.go b/caddy/setup/basicauth.go similarity index 100% rename from config/setup/basicauth.go rename to caddy/setup/basicauth.go diff --git a/config/setup/basicauth_test.go b/caddy/setup/basicauth_test.go similarity index 100% rename from config/setup/basicauth_test.go rename to caddy/setup/basicauth_test.go diff --git a/config/setup/bindhost.go b/caddy/setup/bindhost.go similarity index 100% rename from config/setup/bindhost.go rename to caddy/setup/bindhost.go diff --git a/config/setup/browse.go b/caddy/setup/browse.go similarity index 100% rename from config/setup/browse.go rename to caddy/setup/browse.go diff --git a/config/setup/controller.go b/caddy/setup/controller.go similarity index 98% rename from config/setup/controller.go rename to caddy/setup/controller.go index 04873082b..02b366cd8 100644 --- a/config/setup/controller.go +++ b/caddy/setup/controller.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/caddy/parse" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/server" ) diff --git a/config/setup/errors.go b/caddy/setup/errors.go similarity index 100% rename from config/setup/errors.go rename to caddy/setup/errors.go diff --git a/config/setup/errors_test.go b/caddy/setup/errors_test.go similarity index 100% rename from config/setup/errors_test.go rename to caddy/setup/errors_test.go diff --git a/config/setup/ext.go b/caddy/setup/ext.go similarity index 100% rename from config/setup/ext.go rename to caddy/setup/ext.go diff --git a/config/setup/ext_test.go b/caddy/setup/ext_test.go similarity index 100% rename from config/setup/ext_test.go rename to caddy/setup/ext_test.go diff --git a/config/setup/fastcgi.go b/caddy/setup/fastcgi.go similarity index 100% rename from config/setup/fastcgi.go rename to caddy/setup/fastcgi.go diff --git a/config/setup/fastcgi_test.go b/caddy/setup/fastcgi_test.go similarity index 100% rename from config/setup/fastcgi_test.go rename to caddy/setup/fastcgi_test.go diff --git a/config/setup/gzip.go b/caddy/setup/gzip.go similarity index 100% rename from config/setup/gzip.go rename to caddy/setup/gzip.go diff --git a/config/setup/gzip_test.go b/caddy/setup/gzip_test.go similarity index 100% rename from config/setup/gzip_test.go rename to caddy/setup/gzip_test.go diff --git a/config/setup/headers.go b/caddy/setup/headers.go similarity index 100% rename from config/setup/headers.go rename to caddy/setup/headers.go diff --git a/config/setup/headers_test.go b/caddy/setup/headers_test.go similarity index 100% rename from config/setup/headers_test.go rename to caddy/setup/headers_test.go diff --git a/config/setup/internal.go b/caddy/setup/internal.go similarity index 100% rename from config/setup/internal.go rename to caddy/setup/internal.go diff --git a/config/setup/internal_test.go b/caddy/setup/internal_test.go similarity index 100% rename from config/setup/internal_test.go rename to caddy/setup/internal_test.go diff --git a/config/setup/log.go b/caddy/setup/log.go similarity index 100% rename from config/setup/log.go rename to caddy/setup/log.go diff --git a/config/setup/log_test.go b/caddy/setup/log_test.go similarity index 100% rename from config/setup/log_test.go rename to caddy/setup/log_test.go diff --git a/config/setup/markdown.go b/caddy/setup/markdown.go similarity index 100% rename from config/setup/markdown.go rename to caddy/setup/markdown.go diff --git a/config/setup/markdown_test.go b/caddy/setup/markdown_test.go similarity index 100% rename from config/setup/markdown_test.go rename to caddy/setup/markdown_test.go diff --git a/config/setup/mime.go b/caddy/setup/mime.go similarity index 100% rename from config/setup/mime.go rename to caddy/setup/mime.go diff --git a/config/setup/mime_test.go b/caddy/setup/mime_test.go similarity index 100% rename from config/setup/mime_test.go rename to caddy/setup/mime_test.go diff --git a/config/setup/proxy.go b/caddy/setup/proxy.go similarity index 100% rename from config/setup/proxy.go rename to caddy/setup/proxy.go diff --git a/config/setup/redir.go b/caddy/setup/redir.go similarity index 100% rename from config/setup/redir.go rename to caddy/setup/redir.go diff --git a/config/setup/rewrite.go b/caddy/setup/rewrite.go similarity index 100% rename from config/setup/rewrite.go rename to caddy/setup/rewrite.go diff --git a/config/setup/rewrite_test.go b/caddy/setup/rewrite_test.go similarity index 100% rename from config/setup/rewrite_test.go rename to caddy/setup/rewrite_test.go diff --git a/config/setup/roller.go b/caddy/setup/roller.go similarity index 100% rename from config/setup/roller.go rename to caddy/setup/roller.go diff --git a/config/setup/root.go b/caddy/setup/root.go similarity index 100% rename from config/setup/root.go rename to caddy/setup/root.go diff --git a/config/setup/root_test.go b/caddy/setup/root_test.go similarity index 100% rename from config/setup/root_test.go rename to caddy/setup/root_test.go diff --git a/config/setup/startupshutdown.go b/caddy/setup/startupshutdown.go similarity index 100% rename from config/setup/startupshutdown.go rename to caddy/setup/startupshutdown.go diff --git a/config/setup/templates.go b/caddy/setup/templates.go similarity index 100% rename from config/setup/templates.go rename to caddy/setup/templates.go diff --git a/config/setup/templates_test.go b/caddy/setup/templates_test.go similarity index 100% rename from config/setup/templates_test.go rename to caddy/setup/templates_test.go diff --git a/config/setup/testdata/blog/first_post.md b/caddy/setup/testdata/blog/first_post.md similarity index 100% rename from config/setup/testdata/blog/first_post.md rename to caddy/setup/testdata/blog/first_post.md diff --git a/config/setup/testdata/header.html b/caddy/setup/testdata/header.html similarity index 100% rename from config/setup/testdata/header.html rename to caddy/setup/testdata/header.html diff --git a/config/setup/testdata/tpl_with_include.html b/caddy/setup/testdata/tpl_with_include.html similarity index 100% rename from config/setup/testdata/tpl_with_include.html rename to caddy/setup/testdata/tpl_with_include.html diff --git a/config/setup/tls.go b/caddy/setup/tls.go similarity index 100% rename from config/setup/tls.go rename to caddy/setup/tls.go diff --git a/config/setup/tls_test.go b/caddy/setup/tls_test.go similarity index 100% rename from config/setup/tls_test.go rename to caddy/setup/tls_test.go diff --git a/config/setup/websocket.go b/caddy/setup/websocket.go similarity index 100% rename from config/setup/websocket.go rename to caddy/setup/websocket.go diff --git a/config/setup/websocket_test.go b/caddy/setup/websocket_test.go similarity index 100% rename from config/setup/websocket_test.go rename to caddy/setup/websocket_test.go diff --git a/main.go b/main.go index b080d663f..68aab11c4 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,19 @@ package main import ( - "bytes" + "errors" "flag" "fmt" "io/ioutil" "log" - "net" "os" - "os/exec" - "path" "runtime" "strconv" "strings" "time" - "github.com/mholt/caddy/app" - "github.com/mholt/caddy/config" - "github.com/mholt/caddy/config/letsencrypt" - "github.com/mholt/caddy/server" + "github.com/mholt/caddy/caddy" + "github.com/mholt/caddy/caddy/letsencrypt" ) var ( @@ -28,25 +23,33 @@ var ( revoke string ) +const ( + appName = "Caddy" + appVersion = "0.8 beta" +) + func init() { - flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+config.DefaultConfigFile+")") - flag.BoolVar(&app.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib - flag.BoolVar(&app.Quiet, "quiet", false, "Quiet mode (no initialization output)") + flag.StringVar(&conf, "conf", "", "Configuration file to use (default="+caddy.DefaultConfigFile+")") + flag.BoolVar(&caddy.HTTP2, "http2", true, "Enable HTTP/2 support") // TODO: temporary flag until http2 merged into std lib + flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)") flag.StringVar(&cpu, "cpu", "100%", "CPU cap") - flag.StringVar(&config.Root, "root", config.DefaultRoot, "Root path to default site") - flag.StringVar(&config.Host, "host", config.DefaultHost, "Default host") - flag.StringVar(&config.Port, "port", config.DefaultPort, "Default port") + flag.StringVar(&caddy.Root, "root", caddy.DefaultRoot, "Root path to default site") + flag.StringVar(&caddy.Host, "host", caddy.DefaultHost, "Default host") + flag.StringVar(&caddy.Port, "port", caddy.DefaultPort, "Default port") flag.BoolVar(&version, "version", false, "Show version") flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions") - flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") + flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke its certificate") } func main() { flag.Parse() + caddy.AppName = appName + caddy.AppVersion = appVersion + if version { - fmt.Printf("%s %s\n", app.Name, app.Version) + fmt.Printf("%s %s\n", caddy.AppName, caddy.AppVersion) os.Exit(0) } if revoke != "" { @@ -59,165 +62,103 @@ func main() { } // Set CPU cap - err := app.SetCPU(cpu) + err := setCPU(cpu) if err != nil { log.Fatal(err) } - // Load config from file - groupings, err := loadConfigs() + // Get Caddyfile input + caddyfile, err := caddy.LoadCaddyfile(loadCaddyfile) if err != nil { log.Fatal(err) } - // Start each server with its one or more configurations - for i, group := range groupings { - s, err := server.New(group.BindAddr.String(), group.Configs) - if err != nil { - log.Fatal(err) - } - s.HTTP2 = app.HTTP2 // TODO: This setting is temporary - - app.Wg.Add(1) - go func(s *server.Server, i int) { - defer app.Wg.Done() - - if os.Getenv("CADDY_RESTART") == "true" { - file := os.NewFile(uintptr(3+i), "") - ln, err := net.FileListener(file) - if err != nil { - log.Fatal("FILE LISTENER:", err) - } - - lnf, ok := ln.(server.ListenerFile) - if !ok { - log.Fatal("Listener was not a ListenerFile") - } - - err = s.Serve(lnf) - // TODO: Better error logging... also, is it even necessary? - if err != nil { - log.Println(err) - } - } else { - err := s.ListenAndServe() - // TODO: Better error logging... also, is it even necessary? - // For example, "use of closed network connection" is normal if doing graceful shutdown... - if err != nil { - log.Println(err) - } - } - }(s, i) - - app.ServersMutex.Lock() - app.Servers = append(app.Servers, s) - app.ServersMutex.Unlock() - } - - // Show initialization output - if !app.Quiet { - var checkedFdLimit bool - for _, group := range groupings { - for _, conf := range group.Configs { - // Print address of site - fmt.Println(conf.Address()) - - // Note if non-localhost site resolves to loopback interface - if group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { - fmt.Printf("Notice: %s is only accessible on this machine (%s)\n", - conf.Host, group.BindAddr.IP.String()) - } - if !checkedFdLimit && !group.BindAddr.IP.IsLoopback() && !isLocalhost(conf.Host) { - checkFdlimit() - checkedFdLimit = true - } - } - } + // Start your engines + err = caddy.Start(caddyfile) + if err != nil { + log.Fatal(err) } // TODO: Temporary; testing restart - if os.Getenv("CADDY_RESTART") != "true" { - go func() { - time.Sleep(5 * time.Second) - fmt.Println("restarting") - log.Println("RESTART ERR:", app.Restart([]byte{})) - }() - } + //if os.Getenv("CADDY_RESTART") != "true" { + go func() { + time.Sleep(5 * time.Second) + fmt.Println("restarting") + log.Println("RESTART ERR:", caddy.Restart(nil)) + }() + //} - // Wait for all servers to be stopped - app.Wg.Wait() + // Twiddle your thumbs + caddy.Wait() } -// checkFdlimit issues a warning if the OS max file descriptors is below a recommended minimum. -func checkFdlimit() { - const min = 4096 - - // Warn if ulimit is too low for production sites - if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { - out, err := exec.Command("sh", "-c", "ulimit -n").Output() // use sh because ulimit isn't in Linux $PATH - if err == nil { - // Note that an error here need not be reported - lim, err := strconv.Atoi(string(bytes.TrimSpace(out))) - if err == nil && lim < min { - fmt.Printf("Warning: File descriptor limit %d is too low for production sites. At least %d is recommended. Set with \"ulimit -n %d\".\n", lim, min, min) - } - } - } -} - -// isLocalhost returns true if the string looks explicitly like a localhost address. -func isLocalhost(s string) bool { - return s == "localhost" || s == "::1" || strings.HasPrefix(s, "127.") -} - -// loadConfigs loads configuration from a file or stdin (piped). -// The configurations are grouped by bind address. -// Configuration is obtained from one of four sources, tried -// in this order: 1. -conf flag, 2. stdin, 3. command line argument 4. Caddyfile. -// If none of those are available, a default configuration is loaded. -func loadConfigs() (config.Group, error) { +func loadCaddyfile() (caddy.Input, error) { // -conf flag if conf != "" { - file, err := os.Open(conf) + contents, err := ioutil.ReadFile(conf) if err != nil { return nil, err } - defer file.Close() - return config.Load(path.Base(conf), file) + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: conf, + }, nil } - // stdin - fi, err := os.Stdin.Stat() - if err == nil && fi.Mode()&os.ModeCharDevice == 0 { - // Note that a non-nil error is not a problem. Windows - // will not create a stdin if there is no pipe, which - // produces an error when calling Stat(). But Unix will - // make one either way, which is why we also check that - // bitmask. - confBody, err := ioutil.ReadAll(os.Stdin) - if err != nil { - log.Fatal(err) - } - if len(confBody) > 0 { - return config.Load("stdin", bytes.NewReader(confBody)) - } - } - - // Command line args + // command line args if flag.NArg() > 0 { - confBody := ":" + config.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") - return config.Load("args", bytes.NewBufferString(confBody)) + confBody := ":" + caddy.DefaultPort + "\n" + strings.Join(flag.Args(), "\n") + return caddy.CaddyfileInput{ + Contents: []byte(confBody), + Filepath: "args", + }, nil } - // Caddyfile - file, err := os.Open(config.DefaultConfigFile) + // Caddyfile in cwd + contents, err := ioutil.ReadFile(caddy.DefaultConfigFile) if err != nil { if os.IsNotExist(err) { - return config.Default() + return caddy.DefaultInput, nil } return nil, err } - defer file.Close() - - return config.Load(config.DefaultConfigFile, file) + return caddy.CaddyfileInput{ + Contents: contents, + Filepath: caddy.DefaultConfigFile, + }, nil +} + +// setCPU parses string cpu and sets GOMAXPROCS +// according to its value. It accepts either +// a number (e.g. 3) or a percent (e.g. 50%). +func setCPU(cpu string) error { + var numCPU int + + availCPU := runtime.NumCPU() + + if strings.HasSuffix(cpu, "%") { + // Percent + var percent float32 + pctStr := cpu[:len(cpu)-1] + pctInt, err := strconv.Atoi(pctStr) + if err != nil || pctInt < 1 || pctInt > 100 { + return errors.New("invalid CPU value: percentage must be between 1-100") + } + percent = float32(pctInt) / 100 + numCPU = int(float32(availCPU) * percent) + } else { + // Number + num, err := strconv.Atoi(cpu) + if err != nil || num < 1 { + return errors.New("invalid CPU value: provide a number or percent greater than 0") + } + numCPU = num + } + + if numCPU > availCPU { + numCPU = availCPU + } + + runtime.GOMAXPROCS(numCPU) + return nil } diff --git a/middleware/proxy/upstream.go b/middleware/proxy/upstream.go index 3ab8aa9b8..f068907ef 100644 --- a/middleware/proxy/upstream.go +++ b/middleware/proxy/upstream.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/mholt/caddy/config/parse" + "github.com/mholt/caddy/caddy/parse" ) var ( diff --git a/server/graceful.go b/server/graceful.go index 8f74ec96f..6b2ae4f5c 100644 --- a/server/graceful.go +++ b/server/graceful.go @@ -65,6 +65,12 @@ type gracefulConn struct { // Close closes c's underlying connection while updating the wg count. func (c gracefulConn) Close() error { + err := c.Conn.Close() + if err != nil { + return err + } + // close can fail on http2 connections (as of Oct. 2015, before http2 in std lib) + // so don't decrement count unless close succeeds c.httpWg.Done() - return c.Conn.Close() + return nil } diff --git a/server/server.go b/server/server.go index 9ead46219..befbe86c4 100644 --- a/server/server.go +++ b/server/server.go @@ -59,14 +59,13 @@ func New(addr string, configs []Config) (*Server, error) { tls: tls, vhosts: make(map[string]virtualHost), } - s.Handler = s // TODO: this is weird + s.Handler = s // this is weird, but whatever // We have to bound our wg with one increment // to prevent a "race condition" that is hard-coded // into sync.WaitGroup.Wait() - basically, an add // with a positive delta must be guaranteed to // occur before Wait() is called on the wg. - fmt.Println("+1 (new)") s.httpWg.Add(1) // Set up each virtualhost @@ -169,11 +168,6 @@ func (s *Server) setup() error { // by the Go Authors. It has been modified to support multiple certificate/key pairs, // client authentication, and our custom Server type. func serveTLSWithSNI(s *Server, ln net.Listener, tlsConfigs []TLSConfig) error { - addr := s.Server.Addr - if addr == "" { - addr = ":https" - } - config := cloneTLSConfig(s.TLSConfig) if config.NextProtos == nil { config.NextProtos = []string{"http/1.1"} @@ -267,7 +261,7 @@ func (s *Server) ListenerFd() uintptr { // (configuration and middleware stack) will handle the request. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Println("Sleeping") - time.Sleep(5 * time.Second) + time.Sleep(5 * time.Second) // TODO: Temporarily making requests hang so we can test graceful restart fmt.Println("Unblocking") defer func() { // In case the user doesn't enable error middleware, we still