server with basic hot reload
This commit is contained in:
4
Makefile
4
Makefile
@@ -27,9 +27,11 @@ run: build
|
|||||||
@echo "-- running $(NAME)"
|
@echo "-- running $(NAME)"
|
||||||
@$(BINARY)
|
@$(BINARY)
|
||||||
|
|
||||||
|
#BUILD_PARAMS = -trimpath
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@echo "-- building $(NAME)"
|
@echo "-- building $(NAME)"
|
||||||
@go build -o $(BINARY) $(ENTRY)
|
@go build $(BUILD_PARAMS) -o $(BINARY) $(ENTRY)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@echo "-- testing $(NAME)"
|
@echo "-- testing $(NAME)"
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
return rootCmd.Execute()
|
return rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
123
cmd/serve.go
123
cmd/serve.go
@@ -1,36 +1,149 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
application "git.oblat.lv/alex/triggerssmith/internal/app"
|
||||||
"git.oblat.lv/alex/triggerssmith/internal/config"
|
"git.oblat.lv/alex/triggerssmith/internal/config"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var opts = struct {
|
var opts = struct {
|
||||||
ConfigPath *string
|
ConfigPath *string
|
||||||
Debug *bool
|
Debug *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)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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() {
|
||||||
|
// if r := recover(); r != nil {
|
||||||
|
// slog.Error("Application panicked", slog.Any("error", r))
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
// configure logger
|
||||||
if *opts.Debug {
|
if *opts.Debug {
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(cmd.OutOrStdout(), &slog.HandlerOptions{Level: slog.LevelDebug})))
|
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")
|
slog.Debug("Starting server")
|
||||||
|
|
||||||
|
// load config
|
||||||
slog.Debug("Reading configuration", slog.String("path", *opts.ConfigPath))
|
slog.Debug("Reading configuration", slog.String("path", *opts.ConfigPath))
|
||||||
config, err := config.LoadConfig(*opts.ConfigPath)
|
cfg, err := config.LoadConfig(*opts.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", *opts.ConfigPath), slog.String("error", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
slog.Debug("Configuration loaded", slog.Any("config", config))
|
slog.Debug("Configuration loaded", slog.Any("config", cfg))
|
||||||
slog.Info("Server started", slog.Int("port", config.Server.Port), slog.String("address", config.Server.Addr))
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
server.SetHandler(handler)
|
||||||
|
server.Init()
|
||||||
|
|
||||||
|
var addr = net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port))
|
||||||
|
slog.Debug("Binding listener", slog.String("address", addr))
|
||||||
|
err = server.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))))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))
|
||||||
|
} else {
|
||||||
|
slog.Info("Server restarted with new configuration", slog.String("address", net.JoinHostPort(cfg.Server.Addr, fmt.Sprintf("%d", cfg.Server.Port))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
server:
|
server:
|
||||||
addr: "0.0.0.0"
|
address: "0.0.0.0"
|
||||||
|
port: 8089
|
||||||
5
go.mod
5
go.mod
@@ -11,6 +11,9 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
@@ -22,4 +25,6 @@ require (
|
|||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.6.0 // indirect
|
||||||
|
gorm.io/gorm v1.31.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -13,10 +13,16 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -54,3 +60,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"git.oblat.lv/alex/triggerssmith/internal/config"
|
||||||
|
"git.oblat.lv/alex/triggerssmith/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
// Application state and configurations can be added here
|
configuration atomic.Value // *config.Config
|
||||||
}
|
ls *server.LiveServer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp creates a new instance of the App struct. Always returns ErrNilPointerWarn.
|
||||||
|
func NewApp() (*App, error) {
|
||||||
|
app := &App{}
|
||||||
|
return app, ErrNilPointerWarn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) LoadConfiguration(cfg *config.Config) {
|
||||||
|
a.configuration.Store(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Configuration() *config.Config {
|
||||||
|
cfg := a.configuration.Load()
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cfg.(*config.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Server() *server.LiveServer {
|
||||||
|
if a.ls == nil {
|
||||||
|
a.ls, _ = server.Create("main_server")
|
||||||
|
}
|
||||||
|
return a.ls
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNilPointerWarn = errors.New("nil pointer dereference warning")
|
ErrNilPointerWarn = errors.New("nil pointer dereference warning")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,26 +1,43 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/akyaiy/GSfass/core/config"
|
"github.com/akyaiy/GSfass/core/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
Addr string `mapstructure:"address"`
|
Addr string `mapstructure:"address"`
|
||||||
|
StaticFilesPath string `mapstructure:"static_dir"`
|
||||||
|
LogPath string `mapstructure:"log_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configPath atomic.Value // string
|
||||||
|
var defaults = map[string]any{
|
||||||
|
"server.port": 8080,
|
||||||
|
"server.address": "127.0.0.0",
|
||||||
|
"server.static_dir": "./static",
|
||||||
|
}
|
||||||
|
|
||||||
|
func read(cfg *Config) error {
|
||||||
|
return config.Read().Config().FilePath(configPath.Load().(string)).SetBy(cfg).SetDefaults(defaults).End()
|
||||||
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
configPath.Store(path)
|
||||||
var cfg Config
|
var cfg Config
|
||||||
err := config.Read().Config().FilePath(path).SetBy(&cfg).SetDefaults(map[string]any{
|
err := read(&cfg)
|
||||||
"server.port": 8080,
|
|
||||||
"server.address": "127.0.0.0",
|
|
||||||
}).End()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReloadConfig(cfg *Config) error {
|
||||||
|
return read(cfg)
|
||||||
|
}
|
||||||
|
|||||||
16
internal/safe/recover.go
Normal file
16
internal/safe/recover.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package safe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SafeGO(fn func(), errs chan<- error) {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
errs <- fmt.Errorf("panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fn()
|
||||||
|
}()
|
||||||
|
}
|
||||||
465
internal/server/server.go
Normal file
465
internal/server/server.go
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
// package server
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "context"
|
||||||
|
// "fmt"
|
||||||
|
// "log/slog"
|
||||||
|
// "net"
|
||||||
|
// "net/http"
|
||||||
|
// "sync/atomic"
|
||||||
|
// "time"
|
||||||
|
|
||||||
|
// "git.oblat.lv/alex/triggerssmith/internal/config"
|
||||||
|
// "git.oblat.lv/alex/triggerssmith/internal/safe"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type LiveServer struct {
|
||||||
|
// current atomic.Value // *Server
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type Server struct {
|
||||||
|
// generalLogger *slog.Logger
|
||||||
|
// cfg *config.ServerConfig
|
||||||
|
|
||||||
|
// srv *http.Server
|
||||||
|
// ln net.Listener
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (s *Server) GetConfig() *config.ServerConfig {
|
||||||
|
// return s.cfg
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) Start(cfg *config.ServerConfig, handler http.Handler) error {
|
||||||
|
// slog.Debug("Starting new server", slog.Any("config", *cfg))
|
||||||
|
// addr := fmt.Sprintf("%s:%d", cfg.Addr, cfg.Port)
|
||||||
|
// ln, err := net.Listen("tcp", addr)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// srv := &http.Server{
|
||||||
|
// Handler: handler,
|
||||||
|
// }
|
||||||
|
// hs := &Server{
|
||||||
|
// cfg: cfg,
|
||||||
|
// ln: ln,
|
||||||
|
// srv: srv,
|
||||||
|
// }
|
||||||
|
// started := make(chan error, 1)
|
||||||
|
// go func() {
|
||||||
|
// err := srv.Serve(ln)
|
||||||
|
// started <- err
|
||||||
|
// }()
|
||||||
|
|
||||||
|
// select {
|
||||||
|
// case err := <-started:
|
||||||
|
// return fmt.Errorf("cannot start server: %w", err)
|
||||||
|
// case <-time.After(1 * time.Millisecond):
|
||||||
|
// }
|
||||||
|
|
||||||
|
// old := ls.current.Load()
|
||||||
|
// ls.current.Store(hs)
|
||||||
|
// if old != nil {
|
||||||
|
// errorChan := make(chan error, 1)
|
||||||
|
// safe.SafeGO(func() {
|
||||||
|
// ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
// defer cancel()
|
||||||
|
// old.(*Server).srv.Shutdown(ctx)
|
||||||
|
// }, errorChan)
|
||||||
|
// select {
|
||||||
|
// case err := <-errorChan:
|
||||||
|
// return err
|
||||||
|
// case <-time.After(4 * time.Second):
|
||||||
|
// return fmt.Errorf("timeout while shutting down old server")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LiveServer struct {
|
||||||
|
name string
|
||||||
|
data any // config, etc
|
||||||
|
|
||||||
|
handler http.Handler
|
||||||
|
|
||||||
|
active atomic.Value // *instance
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
statusMu sync.Mutex
|
||||||
|
status Status
|
||||||
|
|
||||||
|
initDone bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type instance struct {
|
||||||
|
srv *http.Server
|
||||||
|
ln net.Listener
|
||||||
|
|
||||||
|
addr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(name string) (*LiveServer, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("server name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
ls := &LiveServer{name: name}
|
||||||
|
ls.setStatus(Status{ID: StatusStopped})
|
||||||
|
return ls, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) setStatus(st Status) {
|
||||||
|
ls.statusMu.Lock()
|
||||||
|
defer ls.statusMu.Unlock()
|
||||||
|
|
||||||
|
ls.status = st
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) Status() Status {
|
||||||
|
ls.statusMu.Lock()
|
||||||
|
defer ls.statusMu.Unlock()
|
||||||
|
|
||||||
|
return ls.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) SetHandler(h http.Handler) {
|
||||||
|
ls.mu.Lock()
|
||||||
|
defer ls.mu.Unlock()
|
||||||
|
|
||||||
|
ls.handler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) listen(addr string) (net.Listener, error) {
|
||||||
|
slog.Debug("listening", slog.String("addr", addr))
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("listening failed", slog.String("err", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ln, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) serve(inst *instance) (chan error, error) {
|
||||||
|
slog.Debug("serving", slog.Any("instance", *inst))
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
if inst == nil {
|
||||||
|
err := fmt.Errorf("instance is nil")
|
||||||
|
slog.Debug("serving failed", slog.String("err", err.Error()))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
srv := inst.srv
|
||||||
|
ln := inst.ln
|
||||||
|
go func() {
|
||||||
|
err := srv.Serve(ln)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
return errChan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) Init() error {
|
||||||
|
slog.Debug("initializating live server", slog.Any("liveserver", *ls))
|
||||||
|
ls.mu.Lock()
|
||||||
|
defer ls.mu.Unlock()
|
||||||
|
ls.setStatus(Status{ID: StatusInitializing})
|
||||||
|
|
||||||
|
if ls.handler == nil {
|
||||||
|
err := fmt.Errorf("handler not set")
|
||||||
|
slog.Debug("initializating failed", slog.String("err", err.Error()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ls.initDone = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) Start(addr string) error {
|
||||||
|
slog.Debug("starting server", slog.String("addr", addr))
|
||||||
|
|
||||||
|
if !ls.initDone {
|
||||||
|
err := fmt.Errorf("server is not initialized")
|
||||||
|
slog.Debug("starting failed", slog.String("err", err.Error()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ls.setStatus(Status{ID: StatusStarting})
|
||||||
|
|
||||||
|
ln, err := ls.listen(addr)
|
||||||
|
if err != nil {
|
||||||
|
ls.setStatus(Status{ID: StatusError, Err: err})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srv := &http.Server{Handler: ls.handler}
|
||||||
|
|
||||||
|
ls.active.Store(&instance{
|
||||||
|
srv: srv,
|
||||||
|
ln: ln,
|
||||||
|
addr: addr,
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err = ls.serve(ls.active.Load().(*instance))
|
||||||
|
if err != nil {
|
||||||
|
ls.setStatus(Status{ID: StatusError, Err: err})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ls.setStatus(Status{ID: StatusOK})
|
||||||
|
|
||||||
|
// go func() {
|
||||||
|
// err := <-errChan
|
||||||
|
// if err != nil && err != http.ErrServerClosed {
|
||||||
|
// ls.setStatus(Status{ID: StatusError, Err: err})
|
||||||
|
// slog.Error("Server stopped with error", slog.String("name", ls.name), slog.String("error", err.Error()))
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
|
slog.Debug("Server started", slog.String("name", ls.name), slog.String("address", addr))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) stop(inst *instance) error {
|
||||||
|
slog.Debug("stopping server")
|
||||||
|
|
||||||
|
instAny := ls.active.Load()
|
||||||
|
if instAny == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.ln.Close()
|
||||||
|
err := inst.srv.Shutdown(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("shutdown", slog.String("err", err.Error()))
|
||||||
|
}
|
||||||
|
ls.setStatus(Status{ID: StatusStopped})
|
||||||
|
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LiveServer) Stop() error {
|
||||||
|
return ls.stop(ls.active.Load().(*instance))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (ls *LiveServer) Reload(newAddr string) error {
|
||||||
|
ls.mu.Lock()
|
||||||
|
oldInstAny := ls.active.Load()
|
||||||
|
var oldAddr string
|
||||||
|
if oldInstAny != nil {
|
||||||
|
oldAddr = oldInstAny.(*instance).addr
|
||||||
|
}
|
||||||
|
ls.mu.Unlock()
|
||||||
|
|
||||||
|
println("Old addr:", oldAddr, "New addr:", newAddr)
|
||||||
|
if oldAddr == newAddr {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slog.Debug("Reloading server", slog.String("name", ls.name), slog.String("new_address", newAddr))
|
||||||
|
|
||||||
|
ls.setStatus(Status{ID: StatusStarting})
|
||||||
|
|
||||||
|
slog.Debug("starting new server")
|
||||||
|
err := ls.Start(newAddr)
|
||||||
|
if err != nil {
|
||||||
|
ls.active.Store(oldInstAny)
|
||||||
|
return fmt.Errorf("cannot start new server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ls.stop(oldInstAny.(*instance)); err != nil {
|
||||||
|
slog.Debug("stopping failed", slog.String("err", err.Error()))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// package server
|
||||||
|
|
||||||
|
// import (
|
||||||
|
// "context"
|
||||||
|
// "errors"
|
||||||
|
// "fmt"
|
||||||
|
// "log/slog"
|
||||||
|
// "net"
|
||||||
|
// "net/http"
|
||||||
|
// "sync"
|
||||||
|
// "sync/atomic"
|
||||||
|
// "time"
|
||||||
|
|
||||||
|
// "git.oblat.lv/alex/triggerssmith/internal/config"
|
||||||
|
// )
|
||||||
|
|
||||||
|
// type LiveServer struct {
|
||||||
|
// name string
|
||||||
|
// handler http.Handler
|
||||||
|
// cfg *config.Config
|
||||||
|
|
||||||
|
// active atomic.Value // *instance
|
||||||
|
// mu sync.Mutex // защищает операции Start/Stop/Reload
|
||||||
|
|
||||||
|
// statusMu sync.Mutex
|
||||||
|
// status Status
|
||||||
|
// }
|
||||||
|
|
||||||
|
// type instance struct {
|
||||||
|
// srv *http.Server
|
||||||
|
// ln net.Listener
|
||||||
|
// cfg *config.ServerConfig
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func Create(name string) (*LiveServer, error) {
|
||||||
|
// if name == "" {
|
||||||
|
// return nil, errors.New("server name is empty")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ls := &LiveServer{name: name}
|
||||||
|
// ls.setStatus(Status{ID: StatusStopped})
|
||||||
|
// return ls, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) setStatus(st Status) {
|
||||||
|
// ls.statusMu.Lock()
|
||||||
|
// ls.status = st
|
||||||
|
// ls.statusMu.Unlock()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) Status() Status {
|
||||||
|
// ls.statusMu.Lock()
|
||||||
|
// s := ls.status
|
||||||
|
// ls.statusMu.Unlock()
|
||||||
|
// return s
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) LoadConfiguration(cfg *config.Config) {
|
||||||
|
// ls.mu.Lock()
|
||||||
|
// defer ls.mu.Unlock()
|
||||||
|
|
||||||
|
// ls.cfg = cfg
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) SetHandler(h http.Handler) {
|
||||||
|
// ls.mu.Lock()
|
||||||
|
// defer ls.mu.Unlock()
|
||||||
|
|
||||||
|
// ls.handler = h
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) Start() error {
|
||||||
|
// ls.mu.Lock()
|
||||||
|
// defer ls.mu.Unlock()
|
||||||
|
|
||||||
|
// if ls.cfg == nil {
|
||||||
|
// return errors.New("configuration not loaded")
|
||||||
|
// }
|
||||||
|
// if ls.handler == nil {
|
||||||
|
// return errors.New("handler not set")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ls.cfg.Server.Addr, ls.cfg.Server.Port))
|
||||||
|
// if err != nil {
|
||||||
|
// ls.setStatus(Status{ID: StatusError, Err: err})
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// srv := &http.Server{Handler: ls.handler}
|
||||||
|
|
||||||
|
// inst := &instance{
|
||||||
|
// srv: srv,
|
||||||
|
// ln: ln,
|
||||||
|
// cfg: &ls.cfg.Server,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ls.setStatus(Status{ID: StatusStarting})
|
||||||
|
// ls.active.Store(inst)
|
||||||
|
|
||||||
|
// go func() {
|
||||||
|
// err := srv.Serve(ln)
|
||||||
|
// if err != nil && err != http.ErrServerClosed {
|
||||||
|
// ls.setStatus(Status{ID: StatusError, Err: err})
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
|
// // даём серверу время забиндиться
|
||||||
|
// time.Sleep(5 * time.Millisecond)
|
||||||
|
|
||||||
|
// ls.setStatus(Status{ID: StatusOK})
|
||||||
|
// slog.Info("Server started", slog.String("name", ls.name), slog.String("address", fmt.Sprintf("%s:%d", ls.cfg.Server.Addr, ls.cfg.Server.Port)))
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) Stop() error {
|
||||||
|
// ls.mu.Lock()
|
||||||
|
// defer ls.mu.Unlock()
|
||||||
|
|
||||||
|
// instAny := ls.active.Load()
|
||||||
|
// if instAny == nil {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
// inst := instAny.(*instance)
|
||||||
|
|
||||||
|
// ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
// defer cancel()
|
||||||
|
|
||||||
|
// err := inst.srv.Shutdown(ctx)
|
||||||
|
// inst.ln.Close()
|
||||||
|
|
||||||
|
// ls.setStatus(Status{ID: StatusStopped})
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (ls *LiveServer) Reload(newCfg *config.Config) error {
|
||||||
|
// ls.mu.Lock()
|
||||||
|
// defer ls.mu.Unlock()
|
||||||
|
|
||||||
|
// oldInstAny := ls.active.Load()
|
||||||
|
// var oldCfg *config.ServerConfig
|
||||||
|
|
||||||
|
// if oldInstAny != nil {
|
||||||
|
// oldCfg = oldInstAny.(*instance).cfg
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if oldCfg != nil &&
|
||||||
|
// oldCfg.Addr == newCfg.Server.Addr &&
|
||||||
|
// oldCfg.Port == newCfg.Server.Port {
|
||||||
|
// ls.cfg = newCfg
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", newCfg.Server.Addr, newCfg.Server.Port))
|
||||||
|
// // if err != nil {
|
||||||
|
// // return fmt.Errorf("cannot bind new address: %w", err)
|
||||||
|
// // }
|
||||||
|
// // srv := &http.Server{Handler: ls.handler}
|
||||||
|
|
||||||
|
// // newInst := &instance{
|
||||||
|
// // srv: srv,
|
||||||
|
// // ln: ln,
|
||||||
|
// // cfg: &newCfg.Server,
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// ls.setStatus(Status{ID: StatusStarting})
|
||||||
|
|
||||||
|
|
||||||
|
// err := ls.Start()
|
||||||
|
// if err != nil {
|
||||||
|
// ls.active.Store(oldInstAny)
|
||||||
|
// return fmt.Errorf("cannot start new server: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := ls.Stop(); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
20
internal/server/status.go
Normal file
20
internal/server/status.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
type StatusID int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusStopped StatusID = iota
|
||||||
|
StatusStarting
|
||||||
|
StatusOK
|
||||||
|
StatusError
|
||||||
|
StatusInitializing
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
ID StatusID
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Status) Error() string {
|
||||||
|
return s.Err.Error()
|
||||||
|
}
|
||||||
2
main.go
2
main.go
@@ -6,4 +6,4 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
1
static/index.html
Normal file
1
static/index.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Hiwqe
|
||||||
Reference in New Issue
Block a user