From a0b9623a9f770b8878a9f8f379d4dc316348d514 Mon Sep 17 00:00:00 2001 From: fumiama Date: Wed, 13 Oct 2021 11:07:34 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20dyloader=20=E5=A2=9E=E5=8A=A0=20win?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=20register?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 2 +- control/register.go | 22 +-- dyloader/plugin/plugin.go | 79 +++++++++++ dyloader/plugin/plugin_dlopen.go | 186 +++++++++++++++++++++++++ dyloader/plugin/plugin_loadlibrary.go | 193 ++++++++++++++++++++++++++ dyloader/plugin/plugin_stubs.go | 21 +++ dyloader/scan.go | 10 +- dyloader/winign.go | 4 - 9 files changed, 500 insertions(+), 18 deletions(-) create mode 100644 dyloader/plugin/plugin.go create mode 100644 dyloader/plugin/plugin_dlopen.go create mode 100644 dyloader/plugin/plugin_loadlibrary.go create mode 100644 dyloader/plugin/plugin_stubs.go delete mode 100644 dyloader/winign.go diff --git a/.gitignore b/.gitignore index d8e6bd6c..6a0b5201 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data/acgimage data/fortune data/hs plugins/*.so +plugins/*.dll .idea/ .DS_Store .vscode diff --git a/README.md b/README.md index e0eacf27..1b5eb482 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ zerobot -h -t token -u url [-d|w] [-g] qq1 qq2 qq3 ... - 需要配合 [webgui](https://github.com/FloatTech/bot-manager) 使用 - **动态加载插件** - [x] /刷新插件 - - 仅 Linux, FreeBSD, macOS 可用,默认注释不开启。 + - 仅 Linux, FreeBSD, macOS, Windows 可用,默认注释不开启。 - 开启后`zbp`可执行文件约增大 2M ,每个插件的`.so`文件约 4 ~ 20 M ,如非必要建议不开启。 - 动态加载的插件需放置在`plugins/`下,编译命令如下。插件包名必须为`main`。 ```bash diff --git a/control/register.go b/control/register.go index 8c1eee51..96313df6 100644 --- a/control/register.go +++ b/control/register.go @@ -4,22 +4,28 @@ import ( zero "github.com/wdvxdr1123/ZeroBot" ) +var enmap = make(map[string]*zero.Engine) + // Register 注册插件控制器 func Register(service string, o *Options) *zero.Engine { engine := zero.New() engine.UsePreHandler(newctrl(service, o).Handler()) + enmap[service] = engine return engine } // Delete 删除插件控制器,不会删除数据 -func Delete(engine *zero.Engine, service string) { - engine.Delete() - mu.RLock() - _, ok := managers[service] - mu.RUnlock() +func Delete(service string) { + engine, ok := enmap[service] if ok { - mu.Lock() - delete(managers, service) - mu.Unlock() + engine.Delete() + mu.RLock() + _, ok = managers[service] + mu.RUnlock() + if ok { + mu.Lock() + delete(managers, service) + mu.Unlock() + } } } diff --git a/dyloader/plugin/plugin.go b/dyloader/plugin/plugin.go new file mode 100644 index 00000000..55666dd8 --- /dev/null +++ b/dyloader/plugin/plugin.go @@ -0,0 +1,79 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package plugin implements loading and symbol resolution of Go plugins. +// +// A plugin is a Go main package with exported functions and variables that +// has been built with: +// +// go build -buildmode=plugin +// +// When a plugin is first opened, the init functions of all packages not +// already part of the program are called. The main function is not run. +// A plugin is only initialized once, and cannot be closed. +// +// Currently plugins are only supported on Linux, FreeBSD, and macOS. +// Please report any issues. +package plugin + +// Plugin is a loaded Go plugin. +type Plugin struct { + pluginpath string + err string // set if plugin failed to load + loaded chan struct{} // closed when loaded + syms map[string]interface{} +} + +// Open opens a Go plugin. +// If a path has already been opened, then the existing *Plugin is returned. +// It is safe for concurrent use by multiple goroutines. +func Open(path string) (*Plugin, error) { + return open(path) +} + +// Lookup searches for a symbol named symName in plugin p. +// A symbol is any exported variable or function. +// It reports an error if the symbol is not found. +// It is safe for concurrent use by multiple goroutines. +func (p *Plugin) Lookup(symName string) (Symbol, error) { + return lookup(p, symName) +} + +// Close closes a Go plugin. +// If a path is noth opened, it is ignored. +// It is safe for concurrent use by multiple goroutines. +func Close(path string) error { + return unload(path) +} + +// A Symbol is a pointer to a variable or function. +// +// For example, a plugin defined as +// +// package main +// +// import "fmt" +// +// var V int +// +// func F() { fmt.Printf("Hello, number %d\n", V) } +// +// may be loaded with the Open function and then the exported package +// symbols V and F can be accessed +// +// p, err := plugin.Open("plugin_name.so") +// if err != nil { +// panic(err) +// } +// v, err := p.Lookup("V") +// if err != nil { +// panic(err) +// } +// f, err := p.Lookup("F") +// if err != nil { +// panic(err) +// } +// *v.(*int) = 7 +// f.(func())() // prints "Hello, number 7" +type Symbol interface{} diff --git a/dyloader/plugin/plugin_dlopen.go b/dyloader/plugin/plugin_dlopen.go new file mode 100644 index 00000000..12edb1a9 --- /dev/null +++ b/dyloader/plugin/plugin_dlopen.go @@ -0,0 +1,186 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux,cgo darwin,cgo freebsd,cgo + +package plugin + +/* +#cgo linux LDFLAGS: -ldl +#include +#include +#include +#include +#include +static uintptr_t pluginOpen(const char* path, char** err) { + void* h = dlopen(path, RTLD_NOW|RTLD_GLOBAL); + if (h == NULL) { + *err = (char*)dlerror(); + } + return (uintptr_t)h; +} +static void* pluginLookup(uintptr_t h, const char* name, char** err) { + void* r = dlsym((void*)h, name); + if (r == NULL) { + *err = (char*)dlerror(); + } + return r; +} +static int pluginClose(void* handle, char** err) { + int res = dlclose(handle) + if (res != 0) { + *err = (char*)dlerror(); + } + return res; +} +*/ +import "C" + +import ( + "errors" + "sync" + "unsafe" +) + +func open(name string) (*Plugin, error) { + cPath := make([]byte, C.PATH_MAX+1) + cRelName := make([]byte, len(name)+1) + copy(cRelName, name) + if C.realpath( + (*C.char)(unsafe.Pointer(&cRelName[0])), + (*C.char)(unsafe.Pointer(&cPath[0]))) == nil { + return nil, errors.New(`plugin.Open("` + name + `"): realpath failed`) + } + + filepath := C.GoString((*C.char)(unsafe.Pointer(&cPath[0]))) + + pluginsMu.Lock() + if p := plugins[filepath]; p != nil { + pluginsMu.Unlock() + if p.err != "" { + return nil, errors.New(`plugin.Open("` + name + `"): ` + p.err + ` (previous failure)`) + } + <-p.loaded + return p, nil + } + var cErr *C.char + h := C.pluginOpen((*C.char)(unsafe.Pointer(&cPath[0])), &cErr) + if h == 0 { + pluginsMu.Unlock() + return nil, errors.New(`plugin.Open("` + name + `"): ` + C.GoString(cErr)) + } + // TODO(crawshaw): look for plugin note, confirm it is a Go plugin + // and it was built with the correct toolchain. + if len(name) > 3 && name[len(name)-3:] == ".so" { + name = name[:len(name)-3] + } + if plugins == nil { + plugins = make(map[string]*Plugin) + } + pluginpath, syms, errstr := lastmoduleinit() + if errstr != "" { + plugins[filepath] = &Plugin{ + pluginpath: pluginpath, + err: errstr, + } + pluginsMu.Unlock() + return nil, errors.New(`plugin.Open("` + name + `"): ` + errstr) + } + // This function can be called from the init function of a plugin. + // Drop a placeholder in the map so subsequent opens can wait on it. + p := &Plugin{ + pluginpath: pluginpath, + loaded: make(chan struct{}), + } + plugins[filepath] = p + pluginsMu.Unlock() + + initStr := make([]byte, len(pluginpath)+len("..inittask")+1) // +1 for terminating NUL + copy(initStr, pluginpath) + copy(initStr[len(pluginpath):], "..inittask") + + initTask := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&initStr[0])), &cErr) + if initTask != nil { + doInit(initTask) + } + + // Fill out the value of each plugin symbol. + updatedSyms := map[string]interface{}{} + for symName, sym := range syms { + isFunc := symName[0] == '.' + if isFunc { + delete(syms, symName) + symName = symName[1:] + } + + fullName := pluginpath + "." + symName + cname := make([]byte, len(fullName)+1) + copy(cname, fullName) + + p := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&cname[0])), &cErr) + if p == nil { + return nil, errors.New(`plugin.Open("` + name + `"): could not find symbol ` + symName + `: ` + C.GoString(cErr)) + } + valp := (*[2]unsafe.Pointer)(unsafe.Pointer(&sym)) + if isFunc { + (*valp)[1] = unsafe.Pointer(&p) + } else { + (*valp)[1] = p + } + // we can't add to syms during iteration as we'll end up processing + // some symbols twice with the inability to tell if the symbol is a function + updatedSyms[symName] = sym + } + p.syms = updatedSyms + + close(p.loaded) + return p, nil +} + +func unload(name string) error { + cPath := make([]byte, C.PATH_MAX+1) + cRelName := make([]byte, len(name)+1) + copy(cRelName, name) + if C.realpath( + (*C.char)(unsafe.Pointer(&cRelName[0])), + (*C.char)(unsafe.Pointer(&cPath[0]))) == nil { + return errors.New(`plugin.Close("` + name + `"): realpath failed`) + } + + filepath := C.GoString((*C.char)(unsafe.Pointer(&cPath[0]))) + + pluginsMu.Lock() + p := plugins[filepath] + if p != nil { + delete(plugins, filepath) + } + pluginsMu.Unlock() + if p != nil { + var cErr *C.char + res := C.pluginClose(unsafe.Pointer(p), &cErr) + if res != 0 { + return errors.New(`plugin.Close("` + name + `"): ` + C.GoString(cErr)) + } + } + return nil +} + +func lookup(p *Plugin, symName string) (Symbol, error) { + if s := p.syms[symName]; s != nil { + return s, nil + } + return nil, errors.New("plugin: symbol " + symName + " not found in plugin " + p.pluginpath) +} + +var ( + pluginsMu sync.Mutex + plugins map[string]*Plugin +) + +// lastmoduleinit is defined in package runtime +func lastmoduleinit() (pluginpath string, syms map[string]interface{}, errstr string) + +// doInit is defined in package runtime +//go:linkname doInit runtime.doInit +func doInit(t unsafe.Pointer) // t should be a *runtime.initTask diff --git a/dyloader/plugin/plugin_loadlibrary.go b/dyloader/plugin/plugin_loadlibrary.go new file mode 100644 index 00000000..c6fd2281 --- /dev/null +++ b/dyloader/plugin/plugin_loadlibrary.go @@ -0,0 +1,193 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows,cgo + +package plugin + +/* +#include +#include +#include +#include +#include +#define ERRBUF 20 +static char *dlerror() { + char *err=(char *) malloc(ERRBUF); + sprintf(err, "error %i", GetLastError()); + return err; +} +static uintptr_t pluginOpen(const char* path, char** err) { + void* h = LoadLibrary(path); + if (h == NULL) { + *err = (char*)dlerror(); + } + return (uintptr_t)h; +} +static void* pluginLookup(uintptr_t h, const char* name, char** err) { + void* r = GetProcAddress((void*)h, name); + if (r == NULL) { + *err = (char*)dlerror(); + } + return r; +} +static int pluginClose(void* handle, char** err) { + int res = FreeLibrary(handle); + if (res != 0) { + *err = (char*)dlerror(); + } + return res; +} +*/ +import "C" + +import ( + "errors" + "sync" + "unsafe" +) + +func open(name string) (*Plugin, error) { + cPath := make([]byte, C.PATH_MAX+1) + cRelName := make([]byte, len(name)+1) + copy(cRelName, name) + if C._fullpath( + (*C.char)(unsafe.Pointer(&cPath[0])), + (*C.char)(unsafe.Pointer(&cRelName[0])), C.PATH_MAX) == nil { + return nil, errors.New(`plugin.Open("` + name + `"): realpath failed`) + } + + filepath := C.GoString((*C.char)(unsafe.Pointer(&cPath[0]))) + + pluginsMu.Lock() + if p := plugins[filepath]; p != nil { + pluginsMu.Unlock() + if p.err != "" { + return nil, errors.New(`plugin.Open("` + name + `"): ` + p.err + ` (previous failure)`) + } + <-p.loaded + return p, nil + } + var cErr *C.char + h := C.pluginOpen((*C.char)(unsafe.Pointer(&cPath[0])), &cErr) + if h == 0 { + pluginsMu.Unlock() + return nil, errors.New(`plugin.Open("` + name + `"): ` + C.GoString(cErr)) + } + // TODO(crawshaw): look for plugin note, confirm it is a Go plugin + // and it was built with the correct toolchain. + if len(name) > 4 && name[len(name)-4:] == ".dll" { + name = name[:len(name)-4] + } + if plugins == nil { + plugins = make(map[string]*Plugin) + } + pluginpath, syms, errstr := lastmoduleinit() + if errstr != "" { + plugins[filepath] = &Plugin{ + pluginpath: pluginpath, + err: errstr, + } + pluginsMu.Unlock() + return nil, errors.New(`plugin.Open("` + name + `"): ` + errstr) + } + // This function can be called from the init function of a plugin. + // Drop a placeholder in the map so subsequent opens can wait on it. + p := &Plugin{ + pluginpath: pluginpath, + loaded: make(chan struct{}), + } + plugins[filepath] = p + pluginsMu.Unlock() + + initStr := make([]byte, len(pluginpath)+len("..inittask")+1) // +1 for terminating NUL + copy(initStr, pluginpath) + copy(initStr[len(pluginpath):], "..inittask") + + initTask := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&initStr[0])), &cErr) + if initTask != nil { + doInit(initTask) + } + + // Fill out the value of each plugin symbol. + updatedSyms := map[string]interface{}{} + for symName, sym := range syms { + isFunc := symName[0] == '.' + if isFunc { + delete(syms, symName) + symName = symName[1:] + } + + fullName := pluginpath + "." + symName + cname := make([]byte, len(fullName)+1) + copy(cname, fullName) + + p := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&cname[0])), &cErr) + if p == nil { + return nil, errors.New(`plugin.Open("` + name + `"): could not find symbol ` + symName + `: ` + C.GoString(cErr)) + } + valp := (*[2]unsafe.Pointer)(unsafe.Pointer(&sym)) + if isFunc { + (*valp)[1] = unsafe.Pointer(&p) + } else { + (*valp)[1] = p + } + // we can't add to syms during iteration as we'll end up processing + // some symbols twice with the inability to tell if the symbol is a function + updatedSyms[symName] = sym + } + p.syms = updatedSyms + + close(p.loaded) + return p, nil +} + +func unload(name string) error { + cPath := make([]byte, C.PATH_MAX+1) + cRelName := make([]byte, len(name)+1) + copy(cRelName, name) + if C._fullpath( + (*C.char)(unsafe.Pointer(&cPath[0])), + (*C.char)(unsafe.Pointer(&cRelName[0])), C.PATH_MAX) == nil { + return errors.New(`plugin.Close("` + name + `"): realpath failed`) + } + + filepath := C.GoString((*C.char)(unsafe.Pointer(&cPath[0]))) + + var cErr *C.char + + pluginsMu.Lock() + p := plugins[filepath] + if p != nil { + delete(plugins, filepath) + } + pluginsMu.Unlock() + if p != nil { + res := C.pluginClose(unsafe.Pointer(p), &cErr) + if res != 0 { + return errors.New(`plugin.Close("` + name + `"): ` + C.GoString(cErr)) + } + } + return nil +} + +func lookup(p *Plugin, symName string) (Symbol, error) { + if s := p.syms[symName]; s != nil { + return s, nil + } + return nil, errors.New("plugin: symbol " + symName + " not found in plugin " + p.pluginpath) +} + +var ( + pluginsMu sync.Mutex + plugins map[string]*Plugin +) + +// lastmoduleinit is defined in package runtime +//go:linkname lastmoduleinit plugin.lastmoduleinit +func lastmoduleinit() (pluginpath string, syms map[string]interface{}, errstr string) + +// doInit is defined in package runtime +//go:linkname doInit runtime.doInit +func doInit(t unsafe.Pointer) // t should be a *runtime.initTask diff --git a/dyloader/plugin/plugin_stubs.go b/dyloader/plugin/plugin_stubs.go new file mode 100644 index 00000000..fea148d1 --- /dev/null +++ b/dyloader/plugin/plugin_stubs.go @@ -0,0 +1,21 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !linux,!freebsd,!darwin,!windows !cgo + +package plugin + +import "errors" + +func lookup(p *Plugin, symName string) (Symbol, error) { + return nil, errors.New("plugin: not implemented") +} + +func open(name string) (*Plugin, error) { + return nil, errors.New("plugin: not implemented") +} + +func unload(name string) error { + return nil, errors.New("plugin: not implemented") +} diff --git a/dyloader/scan.go b/dyloader/scan.go index ea425cac..1a28d31a 100644 --- a/dyloader/scan.go +++ b/dyloader/scan.go @@ -1,17 +1,16 @@ -//go:build !windows -// +build !windows - +// Package dyloader 动态插件加载器 package dyloader import ( "io/fs" "path/filepath" - "plugin" "strings" "github.com/sirupsen/logrus" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" + + "github.com/FloatTech/ZeroBot-Plugin/dyloader/plugin" ) func init() { @@ -36,7 +35,8 @@ func load(path string, d fs.DirEntry, err error) error { if d.IsDir() { return nil } - if strings.HasSuffix(d.Name(), ".so") { + n := d.Name() + if strings.HasSuffix(n, ".so") || strings.HasSuffix(n, ".dll") { _, err = plugin.Open(path) if err == nil { logrus.Infoln("[dyloader]加载插件", path, "成功") diff --git a/dyloader/winign.go b/dyloader/winign.go deleted file mode 100644 index f47b5180..00000000 --- a/dyloader/winign.go +++ /dev/null @@ -1,4 +0,0 @@ -//go:build windows -// +build windows - -package dyloader