From 1e5c432e1029601a664454388ae366ef69618d62 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 25 Jun 2018 12:33:13 -0700 Subject: MM-10702 Moving plugins to use hashicorp go-plugin. (#8978) * Moving plugins to use hashicorp go-plugin. * Tweaks from feedback. --- plugin/interface_generator/main.go | 377 +++++++++++++++++++++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 plugin/interface_generator/main.go (limited to 'plugin/interface_generator') diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go new file mode 100644 index 000000000..5f66506d3 --- /dev/null +++ b/plugin/interface_generator/main.go @@ -0,0 +1,377 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/alecthomas/template" + "github.com/pkg/errors" +) + +type IHookEntry struct { + FuncName string + Args *ast.FieldList + Results *ast.FieldList +} + +type PluginInterfaceInfo struct { + Hooks []IHookEntry + API []IHookEntry + FileSet *token.FileSet +} + +func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "()" + } + for _, field := range fieldList.List { + typeNameBuffer := &bytes.Buffer{} + err := printer.Fprint(typeNameBuffer, fileset, field.Type) + if err != nil { + panic(err) + } + typeName := typeNameBuffer.String() + names := []string{} + for _, name := range field.Names { + names = append(names, name.Name) + } + result = append(result, strings.Join(names, ", ")+" "+typeName) + } + + return "(" + strings.Join(result, ", ") + ")" +} + +func FieldListToNames(fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "" + } + for _, field := range fieldList.List { + for _, name := range field.Names { + result = append(result, name.Name) + } + } + + return strings.Join(result, ", ") +} + +func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "" + } + nextLetter := 'A' + for _, field := range fieldList.List { + if len(field.Names) == 0 { + result = append(result, structPrefix+string(nextLetter)) + nextLetter += 1 + } else { + for range field.Names { + result = append(result, structPrefix+string(nextLetter)) + nextLetter += 1 + } + } + } + + return strings.Join(result, ", ") +} + +func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "" + } + nextLetter := 'A' + for _, field := range fieldList.List { + typeNameBuffer := &bytes.Buffer{} + err := printer.Fprint(typeNameBuffer, fileset, field.Type) + if err != nil { + panic(err) + } + typeName := typeNameBuffer.String() + if len(field.Names) == 0 { + result = append(result, string(nextLetter)+" "+typeName) + nextLetter += 1 + } else { + for range field.Names { + result = append(result, string(nextLetter)+" "+typeName) + nextLetter += 1 + } + } + } + + return strings.Join(result, "\n\t") +} + +func goList(dir string) ([]string, error) { + cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir) + bytes, err := cmd.Output() + if err != nil { + return nil, errors.Wrap(err, "Can't list packages") + } + + return strings.Fields(string(bytes)), nil +} + +func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) { + info.Hooks = append(info.Hooks, IHookEntry{ + FuncName: method.Names[0].Name, + Args: method.Type.(*ast.FuncType).Params, + Results: method.Type.(*ast.FuncType).Results, + }) +} + +func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) { + info.API = append(info.API, IHookEntry{ + FuncName: method.Names[0].Name, + Args: method.Type.(*ast.FuncType).Params, + Results: method.Type.(*ast.FuncType).Results, + }) +} + +func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool { + return func(node ast.Node) bool { + if typeSpec, ok := node.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == "Hooks" { + for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List { + info.addHookMethod(method) + } + return false + } else if typeSpec.Name.Name == "API" { + for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List { + info.addAPIMethod(method) + } + return false + } + } + return true + } +} + +func getPluginInfo(dir string) (*PluginInterfaceInfo, error) { + pluginInfo := &PluginInterfaceInfo{ + Hooks: make([]IHookEntry, 0), + FileSet: token.NewFileSet(), + } + + packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments) + if err != nil { + log.Println("Parser error in dir "+dir+": ", err) + } + + for _, pkg := range packages { + if pkg.Name != "plugin" { + continue + } + + for _, file := range pkg.Files { + ast.Inspect(file, pluginInfo.makeHookInspector()) + } + } + + return pluginInfo, nil +} + +var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// Code generated by "make pluginapi" +// DO NOT EDIT + +package plugin + +{{range .HooksMethods}} + +func init() { + HookNameToId["{{.Name}}"] = {{.Name}}Id +} + +type {{.Name}}Args struct { + {{structStyle .Params}} +} + +type {{.Name}}Returns struct { + {{structStyle .Return}} +} + +func (g *HooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { + _args := &{{.Name}}Args{ {{valuesOnly .Params}} } + _returns := &{{.Name}}Returns{} + if g.implemented[{{.Name}}Id] { + if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { + g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err)) + } + } + return {{destruct "_returns." .Return}} +} + +func (s *HooksRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error { + if hook, ok := s.impl.(interface { + {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} + }); ok { + {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) + } + return nil +} +{{end}} + +{{range .APIMethods}} + +type {{.Name}}Args struct { + {{structStyle .Params}} +} + +type {{.Name}}Returns struct { + {{structStyle .Return}} +} + +func (g *APIRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { + _args := &{{.Name}}Args{ {{valuesOnly .Params}} } + _returns := &{{.Name}}Returns{} + if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { + g.log.Error("RPC call to {{.Name}} API failed.", mlog.Err(err)) + } + return {{destruct "_returns." .Return}} +} + +func (s *APIRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error { + if hook, ok := s.impl.(interface { + {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} + }); ok { + {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) + } + return nil +} +{{end}} +` + +type MethodParams struct { + Name string + Params *ast.FieldList + Return *ast.FieldList +} + +type HooksTemplateParams struct { + HooksMethods []MethodParams + APIMethods []MethodParams +} + +func generateGlue(info *PluginInterfaceInfo) { + templateFunctions := map[string]interface{}{ + "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) }, + "structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) }, + "valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet) }, + "destruct": func(structPrefix string, fields *ast.FieldList) string { + return FieldListDestruct(structPrefix, fields, info.FileSet) + }, + } + + hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate) + if err != nil { + panic(err) + } + + templateParams := HooksTemplateParams{} + for _, hook := range info.Hooks { + templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{ + Name: hook.FuncName, + Params: hook.Args, + Return: hook.Results, + }) + } + for _, api := range info.API { + templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{ + Name: api.FuncName, + Params: api.Args, + Return: api.Results, + }) + } + templateResult := &bytes.Buffer{} + hooksTemplate.Execute(templateResult, &templateParams) + + importsBuffer := &bytes.Buffer{} + cmd := exec.Command("goimports") + cmd.Stdin = templateResult + cmd.Stdout = importsBuffer + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic(err) + } + + if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), importsBuffer.Bytes(), 0664); err != nil { + panic(err) + } +} + +func getPluginPackageDir() string { + dirs, err := goList("github.com/mattermost/mattermost-server/plugin") + if err != nil { + panic(err) + } else if len(dirs) != 1 { + panic("More than one package dir, or no dirs!") + } + + return dirs[0] +} + +func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { + toBeExcluded := func(item string) bool { + excluded := []string{ + "OnActivate", + "Implemented", + "LoadPluginConfiguration", + "ServeHTTP", + } + for _, exclusion := range excluded { + if exclusion == item { + return true + } + } + return false + } + hooksResult := make([]IHookEntry, 0, len(info.Hooks)) + for _, hook := range info.Hooks { + if !toBeExcluded(hook.FuncName) { + hooksResult = append(hooksResult, hook) + } + } + info.Hooks = hooksResult + + apiResult := make([]IHookEntry, 0, len(info.API)) + for _, api := range info.API { + if !toBeExcluded(api.FuncName) { + apiResult = append(apiResult, api) + } + } + info.API = apiResult + + return info +} + +func main() { + pluginPackageDir := getPluginPackageDir() + + log.Println("Generating plugin glue") + info, err := getPluginInfo(pluginPackageDir) + if err != nil { + fmt.Println("Unable to get plugin info: " + err.Error()) + } + + info = removeExcluded(info) + + generateGlue(info) +} -- cgit v1.2.3-1-g7c22