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