Files
triggerssmith/cmd/serve.go
2025-12-17 18:51:46 +02:00

209 lines
6.1 KiB
Go

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)
}