From b58c15d121095ef58f8fb1ac5d3a456e89baee2a Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Mon, 20 Jan 2020 22:04:50 +0100 Subject: [PATCH] Extract Lua infrastructure into a plugin --- .gitignore | 1 + cmd/koushin/main.go | 1 + plugin.go | 3 +- plugin_go.go | 4 +- plugin_lua.go | 180 ------------------------------------------------ plugins/lua/lua.go | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++ plugins/lua/plugin.go | 9 +++ server.go | 18 ++--- 8 files changed, 204 insertions(+), 196 deletions(-) delete mode 100644 plugin_lua.go create mode 100644 plugins/lua/lua.go create mode 100644 plugins/lua/plugin.go diff --git a/.gitignore b/.gitignore index b7d2b50..758f38e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ !/public/themes/sourcehut /plugins/* !/plugins/base +!/plugins/lua diff --git a/cmd/koushin/main.go b/cmd/koushin/main.go index d22b404..4897dc9 100644 --- a/cmd/koushin/main.go +++ b/cmd/koushin/main.go @@ -13,6 +13,7 @@ import ( "github.com/labstack/gommon/log" _ "git.sr.ht/~emersion/koushin/plugins/base" + _ "git.sr.ht/~emersion/koushin/plugins/lua" ) func main() { diff --git a/plugin.go b/plugin.go index 4e46aaf..c14f700 100644 --- a/plugin.go +++ b/plugin.go @@ -6,7 +6,8 @@ import ( "github.com/labstack/echo/v4" ) -const pluginDir = "plugins" +// PluginDir is the path to the plugins directory. +const PluginDir = "plugins" // Plugin extends koushin with additional functionality. type Plugin interface { diff --git a/plugin_go.go b/plugin_go.go index 9a6bb64..24c4df6 100644 --- a/plugin_go.go +++ b/plugin_go.go @@ -19,7 +19,7 @@ func (p *goPlugin) Name() string { func (p *goPlugin) LoadTemplate(t *template.Template) error { t.Funcs(p.p.templateFuncs) - paths, err := filepath.Glob(pluginDir + "/" + p.p.Name + "/public/*.html") + paths, err := filepath.Glob(PluginDir + "/" + p.p.Name + "/public/*.html") if err != nil { return err } @@ -40,7 +40,7 @@ func (p *goPlugin) SetRoutes(group *echo.Group) { }) } - group.Static("/plugins/"+p.p.Name+"/assets", pluginDir+"/"+p.p.Name+"/public/assets") + group.Static("/plugins/"+p.p.Name+"/assets", PluginDir+"/"+p.p.Name+"/public/assets") } func (p *goPlugin) Inject(ctx *Context, name string, data RenderData) error { diff --git a/plugin_lua.go b/plugin_lua.go deleted file mode 100644 index 8291a20..0000000 --- a/plugin_lua.go +++ /dev/null @@ -1,180 +0,0 @@ -package koushin - -import ( - "fmt" - "html/template" - "path/filepath" - - "github.com/labstack/echo/v4" - "github.com/yuin/gopher-lua" - "layeh.com/gopher-luar" -) - -type luaRoute struct { - method string - path string - f *lua.LFunction -} - -type luaPlugin struct { - filename string - state *lua.LState - renderCallbacks map[string]*lua.LFunction - filters template.FuncMap - routes []luaRoute -} - -func (p *luaPlugin) Name() string { - return p.filename -} - -func (p *luaPlugin) onRender(l *lua.LState) int { - name := l.CheckString(1) - f := l.CheckFunction(2) - p.renderCallbacks[name] = f - return 0 -} - -func (p *luaPlugin) setFilter(l *lua.LState) int { - name := l.CheckString(1) - f := l.CheckFunction(2) - p.filters[name] = func(args ...interface{}) string { - luaArgs := make([]lua.LValue, len(args)) - for i, v := range args { - luaArgs[i] = luar.New(l, v) - } - - err := l.CallByParam(lua.P{ - Fn: f, - NRet: 1, - Protect: true, - }, luaArgs...) - if err != nil { - panic(err) // TODO: better error handling? - } - - ret := l.CheckString(-1) - l.Pop(1) - return ret - } - return 0 -} - -func (p *luaPlugin) setRoute(l *lua.LState) int { - method := l.CheckString(1) - path := l.CheckString(2) - f := l.CheckFunction(3) - p.routes = append(p.routes, luaRoute{method, path, f}) - return 0 -} - -func (p *luaPlugin) inject(name string, data RenderData) error { - f, ok := p.renderCallbacks[name] - if !ok { - return nil - } - - err := p.state.CallByParam(lua.P{ - Fn: f, - NRet: 0, - Protect: true, - }, luar.New(p.state, data)) - if err != nil { - return err - } - - return nil -} - -func (p *luaPlugin) Inject(ctx *Context, name string, data RenderData) error { - if err := p.inject("*", data); err != nil { - return err - } - return p.inject(name, data) -} - -func (p *luaPlugin) LoadTemplate(t *template.Template) error { - t.Funcs(p.filters) - - paths, err := filepath.Glob(filepath.Dir(p.filename) + "/public/*.html") - if err != nil { - return err - } - if len(paths) > 0 { - if _, err := t.ParseFiles(paths...); err != nil { - return err - } - } - - return nil -} - -func (p *luaPlugin) SetRoutes(group *echo.Group) { - for _, r := range p.routes { - group.Match([]string{r.method}, r.path, func(ctx echo.Context) error { - err := p.state.CallByParam(lua.P{ - Fn: r.f, - NRet: 0, - Protect: true, - }, luar.New(p.state, ctx)) - if err != nil { - return fmt.Errorf("Lua plugin error: %v", err) - } - - return nil - }) - } - - _, name := filepath.Split(filepath.Dir(p.filename)) - group.Static("/plugins/"+name+"/assets", filepath.Dir(p.filename)+"/public/assets") -} - -func (p *luaPlugin) Close() error { - p.state.Close() - return nil -} - -func loadLuaPlugin(filename string) (*luaPlugin, error) { - l := lua.NewState() - p := &luaPlugin{ - filename: filename, - state: l, - renderCallbacks: make(map[string]*lua.LFunction), - filters: make(template.FuncMap), - } - - mt := l.NewTypeMetatable("koushin") - l.SetGlobal("koushin", mt) - l.SetField(mt, "on_render", l.NewFunction(p.onRender)) - l.SetField(mt, "set_filter", l.NewFunction(p.setFilter)) - l.SetField(mt, "set_route", l.NewFunction(p.setRoute)) - - if err := l.DoFile(filename); err != nil { - l.Close() - return nil, err - } - - return p, nil -} - -func loadAllLuaPlugins(log echo.Logger) ([]Plugin, error) { - filenames, err := filepath.Glob(pluginDir + "/*/main.lua") - if err != nil { - return nil, fmt.Errorf("filepath.Glob failed: %v", err) - } - - plugins := make([]Plugin, 0, len(filenames)) - for _, filename := range filenames { - log.Printf("Loading Lua plugin '%v'", filename) - p, err := loadLuaPlugin(filename) - if err != nil { - for _, p := range plugins { - p.Close() - } - return nil, fmt.Errorf("failed to load Lua plugin '%v': %v", filename, err) - } - plugins = append(plugins, p) - } - - return plugins, nil -} diff --git a/plugins/lua/lua.go b/plugins/lua/lua.go new file mode 100644 index 0000000..bbc5c39 --- /dev/null +++ b/plugins/lua/lua.go @@ -0,0 +1,184 @@ +package koushinlua + +import ( + "fmt" + "html/template" + "path/filepath" + + "github.com/labstack/echo/v4" + "github.com/yuin/gopher-lua" + "layeh.com/gopher-luar" + "git.sr.ht/~emersion/koushin" +) + +type luaRoute struct { + method string + path string + f *lua.LFunction +} + +type luaPlugin struct { + filename string + state *lua.LState + renderCallbacks map[string]*lua.LFunction + filters template.FuncMap + routes []luaRoute +} + +func (p *luaPlugin) Name() string { + return p.filename +} + +func (p *luaPlugin) onRender(l *lua.LState) int { + name := l.CheckString(1) + f := l.CheckFunction(2) + p.renderCallbacks[name] = f + return 0 +} + +func (p *luaPlugin) setFilter(l *lua.LState) int { + name := l.CheckString(1) + f := l.CheckFunction(2) + p.filters[name] = func(args ...interface{}) string { + luaArgs := make([]lua.LValue, len(args)) + for i, v := range args { + luaArgs[i] = luar.New(l, v) + } + + err := l.CallByParam(lua.P{ + Fn: f, + NRet: 1, + Protect: true, + }, luaArgs...) + if err != nil { + panic(err) // TODO: better error handling? + } + + ret := l.CheckString(-1) + l.Pop(1) + return ret + } + return 0 +} + +func (p *luaPlugin) setRoute(l *lua.LState) int { + method := l.CheckString(1) + path := l.CheckString(2) + f := l.CheckFunction(3) + p.routes = append(p.routes, luaRoute{method, path, f}) + return 0 +} + +func (p *luaPlugin) inject(name string, data koushin.RenderData) error { + f, ok := p.renderCallbacks[name] + if !ok { + return nil + } + + err := p.state.CallByParam(lua.P{ + Fn: f, + NRet: 0, + Protect: true, + }, luar.New(p.state, data)) + if err != nil { + return err + } + + return nil +} + +func (p *luaPlugin) Inject(ctx *koushin.Context, name string, data koushin.RenderData) error { + if err := p.inject("*", data); err != nil { + return err + } + return p.inject(name, data) +} + +func (p *luaPlugin) LoadTemplate(t *template.Template) error { + t.Funcs(p.filters) + + paths, err := filepath.Glob(filepath.Dir(p.filename) + "/public/*.html") + if err != nil { + return err + } + if len(paths) > 0 { + if _, err := t.ParseFiles(paths...); err != nil { + return err + } + } + + return nil +} + +func (p *luaPlugin) SetRoutes(group *echo.Group) { + for _, r := range p.routes { + group.Match([]string{r.method}, r.path, func(ctx echo.Context) error { + err := p.state.CallByParam(lua.P{ + Fn: r.f, + NRet: 0, + Protect: true, + }, luar.New(p.state, ctx)) + if err != nil { + return fmt.Errorf("Lua plugin error: %v", err) + } + + return nil + }) + } + + _, name := filepath.Split(filepath.Dir(p.filename)) + group.Static("/plugins/"+name+"/assets", filepath.Dir(p.filename)+"/public/assets") +} + +func (p *luaPlugin) Close() error { + p.state.Close() + return nil +} + +func loadLuaPlugin(filename string) (*luaPlugin, error) { + l := lua.NewState() + p := &luaPlugin{ + filename: filename, + state: l, + renderCallbacks: make(map[string]*lua.LFunction), + filters: make(template.FuncMap), + } + + mt := l.NewTypeMetatable("koushin") + l.SetGlobal("koushin", mt) + l.SetField(mt, "on_render", l.NewFunction(p.onRender)) + l.SetField(mt, "set_filter", l.NewFunction(p.setFilter)) + l.SetField(mt, "set_route", l.NewFunction(p.setRoute)) + + if err := l.DoFile(filename); err != nil { + l.Close() + return nil, err + } + + return p, nil +} + +func loadAllLuaPlugins(s *koushin.Server) ([]koushin.Plugin, error) { + log := s.Logger() + + filenames, err := filepath.Glob(koushin.PluginDir + "/*/main.lua") + if err != nil { + return nil, fmt.Errorf("filepath.Glob failed: %v", err) + } + + plugins := make([]koushin.Plugin, 0, len(filenames)) + for _, filename := range filenames { + log.Printf("Loading Lua plugin %q", filename) + + p, err := loadLuaPlugin(filename) + if err != nil { + for _, p := range plugins { + p.Close() + } + return nil, fmt.Errorf("failed to load Lua plugin %q: %v", filename, err) + } + plugins = append(plugins, p) + } + + return plugins, nil +} diff --git a/plugins/lua/plugin.go b/plugins/lua/plugin.go new file mode 100644 index 0000000..dbfee6d --- /dev/null +++ b/plugins/lua/plugin.go @@ -0,0 +1,9 @@ +package koushinlua + +import ( + "git.sr.ht/~emersion/koushin" +) + +func init() { + koushin.RegisterPluginLoader(loadAllLuaPlugins) +} diff --git a/server.go b/server.go index 83a636e..a8c2123 100644 --- a/server.go +++ b/server.go @@ -18,9 +18,8 @@ type Server struct { e *echo.Echo Sessions *SessionManager - mutex sync.RWMutex // used for server reload - plugins []Plugin - luaPlugins []Plugin + mutex sync.RWMutex // used for server reload + plugins []Plugin // maps protocols to URLs (protocol can be empty for auto-discovery) upstreams map[string]*url.URL @@ -188,12 +187,6 @@ func (s *Server) load() error { plugins = append(plugins, l...) } - luaPlugins, err := loadAllLuaPlugins(s.e.Logger) - if err != nil { - return fmt.Errorf("failed to load plugins: %v", err) - } - plugins = append(plugins, luaPlugins...) - renderer := newRenderer(s.e.Logger, s.defaultTheme) if err := renderer.Load(plugins); err != nil { return fmt.Errorf("failed to load templates: %v", err) @@ -204,15 +197,14 @@ func (s *Server) load() error { s.mutex.Lock() defer s.mutex.Unlock() - // Close previous Lua plugins - for _, p := range s.luaPlugins { + // Close previous plugins + for _, p := range s.plugins { if err := p.Close(); err != nil { - s.e.Logger.Printf("Failed to unload plugin '%v': %v", p.Name(), err) + s.e.Logger.Printf("Failed to unload plugin %q: %v", p.Name(), err) } } s.plugins = plugins - s.luaPlugins = luaPlugins s.e.Renderer = renderer for _, p := range plugins {