add reloading and stopping from tsctl

This commit is contained in:
2025-11-30 11:26:22 +02:00
parent c51dfce9ec
commit ea7358a35f
6 changed files with 205 additions and 68 deletions

70
cmd/reload.go Normal file
View File

@@ -0,0 +1,70 @@
package cmd
import (
"io/ioutil"
"log/slog"
"os"
"strconv"
"strings"
"syscall"
"git.oblat.lv/alex/triggerssmith/internal/vars"
"github.com/spf13/cobra"
)
var optsReloadCmd = struct {
Debug *bool
PID *int
}{}
func readPID(path string) (int, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return 0, err
}
s := strings.TrimSpace(string(data))
pid, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return pid, nil
}
var reloadCmd = &cobra.Command{
Use: "reload",
Short: "Reload active server by PID using SIGHUP",
Run: func(cmd *cobra.Command, args []string) {
defer func() {
if r := recover(); r != nil {
slog.Error("Application panicked", slog.Any("error", r))
}
}()
// configure logger
if *optsReloadCmd.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, err := readPID(vars.PID_PATH)
if err != nil {
panic(err)
}
*optsReloadCmd.PID = pid
slog.Debug("restarting server", slog.Int("pid", *optsReloadCmd.PID))
proc, err := os.FindProcess(*optsReloadCmd.PID)
if err != nil {
slog.Error("failed to find process", slog.Int("pid", *optsReloadCmd.PID), slog.String("err", err.Error()))
}
proc.Signal(syscall.SIGHUP)
slog.Debug("done")
},
}
func init() {
optsReloadCmd.Debug = reloadCmd.Flags().BoolP("debug", "d", false, "Enable debug logs")
optsReloadCmd.PID = reloadCmd.Flags().IntP("pid", "p", -1, "Define server PID")
rootCmd.AddCommand(reloadCmd)
}

View File

@@ -5,14 +5,18 @@ import (
"log/slog" "log/slog"
"net" "net"
"net/http" "net/http"
"os"
"os/signal"
"syscall"
"time" "time"
application "git.oblat.lv/alex/triggerssmith/internal/app" application "git.oblat.lv/alex/triggerssmith/internal/app"
"git.oblat.lv/alex/triggerssmith/internal/config" "git.oblat.lv/alex/triggerssmith/internal/config"
"git.oblat.lv/alex/triggerssmith/internal/vars"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var opts = struct { var optsServeCmd = struct {
ConfigPath *string ConfigPath *string
Debug *bool Debug *bool
}{} }{}
@@ -38,29 +42,49 @@ func loggingMiddleware(next http.Handler) http.Handler {
}) })
} }
func writePID(path string) error {
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{ var serveCmd = &cobra.Command{
Use: "serve", Use: "serve",
Short: "Start the server", Short: "Start the server",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// defer func() { defer func() {
// if r := recover(); r != nil { if r := recover(); r != nil {
// slog.Error("Application panicked", slog.Any("error", r)) slog.Error("Application panicked", slog.Any("error", r))
// } os.Exit(-1)
// }() }
}()
// configure logger // configure logger
if *opts.Debug { if *optsServeCmd.Debug {
slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))) slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true})))
} else { } else {
slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelInfo}))) slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelInfo})))
} }
slog.Debug("Starting server") 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 // load config
slog.Debug("Reading configuration", slog.String("path", *opts.ConfigPath)) slog.Debug("Reading configuration", slog.String("path", *optsServeCmd.ConfigPath))
cfg, err := config.LoadConfig(*opts.ConfigPath) cfg, err := config.LoadConfig(*optsServeCmd.ConfigPath)
if err != nil { if err != nil {
slog.Error("Failed to load configuration", slog.String("path", *opts.ConfigPath), slog.String("error", err.Error())) slog.Error("Failed to load configuration", slog.String("path", *optsServeCmd.ConfigPath), slog.String("error", err.Error()))
return return
} }
slog.Debug("Configuration loaded", slog.Any("config", cfg)) slog.Debug("Configuration loaded", slog.Any("config", cfg))
@@ -73,39 +97,6 @@ var serveCmd = &cobra.Command{
} }
app.LoadConfiguration(cfg) app.LoadConfiguration(cfg)
/*
// setup handlers
mux := http.NewServeMux()
// static files
staticPath := cfg.Server.StaticFilesPath
slog.Debug("Setting up static file server", slog.String("path", staticPath))
fs := http.FileServer(http.Dir(staticPath))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
handler := loggingMiddleware(mux)
*/
// start server
/*addr := fmt.Sprintf("%s:%d", cfg.Server.Addr, cfg.Server.Port)
slog.Debug("Binding listener", slog.String("address", addr))
ln, err := net.Listen("tcp", addr)
if err != nil {
slog.Error("Failed to start listener", slog.String("address", addr), slog.String("error", err.Error()))
return
}
srv := &http.Server{
Addr: addr,
Handler: handler,
}
slog.Info("Server started", slog.String("address", addr))
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
slog.Error("HTTP server stopped with error", slog.String("error", err.Error()))
}*/
server := app.Server() server := app.Server()
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -129,26 +120,49 @@ var serveCmd = &cobra.Command{
slog.Info("Server started", slog.String("address", net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)))) 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 { for true {
fmt.Scanln() sig := <-sigch
if err := config.ReloadConfig(cfg); err != nil { slog.Debug("got signal", slog.Any("os.Signal", sig))
slog.Error("Failed to reload configuration", slog.String("error", err.Error())) switch sig {
} else { case syscall.SIGHUP:
slog.Info("Configuration reloaded", slog.Any("config", cfg)) if err := config.ReloadConfig(cfg); err != nil {
var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)) slog.Error("Failed to reload configuration", slog.String("error", err.Error()))
err = server.Reload(addr)
if err != nil {
slog.Error("Failed to restart server with new configuration", slog.String("error", err.Error()))
} else { } else {
slog.Info("Server restarted with new configuration", slog.String("address", net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)))) 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 = server.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")
err := server.Stop()
if err != nil {
slog.Error("Failed to stop server", slog.String("err", err.Error()))
os.Exit(1)
}
os.Remove(vars.PID_PATH)
return
case syscall.SIGTERM:
slog.Info("Stopping server by SIGTERM")
err := server.Stop()
if err != nil {
slog.Error("Failed to stop server", slog.String("err", err.Error()))
os.Exit(1)
}
os.Remove(vars.PID_PATH)
return
} }
} }
}, },
} }
func init() { func init() {
opts.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs")
opts.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") optsServeCmd.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file")
rootCmd.AddCommand(serveCmd) rootCmd.AddCommand(serveCmd)
} }

