Files
triggerssmith/cmd/serve.go
2025-12-19 14:26:37 +02:00

322 lines
9.9 KiB
Go

package cmd
import (
"fmt"
"log/slog"
"net"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"syscall"
"time"
"git.oblat.lv/alex/triggerssmith/api"
"git.oblat.lv/alex/triggerssmith/internal/acl"
application "git.oblat.lv/alex/triggerssmith/internal/app"
"git.oblat.lv/alex/triggerssmith/internal/auth"
"git.oblat.lv/alex/triggerssmith/internal/config"
"git.oblat.lv/alex/triggerssmith/internal/jwt"
"git.oblat.lv/alex/triggerssmith/internal/server"
"git.oblat.lv/alex/triggerssmith/internal/token"
"git.oblat.lv/alex/triggerssmith/internal/user"
"git.oblat.lv/alex/triggerssmith/internal/vars"
"github.com/spf13/cobra"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var optsServeCmd = struct {
ConfigPath *string
Debug *bool
HideGreetings *bool
NoPIDFile *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.Fprintf(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.Fprintf(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})))
}
if !*optsServeCmd.NoPIDFile {
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)
} else {
slog.Warn("Starting server without PID file as requested by --no-pidfile flag: this may complicate process management")
}
// 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()
// Services initialization
var jwtSigner jwt.Signer
// TODO: support more signing algorithms
// : support hot config reload for signing alg and secret
switch cfg.Auth.SignAlg {
case "HS256":
secretBytes, err := os.ReadFile(cfg.Auth.HMACSecretPath)
if err != nil {
slog.Error("Failed to read HMAC secret file", slog.String("path", cfg.Auth.HMACSecretPath), slog.String("error", err.Error()))
return
}
jwtSigner = jwt.NewHMACSigner(secretBytes)
default:
slog.Error("Unsupported JWT signing algorithm", slog.String("alg", cfg.Auth.SignAlg))
return
}
jwtService := jwt.NewService(jwtSigner)
err = os.MkdirAll(cfg.Data.DataPath, 0755)
if err != nil {
slog.Error("Failed to create data directory", slog.String("path", cfg.Data.DataPath), slog.String("error", err.Error()))
return
}
tokenDb, err := gorm.Open(sqlite.Open(filepath.Join(cfg.Data.DataPath, "tokens.sqlite3")), &gorm.Config{})
if err != nil {
slog.Error("Failed to open token database", slog.String("error", err.Error()))
return
}
// err = tokenDb.AutoMigrate(&token.Token{})
// if err != nil {
// slog.Error("Failed to migrate token database", slog.String("error", err.Error()))
// return
// }
tokenStore, err := token.NewSQLiteTokenStore(tokenDb)
if err != nil {
slog.Error("Failed to create token store", slog.String("error", err.Error()))
return
}
tokenService, err := token.NewTokenService(&cfg.Auth, tokenStore)
if err != nil {
slog.Error("Failed to create token service", slog.String("error", err.Error()))
return
}
err = tokenService.Init()
if err != nil {
slog.Error("Failed to initialize token service", slog.String("error", err.Error()))
return
}
// also acl !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
userData, err := gorm.Open(sqlite.Open(filepath.Join(cfg.Data.DataPath, "user_data.sqlite3")), &gorm.Config{})
if err != nil {
slog.Error("Failed to open user database", slog.String("error", err.Error()))
return
}
// err =
// if err != nil {
// slog.Error("Failed to migrate user database", slog.String("error", err.Error()))
// return
// }
userStore, err := user.NewGormUserStore(userData)
if err != nil {
slog.Error("Failed to create user store", slog.String("error", err.Error()))
return
}
userService, err := user.NewService(userStore)
if err != nil {
slog.Error("Failed to create user service", slog.String("error", err.Error()))
return
}
err = userService.Init()
if err != nil {
slog.Error("Failed to initialize user service", slog.String("error", err.Error()))
return
}
aclService, err := acl.NewService(userData)
if err != nil {
slog.Error("Failed to create acl service", slog.String("error", err.Error()))
return
}
err = aclService.Init()
if err != nil {
slog.Error("Failed to initialize acl service", slog.String("error", err.Error()))
return
}
authService, err := auth.NewAuthService(auth.AuthServiceDependencies{
Configuration: cfg,
JWTService: jwtService,
UserService: userService,
TokenService: tokenService,
})
if err != nil {
slog.Error("Failed to create auth service", slog.String("error", err.Error()))
return
}
router := api.NewRouter(api.RouterDependencies{
AuthService: authService,
Configuration: cfg,
ACLService: aclService,
})
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")
optsServeCmd.NoPIDFile = serveCmd.Flags().BoolP("no-pidfile", "p", false, "Do not write a PID file")
rootCmd.AddCommand(serveCmd)
}