package cmd import ( "fmt" "log/slog" "net" "os" "os/signal" "path/filepath" "runtime/debug" "syscall" "time" "git.oblat.lv/alex/triggerssmith/api" application "git.oblat.lv/alex/triggerssmith/internal/app" "git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/server" "git.oblat.lv/alex/triggerssmith/internal/vars" "github.com/spf13/cobra" ) var optsServeCmd = struct { ConfigPath *string Debug *bool HideGreetings *bool }{} // // simple middleware for request logging // func loggingMiddleware(next http.Handler) http.Handler { // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // start := time.Now() // slog.Info("HTTP request", // slog.String("method", r.Method), // slog.String("path", r.URL.Path), // slog.String("remote", r.RemoteAddr), // ) // next.ServeHTTP(w, r) // slog.Debug("HTTP request finished", // slog.String("method", r.Method), // slog.String("path", r.URL.Path), // slog.Duration("latency", time.Since(start)), // ) // }) // } func writePID(path string) error { dir := filepath.Dir(path) err := os.MkdirAll(dir, 0644) if err != nil { return nil } pid := os.Getpid() f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer f.Close() _, err = fmt.Fprintf(f, "%d\n", pid) return err } var serveCmd = &cobra.Command{ Use: "serve", Short: "Start the server", Run: func(cmd *cobra.Command, args []string) { text := fmt.Sprintf(` _______ _____ |__ __/ ____| | | | (___ | | \___ \ | | ____) | |_| |_____/ TriggerSmith - v%s `, vars.Version) if !*optsServeCmd.HideGreetings { fmt.Println(text) } defer func() { if r := recover(); r != nil { slog.Debug("panic recovered: preparing panic.log", slog.Any("error", r)) stack := debug.Stack() f, err := os.OpenFile("panic.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { slog.Error("Failed to open panic.log", slog.Any("error", err)) } else { defer f.Close() slog.Debug("flushing stack in to panic.log") fmt.Fprintln(f, "\n--------------------------------------------------------\n") fmt.Fprintf(f, "Time: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintln(f, "If this is unexpected, please report: https://git.oblat.lv/alex/triggerssmith/issues") fmt.Fprintln(f, "\n--------------------------------------------------------\n") fmt.Fprintf(f, "Panic: %v\n", r) f.Write(stack) f.WriteString("\n\n") } slog.Error("Application panicked: the stack is flushed to disk", slog.Any("error", r)) os.Exit(-1) } }() // configure logger if *optsServeCmd.Debug { slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))) } else { slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelInfo}))) } pid := os.Getpid() slog.Debug("Starting server", slog.Int("pid", pid)) if err := writePID(vars.PID_PATH); err != nil { panic(err) } slog.Debug("created pid file", slog.String("path", vars.PID_PATH)) defer os.Remove(vars.PID_PATH) // load config slog.Debug("Reading configuration", slog.String("path", *optsServeCmd.ConfigPath)) cfg, err := config.LoadConfig(*optsServeCmd.ConfigPath) if err != nil { slog.Error("Failed to load configuration", slog.String("path", *optsServeCmd.ConfigPath), slog.String("error", err.Error())) return } slog.Debug("Configuration loaded", slog.Any("config", cfg)) // init app app, err := application.NewApp() if err != application.ErrNilPointerWarn && err != nil { slog.Error("Failed to create app instance", slog.String("error", err.Error())) return } app.LoadConfiguration(cfg) srv := app.Server() router := api.NewRouter(cfg) srv.SetHandler(router.MustRoute()) srv.Init() var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)) slog.Debug("Binding listener", slog.String("address", addr)) err = srv.Start(addr) if err != nil { slog.Error("Failed to start server", slog.String("error", err.Error())) return } else { slog.Info("Server started", slog.String("address", net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)))) } sigch := make(chan os.Signal, 1) signal.Notify(sigch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) for true { sig := <-sigch slog.Debug("got signal", slog.Any("os.Signal", sig)) switch sig { case syscall.SIGHUP: if err := config.ReloadConfig(cfg); err != nil { slog.Error("Failed to reload configuration", slog.String("error", err.Error())) } else { slog.Info("Configuration reloaded") var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)) slog.Debug("New configuration", slog.Any("config", cfg)) err = srv.Reload(addr) if err != nil { slog.Error("Failed to restart server with new configuration", slog.String("error", err.Error())) } } case syscall.SIGINT: slog.Info("Stopping server by SIGINT") os.Remove(vars.PID_PATH) _ = server.StopAll() //err := srv.Stop() // if err != nil { // slog.Error("Failed to stop server", slog.String("err", err.Error())) // os.Exit(1) // } return case syscall.SIGTERM: slog.Info("Stopping server by SIGTERM") os.Remove(vars.PID_PATH) _ = server.StopAll() //err := srv.Stop() // if err != nil { // slog.Error("Failed to stop server", slog.String("err", err.Error())) // os.Exit(1) // } return } } }, } func init() { optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") optsServeCmd.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") optsServeCmd.HideGreetings = serveCmd.Flags().BoolP("hide-greetings", "g", false, "Hide the welcome message and version when starting the server") rootCmd.AddCommand(serveCmd) }