51
cmd/stop.go Normal file
View File

@@ -0,0 +1,51 @@
package cmd
import (
"log/slog"
"os"
"syscall"
"git.oblat.lv/alex/triggerssmith/internal/vars"
"github.com/spf13/cobra"
)
var optsStopCmd = struct {
Debug *bool
PID *int
}{}
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop active server by PID using SIGTERM",
Run: func(cmd *cobra.Command, args []string) {
defer func() {
if r := recover(); r != nil {
slog.Error("Application panicked", slog.Any("error", r))
}
}()
// configure logger
if *optsReloadCmd.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, err := readPID(vars.PID_PATH)
if err != nil {
panic(err)
}
*optsStopCmd.PID = pid
slog.Debug("restarting server", slog.Int("pid", *optsStopCmd.PID))
proc, err := os.FindProcess(*optsStopCmd.PID)
if err != nil {
slog.Error("failed to find process", slog.Int("pid", *optsStopCmd.PID), slog.String("err", err.Error()))
}
proc.Signal(syscall.SIGTERM)
slog.Debug("done")
},
}
func init() {
optsStopCmd.Debug = stopCmd.Flags().BoolP("debug", "d", false, "Enable debug logs")
optsStopCmd.PID = stopCmd.Flags().IntP("pid", "p", -1, "Define server PID")
rootCmd.AddCommand(stopCmd)
}

View File

@@ -94,17 +94,17 @@ type LiveServer struct {
handler http.Handler handler http.Handler
active atomic.Value // *instance active atomic.Value // *instance
mu sync.Mutex mu sync.Mutex
statusMu sync.Mutex statusMu sync.Mutex
status Status status Status
initDone bool initDone bool
} }
type instance struct { type instance struct {
srv *http.Server srv *http.Server
ln net.Listener ln net.Listener
addr string addr string
} }
@@ -200,8 +200,8 @@ func (ls *LiveServer) Start(addr string) error {
srv := &http.Server{Handler: ls.handler} srv := &http.Server{Handler: ls.handler}
ls.active.Store(&instance{ ls.active.Store(&instance{
srv: srv, srv: srv,
ln: ln, ln: ln,
addr: addr, addr: addr,
}) })
@@ -249,7 +249,6 @@ func (ls *LiveServer) Stop() error {
return ls.stop(inst) return ls.stop(inst)
} }
func (ls *LiveServer) Reload(newAddr string) error { func (ls *LiveServer) Reload(newAddr string) error {
ls.mu.Lock() ls.mu.Lock()
oldInstAny := ls.active.Load() oldInstAny := ls.active.Load()
@@ -448,7 +447,6 @@ func (ls *LiveServer) Reload(newAddr string) error {
// ls.setStatus(Status{ID: StatusStarting}) // ls.setStatus(Status{ID: StatusStarting})
// err := ls.Start() // err := ls.Start()
// if err != nil { // if err != nil {
// ls.active.Store(oldInstAny) // ls.active.Store(oldInstAny)

View File

@@ -11,8 +11,8 @@ const (
) )
type Status struct { type Status struct {
ID StatusID ID StatusID
Err error Err error
} }
func (s Status) Error() string { func (s Status) Error() string {

4
internal/vars/const.go Normal file
View File

@@ -0,0 +1,4 @@
package vars
const VAR_PATH = "/var/run/triggerssmith/"
const PID_PATH = VAR_PATH + "serve.pid"