diff --git a/app.go b/app.go index 9386b590b..d85ff7121 100644 --- a/app.go +++ b/app.go @@ -1,20 +1,11 @@ package buffalo import ( - "context" - "net" "net/http" - "os" - "strings" "sync" - "syscall" - - "github.com/sirupsen/logrus" "github.com/gobuffalo/envy" "github.com/gorilla/mux" - "github.com/markbates/refresh/refresh/web" - "github.com/markbates/sigtx" "github.com/pkg/errors" ) @@ -33,100 +24,6 @@ type App struct { children []*App } -// Serve the application at the specified address/port and listen for OS -// interrupt and kill signals and will attempt to stop the application -// gracefully. This will also start the Worker process, unless WorkerOff is enabled. -func (a *App) Serve() error { - logrus.Infof("Starting application at %s", a.Options.Host) - server := http.Server{ - Handler: a, - } - ctx, cancel := sigtx.WithCancel(a.Context, syscall.SIGTERM, os.Interrupt) - defer cancel() - - go func() { - // gracefully shut down the application when the context is cancelled - <-ctx.Done() - logrus.Info("Shutting down application") - - if err := a.Stop(ctx.Err()); err != nil { - logrus.Error(err) - } - - if !a.WorkerOff { - // stop the workers - logrus.Info("Shutting down worker") - if err := a.Worker.Stop(); err != nil { - logrus.Error(err) - } - } - - if err := server.Shutdown(ctx); err != nil { - logrus.Error(err) - } - - }() - - // if configured to do so, start the workers - if !a.WorkerOff { - go func() { - if err := a.Worker.Start(ctx); err != nil { - a.Stop(err) - } - }() - } - - if strings.HasPrefix(a.Options.Addr, "unix:") { - // Use an UNIX socket - listener, err := net.Listen("unix", a.Options.Addr[5:]) - if err != nil { - return a.Stop(err) - } - // start the web server - if err = server.Serve(listener); err != nil { - return a.Stop(err) - } - return nil - } - // Use a TCP socket - server.Addr = a.Options.Addr - - // start the web server - if err := server.ListenAndServe(); err != nil { - return a.Stop(err) - } - - return nil -} - -// Stop the application and attempt to gracefully shutdown -func (a *App) Stop(err error) error { - a.cancel() - if err != nil && errors.Cause(err) != context.Canceled { - logrus.Error(err) - return err - } - return nil -} - -func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ws := &Response{ - ResponseWriter: w, - } - if a.MethodOverride != nil { - a.MethodOverride(w, r) - } - if ok := a.processPreHandlers(ws, r); !ok { - return - } - - var h http.Handler = a.router - if a.Env == "development" { - h = web.ErrorChecker(h) - } - h.ServeHTTP(ws, r) -} - // New returns a new instance of App and adds some sane, and useful, defaults. func New(opts Options) *App { envy.Load() @@ -165,30 +62,3 @@ func New(opts Options) *App { return a } - -func (a *App) processPreHandlers(res http.ResponseWriter, req *http.Request) bool { - sh := func(h http.Handler) bool { - h.ServeHTTP(res, req) - if br, ok := res.(*Response); ok { - if br.Status > 0 || br.Size > 0 { - return false - } - } - return true - } - - for _, ph := range a.PreHandlers { - if ok := sh(ph); !ok { - return false - } - } - - last := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {})) - for _, ph := range a.PreWares { - last = ph(last) - if ok := sh(last); !ok { - return false - } - } - return true -} diff --git a/server.go b/server.go new file mode 100644 index 000000000..0e36e0689 --- /dev/null +++ b/server.go @@ -0,0 +1,138 @@ +package buffalo + +import ( + "context" + "net/http" + "os" + "strings" + "syscall" + + "github.com/gobuffalo/buffalo/servers" + "github.com/markbates/refresh/refresh/web" + "github.com/markbates/sigtx" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Serve the application at the specified address/port and listen for OS +// interrupt and kill signals and will attempt to stop the application +// gracefully. This will also start the Worker process, unless WorkerOff is enabled. +func (a *App) Serve(srvs ...servers.Server) error { + logrus.Infof("Starting application at %s", a.Options.Host) + + if len(srvs) == 0 { + if strings.HasPrefix(a.Options.Addr, "unix:") { + tcp, err := servers.UnixSocket(a.Options.Addr[5:]) + if err != nil { + return errors.WithStack(err) + } + srvs = append(srvs, tcp) + } else { + srvs = append(srvs, servers.New()) + } + } + + ctx, cancel := sigtx.WithCancel(a.Context, syscall.SIGTERM, os.Interrupt) + defer cancel() + + go func() { + // gracefully shut down the application when the context is cancelled + <-ctx.Done() + logrus.Info("Shutting down application") + + if err := a.Stop(ctx.Err()); err != nil { + logrus.Error(err) + } + + if !a.WorkerOff { + // stop the workers + logrus.Info("Shutting down worker") + if err := a.Worker.Stop(); err != nil { + logrus.Error(err) + } + } + + for _, s := range srvs { + if err := s.Shutdown(ctx); err != nil { + logrus.Error(err) + } + } + + }() + + // if configured to do so, start the workers + if !a.WorkerOff { + go func() { + if err := a.Worker.Start(ctx); err != nil { + a.Stop(err) + } + }() + } + + for _, s := range srvs { + s.SetAddr(a.Addr) + go func(s servers.Server) { + if err := s.Start(ctx, a); err != nil { + a.Stop(err) + } + }(s) + } + + <-ctx.Done() + return a.Context.Err() +} + +// Stop the application and attempt to gracefully shutdown +func (a *App) Stop(err error) error { + a.cancel() + if err != nil && errors.Cause(err) != context.Canceled { + logrus.Error(err) + return err + } + return nil +} + +func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ws := &Response{ + ResponseWriter: w, + } + if a.MethodOverride != nil { + a.MethodOverride(w, r) + } + if ok := a.processPreHandlers(ws, r); !ok { + return + } + + var h http.Handler = a.router + if a.Env == "development" { + h = web.ErrorChecker(h) + } + h.ServeHTTP(ws, r) +} + +func (a *App) processPreHandlers(res http.ResponseWriter, req *http.Request) bool { + sh := func(h http.Handler) bool { + h.ServeHTTP(res, req) + if br, ok := res.(*Response); ok { + if br.Status > 0 || br.Size > 0 { + return false + } + } + return true + } + + for _, ph := range a.PreHandlers { + if ok := sh(ph); !ok { + return false + } + } + + last := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {})) + for _, ph := range a.PreWares { + last = ph(last) + if ok := sh(last); !ok { + return false + } + } + return true +} diff --git a/servers/listener.go b/servers/listener.go new file mode 100644 index 000000000..364ec6796 --- /dev/null +++ b/servers/listener.go @@ -0,0 +1,40 @@ +package servers + +import ( + "context" + "net" + "net/http" + + "github.com/pkg/errors" +) + +// Listener server for using a pre-defined net.Listener +type Listener struct { + *http.Server + Listener net.Listener +} + +// SetAddr sets the servers address, if it hasn't already been set +func (s *Listener) SetAddr(addr string) { + if s.Server.Addr == "" { + s.Server.Addr = addr + } +} + +// Start the server +func (s *Listener) Start(c context.Context, h http.Handler) error { + s.Handler = h + return s.Serve(s.Listener) +} + +// UnixSocket returns a new Listener on that address +func UnixSocket(addr string) (*Listener, error) { + listener, err := net.Listen("unix", addr) + if err != nil { + return nil, errors.WithStack(err) + } + return &Listener{ + Server: &http.Server{}, + Listener: listener, + }, nil +} diff --git a/servers/servers.go b/servers/servers.go new file mode 100644 index 000000000..4a703d74f --- /dev/null +++ b/servers/servers.go @@ -0,0 +1,37 @@ +package servers + +import ( + "context" + "net" + "net/http" +) + +// Server allows for custom server implementations +type Server interface { + Shutdown(context.Context) error + Start(context.Context, http.Handler) error + SetAddr(string) +} + +// Wrap converts a standard *http.Server to a buffalo.Server +func Wrap(s *http.Server) Server { + return &Simple{Server: s} +} + +// WrapTLS Server converts a standard *http.Server to a buffalo.Server +// but makes sure it is run with TLS. +func WrapTLS(s *http.Server, certFile string, keyFile string) Server { + return &TLS{ + Server: s, + CertFile: certFile, + KeyFile: keyFile, + } +} + +// WrapListener wraps an *http.Server and a net.Listener +func WrapListener(s *http.Server, l net.Listener) Server { + return &Listener{ + Server: s, + Listener: l, + } +} diff --git a/servers/simple.go b/servers/simple.go new file mode 100644 index 000000000..b0e30d0d1 --- /dev/null +++ b/servers/simple.go @@ -0,0 +1,31 @@ +package servers + +import ( + "context" + "net/http" +) + +// Simple server +type Simple struct { + *http.Server +} + +// SetAddr sets the servers address, if it hasn't already been set +func (s *Simple) SetAddr(addr string) { + if s.Server.Addr == "" { + s.Server.Addr = addr + } +} + +// Start the server +func (s *Simple) Start(c context.Context, h http.Handler) error { + s.Handler = h + return s.ListenAndServe() +} + +// New Simple server +func New() *Simple { + return &Simple{ + Server: &http.Server{}, + } +} diff --git a/servers/tls.go b/servers/tls.go new file mode 100644 index 000000000..826eaa748 --- /dev/null +++ b/servers/tls.go @@ -0,0 +1,26 @@ +package servers + +import ( + "context" + "net/http" +) + +// TLS server +type TLS struct { + *http.Server + CertFile string + KeyFile string +} + +// SetAddr sets the servers address, if it hasn't already been set +func (s *TLS) SetAddr(addr string) { + if s.Server.Addr == "" { + s.Server.Addr = addr + } +} + +// Start the server +func (s *TLS) Start(c context.Context, h http.Handler) error { + s.Handler = h + return s.ListenAndServeTLS(s.CertFile, s.KeyFile) +}