Implement plugin/template reload on SIGUSR1
There's no way around having a global mutex, because we need to update the HTTP routes when reloading plugins. During reload we need to lock the whole server. Closes: https://todo.sr.ht/~sircmpwn/koushin/43
This commit is contained in:
parent
3d8569d185
commit
ad1d2ee7f4
3 changed files with 61 additions and 40 deletions
|
@ -48,7 +48,6 @@ func main() {
|
||||||
signal.Notify(sigs, syscall.SIGUSR1)
|
signal.Notify(sigs, syscall.SIGUSR1)
|
||||||
go func() {
|
go func() {
|
||||||
for range sigs {
|
for range sigs {
|
||||||
e.Logger.Printf("Reloading server")
|
|
||||||
if err := s.Reload(); err != nil {
|
if err := s.Reload(); err != nil {
|
||||||
e.Logger.Errorf("Failed to reload server: %v", err)
|
e.Logger.Errorf("Failed to reload server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
89
server.go
89
server.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
@ -14,21 +15,23 @@ const cookieName = "koushin_session"
|
||||||
|
|
||||||
// Server holds all the koushin server state.
|
// Server holds all the koushin server state.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
renderer *renderer
|
e *echo.Echo
|
||||||
Sessions *SessionManager
|
Sessions *SessionManager
|
||||||
Plugins []Plugin
|
|
||||||
|
mutex sync.RWMutex // used for server reload
|
||||||
|
plugins []Plugin
|
||||||
|
|
||||||
imap struct {
|
imap struct {
|
||||||
host string
|
host string
|
||||||
tls bool
|
tls bool
|
||||||
insecure bool
|
insecure bool
|
||||||
}
|
}
|
||||||
|
|
||||||
smtp struct {
|
smtp struct {
|
||||||
host string
|
host string
|
||||||
tls bool
|
tls bool
|
||||||
insecure bool
|
insecure bool
|
||||||
}
|
}
|
||||||
|
defaultTheme string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) parseIMAPURL(imapURL string) error {
|
func (s *Server) parseIMAPURL(imapURL string) error {
|
||||||
|
@ -73,19 +76,53 @@ func (s *Server) parseSMTPURL(smtpURL string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Reload() error {
|
func (s *Server) load() error {
|
||||||
return s.renderer.reload(s.Plugins)
|
plugins := append([]Plugin(nil), plugins...)
|
||||||
|
for _, p := range plugins {
|
||||||
|
s.e.Logger.Printf("Registered plugin '%v'", p.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(imapURL, smtpURL string) (*Server, error) {
|
luaPlugins, err := loadAllLuaPlugins(s.e.Logger)
|
||||||
s := &Server{}
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load plugins: %v", err)
|
||||||
|
}
|
||||||
|
plugins = append(plugins, luaPlugins...)
|
||||||
|
|
||||||
if err := s.parseIMAPURL(imapURL); err != nil {
|
renderer := newRenderer(s.e.Logger, s.defaultTheme)
|
||||||
|
if err := renderer.Load(plugins); err != nil {
|
||||||
|
return fmt.Errorf("failed to load templates: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we've loaded plugins and templates from disk (which can take time),
|
||||||
|
// swap them in the Server struct
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
s.plugins = plugins
|
||||||
|
s.e.Renderer = renderer
|
||||||
|
|
||||||
|
for _, p := range plugins {
|
||||||
|
p.SetRoutes(s.e.Group(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload loads Lua plugins and templates from disk.
|
||||||
|
func (s *Server) Reload() error {
|
||||||
|
s.e.Logger.Printf("Reloading server")
|
||||||
|
return s.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServer(e *echo.Echo, options *Options) (*Server, error) {
|
||||||
|
s := &Server{e: e, defaultTheme: options.Theme}
|
||||||
|
|
||||||
|
if err := s.parseIMAPURL(options.IMAPURL); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if smtpURL != "" {
|
if options.SMTPURL != "" {
|
||||||
if err := s.parseSMTPURL(smtpURL); err != nil {
|
if err := s.parseSMTPURL(options.SMTPURL); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,26 +176,13 @@ type Options struct {
|
||||||
|
|
||||||
// New creates a new server.
|
// New creates a new server.
|
||||||
func New(e *echo.Echo, options *Options) (*Server, error) {
|
func New(e *echo.Echo, options *Options) (*Server, error) {
|
||||||
s, err := newServer(options.IMAPURL, options.SMTPURL)
|
s, err := newServer(e, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Plugins = append([]Plugin(nil), plugins...)
|
if err := s.load(); err != nil {
|
||||||
for _, p := range s.Plugins {
|
return nil, err
|
||||||
e.Logger.Printf("Registered plugin '%v'", p.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
luaPlugins, err := loadAllLuaPlugins(e.Logger)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load plugins: %v", err)
|
|
||||||
}
|
|
||||||
s.Plugins = append(s.Plugins, luaPlugins...)
|
|
||||||
|
|
||||||
s.renderer = newRenderer(e.Logger, options.Theme)
|
|
||||||
e.Renderer = s.renderer
|
|
||||||
if err := s.renderer.reload(s.Plugins); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load templates: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||||
|
@ -172,6 +196,15 @@ func New(e *echo.Echo, options *Options) (*Server, error) {
|
||||||
c.String(code, err.Error())
|
c.String(code, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(ectx echo.Context) error {
|
||||||
|
s.mutex.RLock()
|
||||||
|
err := next(ectx)
|
||||||
|
s.mutex.RUnlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(ectx echo.Context) error {
|
return func(ectx echo.Context) error {
|
||||||
ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'")
|
ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'")
|
||||||
|
@ -211,9 +244,5 @@ func New(e *echo.Echo, options *Options) (*Server, error) {
|
||||||
|
|
||||||
e.Static("/themes", "themes")
|
e.Static("/themes", "themes")
|
||||||
|
|
||||||
for _, p := range s.Plugins {
|
|
||||||
p.SetRoutes(e.Group(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
11
template.go
11
template.go
|
@ -6,7 +6,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
@ -81,19 +80,15 @@ type renderer struct {
|
||||||
logger echo.Logger
|
logger echo.Logger
|
||||||
defaultTheme string
|
defaultTheme string
|
||||||
|
|
||||||
mutex sync.RWMutex
|
|
||||||
base *template.Template
|
base *template.Template
|
||||||
themes map[string]*template.Template
|
themes map[string]*template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) Render(w io.Writer, name string, data interface{}, ectx echo.Context) error {
|
func (r *renderer) Render(w io.Writer, name string, data interface{}, ectx echo.Context) error {
|
||||||
r.mutex.RLock()
|
|
||||||
defer r.mutex.RUnlock()
|
|
||||||
|
|
||||||
// ectx is the raw *echo.context, not our own *Context
|
// ectx is the raw *echo.context, not our own *Context
|
||||||
ctx := ectx.Get("context").(*Context)
|
ctx := ectx.Get("context").(*Context)
|
||||||
|
|
||||||
for _, plugin := range ctx.Server.Plugins {
|
for _, plugin := range ctx.Server.plugins {
|
||||||
if err := plugin.Inject(ctx, name, data.(RenderData)); err != nil {
|
if err := plugin.Inject(ctx, name, data.(RenderData)); err != nil {
|
||||||
return fmt.Errorf("failed to run plugin '%v': %v", plugin.Name(), err)
|
return fmt.Errorf("failed to run plugin '%v': %v", plugin.Name(), err)
|
||||||
}
|
}
|
||||||
|
@ -121,7 +116,7 @@ func loadTheme(name string, base *template.Template) (*template.Template, error)
|
||||||
return theme, nil
|
return theme, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) reload(plugins []Plugin) error {
|
func (r *renderer) Load(plugins []Plugin) error {
|
||||||
base := template.New("")
|
base := template.New("")
|
||||||
|
|
||||||
for _, p := range plugins {
|
for _, p := range plugins {
|
||||||
|
@ -155,10 +150,8 @@ func (r *renderer) reload(plugins []Plugin) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.mutex.Lock()
|
|
||||||
r.base = base
|
r.base = base
|
||||||
r.themes = themes
|
r.themes = themes
|
||||||
r.mutex.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue