...

Source file src/codeberg.org/tslocum/sriracha/internal/server/plugin.go

Documentation: codeberg.org/tslocum/sriracha/internal/server

     1  package server
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"html/template"
     7  	"io/fs"
     8  	"log"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"plugin"
    13  	"reflect"
    14  	"strings"
    15  
    16  	"codeberg.org/tslocum/sriracha"
    17  	. "codeberg.org/tslocum/sriracha/model"
    18  )
    19  
    20  type attachHandler func(db sriracha.DB, post *Post, file []byte) (handled bool, err error)
    21  
    22  type attachHandlerInfo struct {
    23  	Name    string
    24  	Handler attachHandler
    25  }
    26  
    27  type postHandler func(db sriracha.DB, post *Post) error
    28  
    29  type postHandlerInfo struct {
    30  	Name    string
    31  	Handler postHandler
    32  }
    33  
    34  type insertHandler func(db sriracha.DB, post *Post) error
    35  
    36  type insertHandlerInfo struct {
    37  	Name    string
    38  	Handler insertHandler
    39  }
    40  
    41  type createHandler func(db sriracha.DB, post *Post) error
    42  
    43  type createHandlerInfo struct {
    44  	Name    string
    45  	Handler createHandler
    46  }
    47  
    48  type reportHandler func(db sriracha.DB, post *Post) error
    49  
    50  type reportHandlerInfo struct {
    51  	Name    string
    52  	Handler reportHandler
    53  }
    54  
    55  type auditHandler func(db sriracha.DB, user string, action string, info string) error
    56  
    57  type auditHandlerInfo struct {
    58  	Name    string
    59  	Handler auditHandler
    60  }
    61  
    62  type serveHandler func(db sriracha.DB, a *Account, w http.ResponseWriter, r *http.Request) (template.HTML, error)
    63  
    64  type serveHandlerInfo struct {
    65  	Name    string
    66  	Handler serveHandler
    67  }
    68  
    69  type pluginInfo struct {
    70  	ID       int
    71  	Name     string
    72  	FullName string
    73  	About    string
    74  	Help     template.HTML
    75  	Config   []sriracha.PluginConfig
    76  	Events   []string
    77  	Serve    serveHandler
    78  }
    79  
    80  var (
    81  	allPlugins              []any
    82  	allPluginInfo           []*pluginInfo
    83  	allPluginAttachHandlers []attachHandlerInfo
    84  	allPluginPostHandlers   []postHandlerInfo
    85  	allPluginInsertHandlers []insertHandlerInfo
    86  	allPluginCreateHandlers []createHandlerInfo
    87  	allPluginReportHandlers []reportHandlerInfo
    88  	allPluginAuditHandlers  []auditHandlerInfo
    89  	allPluginServeHandlers  []serveHandlerInfo
    90  )
    91  
    92  // registerPlugin registers a Sriracha plugin to start receiving events.
    93  func (s *Server) registerPlugin(plugin any) {
    94  	info := &pluginInfo{
    95  		ID: len(allPlugins) + 1,
    96  	}
    97  
    98  	v := reflect.ValueOf(plugin)
    99  	if v.Kind() == reflect.Interface || v.Kind() == reflect.Pointer {
   100  		v = v.Elem()
   101  	}
   102  	info.FullName = v.Type().Name()
   103  	info.Name = strings.ToLower(info.FullName)
   104  
   105  	if pAbout, ok := plugin.(sriracha.Plugin); ok {
   106  		info.About = pAbout.About()
   107  	} else {
   108  		log.Fatalf("%s does not implement required methods", info.Name)
   109  	}
   110  
   111  	if pHelp, ok := plugin.(sriracha.PluginWithHelp); ok {
   112  		info.Help = pHelp.Help()
   113  	}
   114  
   115  	if pConfig, ok := plugin.(sriracha.PluginWithConfig); ok {
   116  		config := pConfig.Config()
   117  		for i := range config {
   118  			err := config[i].Validate()
   119  			if err != nil {
   120  				optionName := config[i].Name
   121  				if strings.TrimSpace(optionName) == "" {
   122  					optionName = fmt.Sprintf("#%d", i)
   123  				} else {
   124  					optionName = fmt.Sprintf(`"%s"`, optionName)
   125  				}
   126  				log.Fatalf("%s configuration option %s is invalid: %s", info.Name, optionName, err)
   127  			} else if config[i].Type == sriracha.TypeBoolean && config[i].Default == "" {
   128  				config[i].Default = "0"
   129  			}
   130  
   131  			if config[i].Type == sriracha.TypeEnum {
   132  				config[i].Value = ""
   133  			} else {
   134  				config[i].Value = config[i].Default
   135  			}
   136  		}
   137  		info.Config = config
   138  	}
   139  
   140  	if _, ok := plugin.(sriracha.PluginWithUpdate); ok {
   141  		info.Events = append(info.Events, "Update")
   142  	}
   143  
   144  	if pAttach, ok := plugin.(sriracha.PluginWithAttach); ok {
   145  		info.Events = append(info.Events, "Attach")
   146  		allPluginAttachHandlers = append(allPluginAttachHandlers, attachHandlerInfo{strings.ToLower(info.Name), pAttach.Attach})
   147  	}
   148  
   149  	if pPost, ok := plugin.(sriracha.PluginWithPost); ok {
   150  		info.Events = append(info.Events, "Post")
   151  		allPluginPostHandlers = append(allPluginPostHandlers, postHandlerInfo{strings.ToLower(info.Name), pPost.Post})
   152  	}
   153  
   154  	if pInsert, ok := plugin.(sriracha.PluginWithInsert); ok {
   155  		info.Events = append(info.Events, "Insert")
   156  		allPluginInsertHandlers = append(allPluginInsertHandlers, insertHandlerInfo{strings.ToLower(info.Name), pInsert.Insert})
   157  	}
   158  
   159  	if pCreate, ok := plugin.(sriracha.PluginWithCreate); ok {
   160  		info.Events = append(info.Events, "Create")
   161  		allPluginCreateHandlers = append(allPluginCreateHandlers, createHandlerInfo{strings.ToLower(info.Name), pCreate.Create})
   162  	}
   163  
   164  	if pReport, ok := plugin.(sriracha.PluginWithReport); ok {
   165  		info.Events = append(info.Events, "Report")
   166  		allPluginReportHandlers = append(allPluginReportHandlers, reportHandlerInfo{strings.ToLower(info.Name), pReport.Report})
   167  	}
   168  
   169  	if pAudit, ok := plugin.(sriracha.PluginWithAudit); ok {
   170  		info.Events = append(info.Events, "Audit")
   171  		allPluginAuditHandlers = append(allPluginAuditHandlers, auditHandlerInfo{strings.ToLower(info.Name), pAudit.Audit})
   172  	}
   173  
   174  	if pServe, ok := plugin.(sriracha.PluginWithServe); ok {
   175  		info.Events = append(info.Events, "Serve")
   176  		info.Serve = pServe.Serve
   177  		allPluginServeHandlers = append(allPluginServeHandlers, serveHandlerInfo{strings.ToLower(info.Name), pServe.Serve})
   178  	}
   179  
   180  	if len(info.Events) == 0 {
   181  		info.Events = append(info.Events, "None")
   182  	}
   183  
   184  	allPlugins = append(allPlugins, plugin)
   185  	allPluginInfo = append(allPluginInfo, info)
   186  }
   187  
   188  func (s *Server) loadPlugin(pluginPath string) error {
   189  	wrapErr := func(err error) error {
   190  		return fmt.Errorf("failed to load plugin %s: %s", pluginPath, err)
   191  	}
   192  
   193  	info, err := os.Stat(pluginPath)
   194  	if err != nil {
   195  		return wrapErr(err)
   196  	} else if info.IsDir() {
   197  		return filepath.WalkDir(pluginPath, func(path string, d fs.DirEntry, err error) error {
   198  			if err != nil {
   199  				return err
   200  			} else if d.IsDir() || path == pluginPath {
   201  				return nil
   202  			}
   203  			return s.loadPlugin(path)
   204  		})
   205  	} else if !strings.HasSuffix(pluginPath, ".so") {
   206  		return nil
   207  	}
   208  
   209  	const pluginExample = "plugins must declare a function named \"Plugin\" which returns a new instance:\n  func Plugin() any {\n    return &MyPlugin{}\n  }"
   210  	plugin, err := plugin.Open(pluginPath)
   211  	if err != nil {
   212  		return wrapErr(err)
   213  	}
   214  	pluginSymbol, err := plugin.Lookup("Plugin")
   215  	if err != nil {
   216  		return wrapErr(fmt.Errorf("expected function \"Plugin\" was not found: " + pluginExample))
   217  	}
   218  	pluginFunc, ok := pluginSymbol.(func() any)
   219  	if !ok {
   220  		return wrapErr(fmt.Errorf("symbol \"Plugin\" was found but does not match the expected function signature: " + pluginExample))
   221  	}
   222  	s.registerPlugin(pluginFunc())
   223  	return nil
   224  }
   225  
   226  func (s *Server) loadPlugins() error {
   227  	for _, pluginPath := range flag.Args() {
   228  		err := s.loadPlugin(pluginPath)
   229  		if err != nil {
   230  			return err
   231  		}
   232  	}
   233  	if len(allPluginInfo) != 0 {
   234  		var plural string
   235  		if len(allPluginInfo) != 1 {
   236  			plural = "s"
   237  		}
   238  		var names []string
   239  		for _, info := range allPluginInfo {
   240  			names = append(names, info.FullName)
   241  		}
   242  		fmt.Printf("Loaded %d plugin%s (%s)\n", len(allPluginInfo), plural, strings.Join(names, ", "))
   243  	}
   244  	return nil
   245  }
   246  

View as plain text