package serve import ( "context" "errors" "fmt" "log" "log/slog" "os" "os/signal" "strconv" "strings" "syscall" "github.com/spf13/cobra" "git.avsdev.uk/Go/job-search/internal/cli" "git.avsdev.uk/Go/job-search/internal/logging" "git.avsdev.uk/Go/job-search/internal/server" ) // NewCommand returns the serve command, which serves the frontend, the REST // API and the WebSocket endpoint. func NewCommand(opts *cli.Options) *cobra.Command { cmd := &cobra.Command{ Use: "serve", Short: "Serve the frontend (/), REST API (/api) and WebSocket (/ws)", Long: `Serve the embedded Vite frontend, the REST API under /api and the WebSocket endpoint under /ws.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { slog.Debug("command persistent pre-run", "command", "serve") }, PersistentPostRun: func(cmd *cobra.Command, args []string) { slog.Debug("command persistent post-run", "command", "serve") }, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) (err error) { slog.Debug("command run", "command", "serve") addrOverride, _ := cmd.Flags().GetString("listen") hostOverride, _ := cmd.Flags().GetString("host") portOverride, _ := cmd.Flags().GetInt("port") foreground, _ := cmd.Flags().GetBool("foreground") accessLogOverride, _ := cmd.Flags().GetString("access-log") if addrOverride != "" { listenParts := strings.Split(addrOverride, ":") if len(listenParts) != 2 { return errors.New( "--listen should be in the format of 127.0.0.1:9393", ) } portOverride, err = strconv.Atoi(listenParts[1]) if err != nil { return fmt.Errorf("failed to parse --listen port: %w", err) } hostOverride = listenParts[0] } conf, err := opts.RunSetup(cmd, true, foreground) if err != nil { return fmt.Errorf("load configuration: %w", err) } if portOverride != 0 { conf.Server.Port = portOverride } if hostOverride != "" { conf.Server.Host = hostOverride } if cmd.Flags().Changed("access-log") { conf.Server.AccessLog = accessLogOverride } var options []server.Option if conf.Server.AccessLog != "" { var ( accessLog *log.Logger closeAccessLog func() error ) accessLog, closeAccessLog, err = logging.OpenAccessLog( conf.Server.AccessLog, ) if err != nil { return err } defer func() { err = errors.Join(err, closeAccessLog()) }() options = append(options, server.WithAccessLog(accessLog)) } // ctx is cancelled on the first SIGINT (Ctrl+C) or SIGTERM. ctx, stop := signal.NotifyContext( context.Background(), os.Interrupt, syscall.SIGTERM, ) defer stop() // After the first signal, restore default signal handling so a second // Ctrl+C force-kills the process if graceful shutdown hangs. go func() { <-ctx.Done() stop() }() appServer, err := server.New(&conf.Server, options...) if err != nil { return err } return appServer.Run(ctx) }, } flags := cmd.Flags() flags.String( "listen", "", "listen address (default from config, 127.0.0.1:9393)", ) flags.String( "host", "", "listen address (default from config, 127.0.0.1)", ) flags.Int("port", 0, "listen address (default from config, 9393)") flags.Bool("foreground", false, "run in the foreground") flags.String( "access-log", "", `access log file, or "-" for stdout; "" for none `+ "(default from config, server.access_log)", ) cmd.MarkFlagsMutuallyExclusive("listen", "host") cmd.MarkFlagsMutuallyExclusive("listen", "port") return cmd }