diff --git a/cmd/reload.go b/cmd/reload.go new file mode 100644 index 0000000..b68bb62 --- /dev/null +++ b/cmd/reload.go @@ -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) +} diff --git a/cmd/serve.go b/cmd/serve.go index b4d2886..46ecad9 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,14 +5,18 @@ import ( "log/slog" "net" "net/http" + "os" + "os/signal" + "syscall" "time" application "git.oblat.lv/alex/triggerssmith/internal/app" "git.oblat.lv/alex/triggerssmith/internal/config" + "git.oblat.lv/alex/triggerssmith/internal/vars" "github.com/spf13/cobra" ) -var opts = struct { +var optsServeCmd = struct { ConfigPath *string 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{ Use: "serve", Short: "Start the server", Run: func(cmd *cobra.Command, args []string) { - // defer func() { - // if r := recover(); r != nil { - // slog.Error("Application panicked", slog.Any("error", r)) - // } - // }() + defer func() { + if r := recover(); r != nil { + slog.Error("Application panicked", slog.Any("error", r)) + os.Exit(-1) + } + }() // configure logger - if *opts.Debug { + 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}))) } - 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 - slog.Debug("Reading configuration", slog.String("path", *opts.ConfigPath)) - cfg, err := config.LoadConfig(*opts.ConfigPath) + 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", *opts.ConfigPath), slog.String("error", err.Error())) + 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)) @@ -73,39 +97,6 @@ var serveCmd = &cobra.Command{ } 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() 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)))) } + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) for true { - fmt.Scanln() - if err := config.ReloadConfig(cfg); err != nil { - slog.Error("Failed to reload configuration", slog.String("error", err.Error())) - } else { - slog.Info("Configuration reloaded", slog.Any("config", cfg)) - var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port)) - err = server.Reload(addr) - if err != nil { - slog.Error("Failed to restart server with new configuration", slog.String("error", err.Error())) + 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("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() { - opts.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") - opts.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") + optsServeCmd.Debug = serveCmd.Flags().BoolP("debug", "d", false, "Enable debug logs") + optsServeCmd.ConfigPath = serveCmd.Flags().StringP("config", "c", "config.yaml", "Path to configuration file") rootCmd.AddCommand(serveCmd) } diff --git a/cmd/stop.go b/cmd/stop.go new file mode 100644 index 0000000..c3f317b --- /dev/null +++ b/cmd/stop.go @@ -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) +} diff --git a/internal/server/server.go b/internal/server/server.go index b5abfd4..cc7d4ec 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -94,17 +94,17 @@ type LiveServer struct { handler http.Handler active atomic.Value // *instance - mu sync.Mutex + mu sync.Mutex statusMu sync.Mutex - status Status + status Status initDone bool } type instance struct { srv *http.Server - ln net.Listener + ln net.Listener addr string } @@ -198,10 +198,10 @@ func (ls *LiveServer) Start(addr string) error { return err } srv := &http.Server{Handler: ls.handler} - + ls.active.Store(&instance{ - srv: srv, - ln: ln, + srv: srv, + ln: ln, addr: addr, }) @@ -249,7 +249,6 @@ func (ls *LiveServer) Stop() error { return ls.stop(inst) } - func (ls *LiveServer) Reload(newAddr string) error { ls.mu.Lock() oldInstAny := ls.active.Load() @@ -258,7 +257,7 @@ func (ls *LiveServer) Reload(newAddr string) error { oldAddr = oldInstAny.(*instance).addr } ls.mu.Unlock() - + if oldAddr == newAddr { return nil } @@ -447,7 +446,6 @@ func (ls *LiveServer) Reload(newAddr string) error { // // } // ls.setStatus(Status{ID: StatusStarting}) - // err := ls.Start() // if err != nil { diff --git a/internal/server/status.go b/internal/server/status.go index ce7eda4..801e4e3 100644 --- a/internal/server/status.go +++ b/internal/server/status.go @@ -11,10 +11,10 @@ const ( ) type Status struct { - ID StatusID - Err error + ID StatusID + Err error } func (s Status) Error() string { return s.Err.Error() -} \ No newline at end of file +} diff --git a/internal/vars/const.go b/internal/vars/const.go new file mode 100644 index 0000000..243743c --- /dev/null +++ b/internal/vars/const.go @@ -0,0 +1,4 @@ +package vars + +const VAR_PATH = "/var/run/triggerssmith/" +const PID_PATH = VAR_PATH + "serve.pid"