package serve import ( "context" "errors" "os" "time" "github.com/spf13/cobra" "github.com/tarampampam/error-pages/internal/breaker" "github.com/tarampampam/error-pages/internal/config" appHttp "github.com/tarampampam/error-pages/internal/http" "github.com/tarampampam/error-pages/internal/pick" "go.uber.org/zap" ) // NewCommand creates `serve` command. func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command { var ( f flags cfg *config.Config ) cmd := &cobra.Command{ Use: "serve", Aliases: []string{"s", "server"}, Short: "Start HTTP server", PreRunE: func(cmd *cobra.Command, _ []string) (err error) { if configFile == nil { return errors.New("path to the config file is required for this command") } if err = f.overrideUsingEnv(cmd.Flags()); err != nil { return err } if cfg, err = config.FromYamlFile(*configFile); err != nil { return err } return f.validate() }, RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) }, } f.init(cmd.Flags()) return cmd } // run current command. func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen var ( ctx, cancel = context.WithCancel(parentCtx) // serve context creation oss = breaker.NewOSSignals(ctx) // OS signals listener ) // subscribe for system signals oss.Subscribe(func(sig os.Signal) { log.Warn("Stopping by OS signal..", zap.String("signal", sig.String())) cancel() }) defer func() { cancel() // call the cancellation function after all oss.Stop() // stop system signals listening }() var ( templateNames = cfg.TemplateNames() picker *pick.StringsSlice ) switch f.template.name { case useRandomTemplate: log.Info("A random template will be used") picker = pick.NewStringsSlice(templateNames, pick.RandomOnce) case useRandomTemplateOnEachRequest: log.Info("A random template on EACH request will be used") picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime) case "": log.Info("The first template (ordered by name) will be used") picker = pick.NewStringsSlice(templateNames, pick.First) default: if t, found := cfg.Template(f.template.name); found { log.Info("We will use the requested template", zap.String("name", t.Name())) picker = pick.NewStringsSlice([]string{t.Name()}, pick.First) } else { return errors.New("requested nonexistent template: " + f.template.name) } } // create HTTP server server := appHttp.NewServer(log) // register server routes, middlewares, etc. server.Register(cfg, picker, f.defaultErrorPage, f.defaultHTTPCode, f.showDetails) startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error // start HTTP server in separate goroutine go func(errCh chan<- error) { defer close(errCh) log.Info("Server starting", zap.String("addr", f.listen.ip), zap.Uint16("port", f.listen.port), zap.String("default error page", f.defaultErrorPage), zap.Uint16("default HTTP response code", f.defaultHTTPCode), zap.Bool("show request details", f.showDetails), ) if err := server.Start(f.listen.ip, f.listen.port); err != nil { errCh <- err } }(startingErrCh) // and wait for... select { case err := <-startingErrCh: // ..server starting error return err case <-ctx.Done(): // ..or context cancellation log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt))) // stop the server using created context above if err := server.Stop(); err != nil { return err } } return nil }