mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-20 08:21:09 -05:00
This reverts commit ea61458f16. This was causing panic in the plugins because the client and the plugin API changed with this PR
636 lines
16 KiB
Go
636 lines
16 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"
|
|
"io/ioutil"
|
|
"log"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/tools/imports"
|
|
)
|
|
|
|
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, 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, ", ")
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
|
|
{{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 }}
|
|
}
|
|
|
|
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}}
|
|
|
|
{{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/mattermost/mattermost-server/v5/einterfaces"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
)
|
|
|
|
type apiTimerLayer struct {
|
|
pluginID string
|
|
apiImpl API
|
|
metrics einterfaces.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"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
)
|
|
|
|
type hooksTimerLayer struct {
|
|
pluginID string
|
|
hooksImpl Hooks
|
|
metrics einterfaces.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}}
|
|
`
|
|
|
|
type MethodParams struct {
|
|
Name string
|
|
Params *ast.FieldList
|
|
Return *ast.FieldList
|
|
}
|
|
|
|
type HooksTemplateParams struct {
|
|
HooksMethods []MethodParams
|
|
APIMethods []MethodParams
|
|
}
|
|
|
|
func generateHooksGlue(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, 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{}
|
|
hooksTemplate.Execute(templateResult, &templateParams)
|
|
|
|
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), formatted, 0664); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func generatePluginTimerLayer(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, true) },
|
|
"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)
|
|
},
|
|
}
|
|
|
|
// Prepare template params
|
|
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,
|
|
})
|
|
}
|
|
|
|
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{}
|
|
parsedTemplate.Execute(templateResult, &templateParams)
|
|
|
|
formatted, err := imports.Process("", templateResult.Bytes(), nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), fileName), formatted, 0664); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getPluginPackageDir() string {
|
|
dirs, err := goList("github.com/mattermost/mattermost-server/v5/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{
|
|
"FileWillBeUploaded",
|
|
"Implemented",
|
|
"LoadPluginConfiguration",
|
|
"InstallPlugin",
|
|
"LogDebug",
|
|
"LogError",
|
|
"LogInfo",
|
|
"LogWarn",
|
|
"MessageWillBePosted",
|
|
"MessageWillBeUpdated",
|
|
"OnActivate",
|
|
"PluginHTTP",
|
|
"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 hooks glue")
|
|
forRPC, err := getPluginInfo(pluginPackageDir)
|
|
if err != nil {
|
|
fmt.Println("Unable to get plugin info: " + err.Error())
|
|
}
|
|
generateHooksGlue(removeExcluded(forRPC))
|
|
|
|
// 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)
|
|
}
|