diff --git a/config/config.go b/config/config.go index b6544b146..32772b763 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,51 @@ const ( defaultRoot = "." ) +// config represents a server configuration. It +// is populated by parsing a config file (via the +// Load function). +type Config struct { + // The hostname or IP to which to bind the server + Host string + + // The port to listen on + Port string + + // The directory from which to serve files + Root string + + // HTTPS configuration + TLS TLSConfig + + // Middleware stack + Middleware map[string][]middleware.Middleware + + // Functions (or methods) to execute at server start; these + // are executed before any parts of the server are configured, + // and the functions are blocking + Startup []func() error + + // Functions (or methods) to execute when the server quits; + // these are executed in response to SIGINT and are blocking + Shutdown []func() error + + // MaxCPU is the maximum number of cores for the whole process to use + MaxCPU int +} + +// Address returns the host:port of c as a string. +func (c Config) Address() string { + return c.Host + ":" + c.Port +} + +// TLSConfig describes how TLS should be configured and used, +// if at all. A certificate and key are both required. +type TLSConfig struct { + Enabled bool + Certificate string + Key string +} + // Load loads a configuration file, parses it, // and returns a slice of Config structs which // can be used to create and configure server @@ -54,43 +99,3 @@ func Default() []Config { } return cfg } - -// config represents a server configuration. It -// is populated by parsing a config file (via the -// Load function). -type Config struct { - // The hostname or IP to which to bind the server - Host string - - // The port to listen on - Port string - - // The directory from which to serve files - Root string - - // HTTPS configuration - TLS TLSConfig - - // Middleware stack - Middleware map[string][]middleware.Middleware - - // Functions (or methods) to execute at server start; these - // are executed before any parts of the server are configured - Startup []func() error - - // MaxCPU is the maximum number of cores for the whole process to use - MaxCPU int -} - -// Address returns the host:port of c as a string. -func (c Config) Address() string { - return c.Host + ":" + c.Port -} - -// TLSConfig describes how TLS should be configured and used, -// if at all. At least a certificate and key are required. -type TLSConfig struct { - Enabled bool - Certificate string - Key string -} diff --git a/config/directives.go b/config/directives.go index c5c9453f3..162d56bf2 100644 --- a/config/directives.go +++ b/config/directives.go @@ -2,9 +2,12 @@ package config import ( "os" + "os/exec" "runtime" "strconv" "strings" + + "github.com/mholt/caddy/middleware" ) // dirFunc is a type of parsing function which processes @@ -111,5 +114,55 @@ func init() { } return nil }, + "startup": func(p *parser) error { + // TODO: This code is duplicated with the shutdown directive below + + if !p.nextArg() { + return p.argErr() + } + + command, args, err := middleware.SplitCommandAndArgs(p.tkn()) + if err != nil { + return p.err("Parse", err.Error()) + } + + startupfn := func() error { + cmd := exec.Command(command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + return nil + } + + p.cfg.Startup = append(p.cfg.Startup, startupfn) + return nil + }, + "shutdown": func(p *parser) error { + if !p.nextArg() { + return p.argErr() + } + + command, args, err := middleware.SplitCommandAndArgs(p.tkn()) + if err != nil { + return p.err("Parse", err.Error()) + } + + shutdownfn := func() error { + cmd := exec.Command(command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + return nil + } + + p.cfg.Shutdown = append(p.cfg.Shutdown, shutdownfn) + return nil + }, } } diff --git a/middleware/commands.go b/middleware/commands.go new file mode 100644 index 000000000..f37731973 --- /dev/null +++ b/middleware/commands.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "errors" + + "github.com/flynn/go-shlex" +) + +// SplitCommandAndArgs takes a command string and parses it +// shell-style into the command and its separate arguments. +func SplitCommandAndArgs(command string) (cmd string, args []string, err error) { + parts, err := shlex.Split(command) + if err != nil { + err = errors.New("Error parsing command: " + err.Error()) + return + } else if len(parts) == 0 { + err = errors.New("No command contained in '" + command + "'") + return + } + + cmd = parts[0] + if len(parts) > 1 { + args = parts[1:] + } + + return +} diff --git a/middleware/websockets/websockets.go b/middleware/websockets/websockets.go index 8141f7d93..1a0f2e310 100644 --- a/middleware/websockets/websockets.go +++ b/middleware/websockets/websockets.go @@ -4,10 +4,8 @@ package websockets import ( - "errors" "net/http" - "github.com/flynn/go-shlex" "github.com/mholt/caddy/middleware" "golang.org/x/net/websocket" ) @@ -101,7 +99,7 @@ func New(c middleware.Controller) (middleware.Middleware, error) { } // Split command into the actual command and its arguments - cmd, args, err := parseCommandAndArgs(command) + cmd, args, err := middleware.SplitCommandAndArgs(command) if err != nil { return nil, err } @@ -122,26 +120,6 @@ func New(c middleware.Controller) (middleware.Middleware, error) { }, nil } -// parseCommandAndArgs takes a command string and parses it -// shell-style into the command and its separate arguments. -func parseCommandAndArgs(command string) (cmd string, args []string, err error) { - parts, err := shlex.Split(command) - if err != nil { - err = errors.New("Error parsing command for websocket: " + err.Error()) - return - } else if len(parts) == 0 { - err = errors.New("No command found for use by websocket") - return - } - - cmd = parts[0] - if len(parts) > 1 { - args = parts[1:] - } - - return -} - var ( // See CGI spec, 4.1.4 GatewayInterface string diff --git a/server/server.go b/server/server.go index ecb596449..b5e3f1663 100644 --- a/server/server.go +++ b/server/server.go @@ -7,6 +7,8 @@ import ( "errors" "log" "net/http" + "os" + "os/signal" "runtime" "github.com/bradfitz/http2" @@ -75,6 +77,21 @@ func (s *Server) Serve() error { http2.ConfigureServer(server, nil) // TODO: This may not be necessary after HTTP/2 merged into std lib + // Execute shutdown commands on exit + // TODO: Is graceful net/http shutdown necessary? + go func() { + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, os.Kill) // TODO: syscall.SIGQUIT? (Ctrl+\, Unix-only) + <-interrupt + for _, shutdownFunc := range s.config.Shutdown { + err := shutdownFunc() + if err != nil { + log.Fatal(err) + } + } + os.Exit(0) + }() + if s.config.TLS.Enabled { return server.ListenAndServeTLS(s.config.TLS.Certificate, s.config.TLS.Key) } else { @@ -103,6 +120,7 @@ func (s *Server) Log(v ...interface{}) { func (s *Server) buildStack() error { s.fileServer = FileServer(http.Dir(s.config.Root)) + // Execute startup functions for _, start := range s.config.Startup { err := start() if err != nil {