summaryrefslogtreecommitdiffstats
path: root/plugin/interface_generator
diff options
context:
space:
mode:
authorChristopher Speller <crspeller@gmail.com>2018-06-25 12:33:13 -0700
committerGitHub <noreply@github.com>2018-06-25 12:33:13 -0700
commit1e5c432e1029601a664454388ae366ef69618d62 (patch)
treecb9e8bfb66640ac3b29c934bb2c3202d25aeb368 /plugin/interface_generator
parentecefa6cdd1e7376046bbec82c1b47f7756fea646 (diff)
downloadchat-1e5c432e1029601a664454388ae366ef69618d62.tar.gz
chat-1e5c432e1029601a664454388ae366ef69618d62.tar.bz2
chat-1e5c432e1029601a664454388ae366ef69618d62.zip
MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)
* Moving plugins to use hashicorp go-plugin. * Tweaks from feedback.
Diffstat (limited to 'plugin/interface_generator')
-rw-r--r--plugin/interface_generator/main.go377
1 files changed, 377 insertions, 0 deletions
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)
+}