server with basic hot reload

This commit is contained in:
2025-11-30 10:09:53 +02:00
parent f1ed3c977a
commit 44e92bcfef
14 changed files with 700 additions and 18 deletions

View File

@@ -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)"

View File

@@ -14,4 +14,4 @@ var rootCmd = &cobra.Command{
func Execute() error { func Execute() error {
return rootCmd.Execute() return rootCmd.Execute()
} }

View File

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

View File

@@ -1,2 +1,3 @@
server: server:
addr: "0.0.0.0" address: "0.0.0.0"
port: 8089

5
go.mod
View File

@@ -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
View File

@@ -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=

View File

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

View File

@@ -6,4 +6,4 @@ import (
var ( var (
ErrNilPointerWarn = errors.New("nil pointer dereference warning") ErrNilPointerWarn = errors.New("nil pointer dereference warning")
) )

View File

@@ -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
View 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
View 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
View 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()
}

View File

@@ -6,4 +6,4 @@ import (
func main() { func main() {
cmd.Execute() cmd.Execute()
} }

1
static/index.html Normal file
View File

@@ -0,0 +1 @@
Hiwqe