MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)
* Moving plugins to use hashicorp go-plugin. * Tweaks from feedback.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ecefa6cdd1
Коммит
1e5c432e10
377
plugin/interface_generator/main.go
Обычный файл
377
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)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user