Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

757 lines
22 KiB
Go

// 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"
"log"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"text/template"
"github.com/pkg/errors"
"golang.org/x/tools/imports"
)
var excludedPluginHooks = []string{
"ChannelMemberWillBeAdded",
"FileWillBeUploaded",
"TeamMemberWillBeAdded",
"Implemented",
"LoadPluginConfiguration",
"InstallPlugin",
"LogAuditRec",
"LogAuditRecWithLevel",
"LogDebug",
"LogError",
"LogInfo",
"LogWarn",
"MessageWillBePosted",
"MessageWillBeUpdated",
"MessagesWillBeConsumed",
"MessagesWillBeConsumedWithContext",
"OnActivate",
"PluginHTTP",
"ServeHTTP",
"UploadData",
"ReceiveSharedChannelAttachmentSyncMsg",
"ServeMetrics",
}
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, ", ") + ")"
}
// FieldListToFuncListAppendErr renders the return signature with an extra trailing
// `error` (the RPC transport error). Used to emit *WithRPCErr companion signatures.
func FieldListToFuncListAppendErr(fieldList *ast.FieldList, fileset *token.FileSet) string {
if fieldList == nil || len(fieldList.List) == 0 {
return "error"
}
base := FieldListToFuncList(fieldList, fileset)
return "(" + base[1:len(base)-1] + ", error)"
}
func FieldListToNames(fieldList *ast.FieldList, variadicForm bool) string {
result := []string{}
if fieldList == nil || len(fieldList.List) == 0 {
return ""
}
for _, field := range fieldList.List {
for _, name := range field.Names {
paramName := name.Name
if _, ok := field.Type.(*ast.Ellipsis); ok && variadicForm {
paramName = fmt.Sprintf("%s...", paramName)
}
result = append(result, paramName)
}
}
return strings.Join(result, ", ")
}
func FieldListToEncodedErrors(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
result := []string{}
if fieldList == nil {
return ""
}
nextLetter := 'A'
for _, field := range fieldList.List {
typeNameBuffer := &bytes.Buffer{}
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
if err != nil {
panic(err)
}
if typeNameBuffer.String() != "error" {
nextLetter++
continue
}
name := ""
if len(field.Names) == 0 {
name = string(nextLetter)
nextLetter++
} else {
for range field.Names {
name += string(nextLetter)
nextLetter++
}
}
result = append(result, structPrefix+name+" = encodableError("+structPrefix+name+")")
}
return strings.Join(result, "\n")
}
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 {
typeNameBuffer := &bytes.Buffer{}
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
if err != nil {
panic(err)
}
typeName := typeNameBuffer.String()
suffix := ""
if strings.HasPrefix(typeName, "...") {
suffix = "..."
}
if len(field.Names) == 0 {
result = append(result, structPrefix+string(nextLetter)+suffix)
nextLetter++
} else {
for range field.Names {
result = append(result, structPrefix+string(nextLetter)+suffix)
nextLetter++
}
}
}
return strings.Join(result, ", ")
}
// FieldListDestructAppendErr is FieldListDestruct with an extra trailing `<prefix>RPCErr`
// variable name appended — used to bind the transport error in *WithRPCErr templates.
func FieldListDestructAppendErr(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
base := FieldListDestruct(structPrefix, fieldList, fileset)
rpcErr := structPrefix + "RPCErr"
if base == "" {
return rpcErr
}
return base + ", " + rpcErr
}
func FieldListToRecordSuccess(structPrefix string, fieldList *ast.FieldList) string {
if fieldList == nil || len(fieldList.List) == 0 {
return "true"
}
result := ""
nextLetter := 'A'
for _, field := range fieldList.List {
typeName := baseTypeName(field.Type)
if typeName == "error" || typeName == "AppError" {
result = structPrefix + string(nextLetter)
break
}
nextLetter++
}
if result == "" {
return "true"
}
return fmt.Sprintf("%s == nil", result)
}
// FieldListToRecordSuccessWithRPCErr renders the success-flag expression for a *WithRPCErr
// timer-layer call. It combines the RPC transport check with the base hook's app-error check
// (if any), so the metric counts a call as successful only when both the transport and the
// plugin's application-level return signal success.
func FieldListToRecordSuccessWithRPCErr(structPrefix string, fieldList *ast.FieldList) string {
rpcErr := structPrefix + "RPCErr == nil"
base := FieldListToRecordSuccess(structPrefix, fieldList)
if base == "true" {
return rpcErr
}
return rpcErr + " && " + base
}
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 strings.HasPrefix(typeName, "...") {
typeName = strings.Replace(typeName, "...", "[]", 1)
}
if len(field.Names) == 0 {
result = append(result, string(nextLetter)+" "+typeName)
nextLetter++
} else {
for range field.Names {
result = append(result, string(nextLetter)+" "+typeName)
nextLetter++
}
}
}
return strings.Join(result, "\n\t")
}
func baseTypeName(x ast.Expr) string {
switch t := x.(type) {
case *ast.Ident:
return t.Name
case *ast.SelectorExpr:
if _, ok := t.X.(*ast.Ident); ok {
// only possible for qualified type names;
// assume type is imported
return t.Sel.Name
}
case *ast.ParenExpr:
return baseTypeName(t.X)
case *ast.StarExpr:
return baseTypeName(t.X)
}
return ""
}
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)
return nil, 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
import (
"fmt"
"log"
saml2 "github.com/mattermost/gosaml2"
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/mlog"
)
{{range .HooksMethods}}
func init() {
hookNameToId["{{.Name}}"] = {{.Name}}ID
}
type {{.Name | obscure}}Args struct {
{{structStyle .Params}}
}
type {{.Name | obscure}}Returns struct {
{{structStyle .Return}}
}
func (g *hooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
_returns := &{{.Name | obscure}}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))
}
}
{{ if .Return }} return {{destruct "_returns." .Return}} {{ end }}
}
// {{.Name}}WithRPCErr returns the same values as {{.Name}}, with an additional trailing error
// for the RPC transport — always the LAST return slot.
func (g *hooksRPCClient) {{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}} {
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
_returns := &{{.Name | obscure}}Returns{}
var _err error
if g.implemented[{{.Name}}ID] {
_err = g.client.Call("Plugin.{{.Name}}", _args, _returns)
if _err != nil {
// Reset _returns so partial gob decoding can't leak non-zero
// values past a transport failure (HooksWithRPCErrGenerated contract).
_returns = &{{.Name | obscure}}Returns{}
g.log.Debug("RPC call {{.Name}} to plugin failed.", mlog.Err(_err))
}
}
{{ if .Return }} return {{destruct "_returns." .Return}}, _err {{ else }} return _err {{ end }}
}
func (s *hooksRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}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}})
{{if .Return}}{{encodeErrors "returns." .Return}}{{end -}}
} else {
return encodableError(fmt.Errorf("Hook {{.Name}} called but not implemented."))
}
return nil
}
{{end}}
// HooksWithRPCErrGenerated provides a WithRPCErr variant for every generated hook. The last error return
// is always the RPC transport error — if non-nil, the plugin's other return values are zero. For
// hooks whose base signature already returns error, the tuple is (originalReturns..., rpcErr)
// where the final slot is always transport.
//
// If the plugin does not implement the hook, the companion returns zero values and a nil error —
// indistinguishable from a successful invocation that returned zeros. Callers MUST gate on
// supervisor.Implements(<HookID>) (or use Environment.RunMultiPluginHookWithRPCErr, which gates
// by the iteration's hook ID — note that any *WithRPCErr method called on the closure's
// HooksWithRPCErrGenerated is independently subject to its own implemented-gate).
type HooksWithRPCErrGenerated interface {
{{range .HooksMethods}}
{{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}}
{{end}}
}
{{range .APIMethods}}
type {{.Name | obscure}}Args struct {
{{structStyle .Params}}
}
type {{.Name | obscure}}Returns struct {
{{structStyle .Return}}
}
func (g *apiRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
_args := &{{.Name | obscure}}Args{ {{valuesOnly .Params}} }
_returns := &{{.Name | obscure}}Returns{}
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
log.Printf("RPC call to {{.Name}} API failed: %s", err.Error())
}
{{ if .Return }} return {{destruct "_returns." .Return}} {{ end }}
}
func (s *apiRPCServer) {{.Name}}(args *{{.Name | obscure}}Args, returns *{{.Name | obscure}}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}})
{{if .Return}}{{encodeErrors "returns." .Return}}{{end -}}
} else {
return encodableError(fmt.Errorf("API {{.Name}} called but not implemented."))
}
return nil
}
{{end}}
`
var apiTimerLayerTemplate = `// 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
import (
"io"
"net/http"
timePkg "time"
"github.com/hanzoteam/server/server/public/model"
)
type apiTimerLayer struct {
pluginID string
apiImpl API
metrics metricsInterface
}
func (api *apiTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
if api.metrics != nil {
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
api.metrics.ObservePluginAPIDuration(api.pluginID, name, success, elapsedTime)
}
}
{{range .APIMethods}}
func (api *apiTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
startTime := timePkg.Now()
{{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} api.apiImpl.{{.Name}}({{valuesOnly .Params}})
api.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }})
{{ if .Return }} return {{destruct "_returns" .Return}} {{ end -}}
}
{{end}}
`
var hooksTimerLayerTemplate = `// 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
import (
"io"
"net/http"
timePkg "time"
saml2 "github.com/mattermost/gosaml2"
"github.com/hanzoteam/server/server/public/model"
)
type hooksTimerLayer struct {
pluginID string
hooksImpl Hooks
hooksWithRPCErrImpl HooksWithRPCErr
metrics metricsInterface
}
func (hooks *hooksTimerLayer) recordTime(startTime timePkg.Time, name string, success bool) {
if hooks.metrics != nil {
elapsedTime := float64(timePkg.Since(startTime)) / float64(timePkg.Second)
hooks.metrics.ObservePluginHookDuration(hooks.pluginID, name, success, elapsedTime)
}
}
{{range .HooksMethods}}
func (hooks *hooksTimerLayer) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
startTime := timePkg.Now()
{{ if .Return }} {{destruct "_returns" .Return}} := {{ end }} hooks.hooksImpl.{{.Name}}({{valuesOnly .Params}})
hooks.recordTime(startTime, "{{.Name}}", {{ shouldRecordSuccess "_returns" .Return }})
{{ if .Return }} return {{destruct "_returns" .Return}} {{end -}}
}
{{end}}
{{range .HooksMethodsRPCErr}}
func (hooks *hooksTimerLayer) {{.Name}}WithRPCErr{{funcStyle .Params}} {{funcStyleAppendErr .Return}} {
startTime := timePkg.Now()
{{destructAppendErr "_returns" .Return}} := hooks.hooksWithRPCErrImpl.{{.Name}}WithRPCErr({{valuesOnly .Params}})
hooks.recordTime(startTime, "{{.Name}}WithRPCErr", {{ shouldRecordSuccessWithRPCErr "_returns" .Return }})
return {{destructAppendErr "_returns" .Return}}
}
{{end}}
`
type MethodParams struct {
Name string
Params *ast.FieldList
Return *ast.FieldList
}
type HooksTemplateParams struct {
HooksMethods []MethodParams
HooksMethodsRPCErr []MethodParams
APIMethods []MethodParams
}
func generateHooksGlue(info *PluginInterfaceInfo) {
templateFunctions := map[string]any{
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
"funcStyleAppendErr": func(fields *ast.FieldList) string { return FieldListToFuncListAppendErr(fields, info.FileSet) },
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, false) },
"encodeErrors": func(structPrefix string, fields *ast.FieldList) string {
return FieldListToEncodedErrors(structPrefix, fields, info.FileSet)
},
"destruct": func(structPrefix string, fields *ast.FieldList) string {
return FieldListDestruct(structPrefix, fields, info.FileSet)
},
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
return FieldListToRecordSuccess(structPrefix, fields)
},
"obscure": func(name string) string {
return "Z_" + name
},
}
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{}
err = hooksTemplate.Execute(templateResult, &templateParams)
if err != nil {
panic(err)
}
formatted, err := imports.Process("", templateResult.Bytes(), nil)
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), formatted, 0664); err != nil {
panic(err)
}
}
func generatePluginTimerLayer(info *PluginInterfaceInfo) {
templateFunctions := map[string]any{
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
"funcStyleAppendErr": func(fields *ast.FieldList) string { return FieldListToFuncListAppendErr(fields, info.FileSet) },
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, true) },
"destruct": func(structPrefix string, fields *ast.FieldList) string {
return FieldListDestruct(structPrefix, fields, info.FileSet)
},
"destructAppendErr": func(structPrefix string, fields *ast.FieldList) string {
return FieldListDestructAppendErr(structPrefix, fields, info.FileSet)
},
"shouldRecordSuccess": func(structPrefix string, fields *ast.FieldList) string {
return FieldListToRecordSuccess(structPrefix, fields)
},
"shouldRecordSuccessWithRPCErr": func(structPrefix string, fields *ast.FieldList) string {
return FieldListToRecordSuccessWithRPCErr(structPrefix, fields)
},
}
// Prepare template params. The timer layer wraps the full Hooks interface, so
// HooksMethods includes excluded hooks too. *WithRPCErr companions only exist
// for non-excluded hooks (see HooksWithRPCErrGenerated in client_rpc_generated.go), so the
// excluded subset is filtered into HooksMethodsRPCErr for that loop.
excluded := func(name string) bool { return slices.Contains(excludedPluginHooks, name) }
templateParams := HooksTemplateParams{}
for _, hook := range info.Hooks {
mp := MethodParams{
Name: hook.FuncName,
Params: hook.Args,
Return: hook.Results,
}
templateParams.HooksMethods = append(templateParams.HooksMethods, mp)
if !excluded(hook.FuncName) {
templateParams.HooksMethodsRPCErr = append(templateParams.HooksMethodsRPCErr, mp)
}
}
for _, api := range info.API {
templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
Name: api.FuncName,
Params: api.Args,
Return: api.Results,
})
}
pluginTemplates := map[string]string{
"api_timer_layer_generated.go": apiTimerLayerTemplate,
"hooks_timer_layer_generated.go": hooksTimerLayerTemplate,
}
for fileName, presetTemplate := range pluginTemplates {
parsedTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(presetTemplate)
if err != nil {
panic(err)
}
templateResult := &bytes.Buffer{}
err = parsedTemplate.Execute(templateResult, &templateParams)
if err != nil {
panic(err)
}
formatted, err := imports.Process("", templateResult.Bytes(), nil)
if err != nil {
panic(err)
}
if err := os.WriteFile(filepath.Join(getPluginPackageDir(), fileName), formatted, 0664); err != nil {
panic(err)
}
}
}
func getPluginPackageDir() string {
dirs, err := goList("github.com/hanzoteam/server/server/public/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, excluded []string) *PluginInterfaceInfo {
newIface := &PluginInterfaceInfo{
FileSet: info.FileSet,
}
toBeExcluded := func(item string) bool {
return slices.Contains(excluded, item)
}
hooksResult := make([]IHookEntry, 0, len(info.Hooks))
for _, hook := range info.Hooks {
if !toBeExcluded(hook.FuncName) {
hooksResult = append(hooksResult, hook)
}
}
newIface.Hooks = hooksResult
apiResult := make([]IHookEntry, 0, len(info.API))
for _, api := range info.API {
if !toBeExcluded(api.FuncName) {
apiResult = append(apiResult, api)
}
}
newIface.API = apiResult
return newIface
}
func main() {
pluginPackageDir := getPluginPackageDir()
forRPC, err := getPluginInfo(pluginPackageDir)
if err != nil {
fmt.Println("Unable to get plugin info: " + err.Error())
}
log.Println("Generating plugin hooks glue")
generateHooksGlue(removeExcluded(forRPC, excludedPluginHooks))
// Generate plugin timer layers
log.Println("Generating plugin timer glue")
forPlugins, err := getPluginInfo(pluginPackageDir)
if err != nil {
fmt.Println("Unable to get plugin info: " + err.Error())
}
generatePluginTimerLayer(forPlugins)
}