├── example ├── run.sh ├── main.go └── testplugin.go ├── .travis.yml ├── go.mod ├── .gitignore ├── go.sum ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── README.md ├── hotplugin.go ├── manager.go └── plugin.go /example/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export GO111MODULE=on 3 | go build -buildmode=plugin ./testplugin.go 4 | go run ./main.go 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8.x 4 | - 1.9.x 5 | - tip 6 | matrix: 7 | allow_failures: 8 | - go: tip 9 | script: ./test.sh 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/letiantech/hotplugin 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .*.swp 17 | 18 | .idea* 19 | .vscode* -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 2 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 3 | golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa h1:lqti/xP+yD/6zH5TqEwx2MilNIJY5Vbc6Qr8J3qyPIQ= 4 | golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 letian0805@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 letian0805@gmail.com 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "time" 26 | 27 | "github.com/letiantech/hotplugin" 28 | ) 29 | 30 | func main() { 31 | options := hotplugin.ManagerOptions{ 32 | Dir: "./", 33 | Suffix: ".so", 34 | } 35 | hotplugin.StartManager(options) 36 | result := hotplugin.Call("testplugin", "Test", time.Now()) 37 | fmt.Println(result...) 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/letiantech/hotplugin.svg)](https://travis-ci.org/letiantech/hotplugin) 2 | 3 | # hotplugin 4 | golang plugin framework for hot update, go version >= 1.8 5 | 6 | # usage 7 | 1. get hotplugin 8 | ```bash 9 | go get github.com/letiantech/hotplugin 10 | ``` 11 | 2. write a plugin with Load, Unload and other functions like this 12 | ```go 13 | //testplugin.go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | ) 20 | 21 | const ( 22 | pluginName = "testplugin" 23 | pluginVersion = 0x00010000 24 | ) 25 | 26 | func Load(register func(name string, version uint64) error) error { 27 | err := register(pluginName, pluginVersion) 28 | if err != nil { 29 | log.Println(err.Error()) 30 | return err 31 | } 32 | log.Println("loading test plugin") 33 | return nil 34 | } 35 | 36 | func Unload() error { 37 | fmt.Printf("unload %s, version: 0x%x\n", pluginName, pluginVersion) 38 | return nil 39 | } 40 | 41 | func Test(data string) string { 42 | return "hello " + data 43 | } 44 | ``` 45 | 46 | 3. build your plugin 47 | ```bash 48 | go build -buildmode=plugin ./testplugin.go 49 | ``` 50 | 51 | 4. save your testplugin.so to /path/of/plugin/dir 52 | 53 | 5. write main.go like this 54 | ```go 55 | //main.go 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | "github.com/letiantech/hotplugin" 61 | ) 62 | 63 | func main() { 64 | options := hotplugin.ManagerOptions{ 65 | Dir: "./", 66 | Suffix: ".so", 67 | } 68 | hotplugin.StartManager(options) 69 | result := hotplugin.Call("testplugin", "Test", "my world") 70 | fmt.Println(result...) 71 | } 72 | 73 | ``` 74 | -------------------------------------------------------------------------------- /example/testplugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 letian0805@gmail.com 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "log" 26 | "time" 27 | ) 28 | 29 | const ( 30 | pluginName = "testplugin" 31 | pluginVersion = 0x00010000 32 | ) 33 | 34 | func Load(register func(name string, version uint64) error) error { 35 | err := register(pluginName, pluginVersion) 36 | if err != nil { 37 | log.Println(err.Error()) 38 | return err 39 | } 40 | log.Println("loading test plugin") 41 | return nil 42 | } 43 | 44 | func Unload() error { 45 | fmt.Printf("unload %s, version: 0x%x\n", pluginName, pluginVersion) 46 | return nil 47 | } 48 | 49 | func Test(t time.Time) string { 50 | return fmt.Sprintf("current time: %d", t.Unix()) 51 | } 52 | -------------------------------------------------------------------------------- /hotplugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 letian0805@gmail.com 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package hotplugin 22 | 23 | var defaultManager Manager 24 | 25 | func StartManager(options ManagerOptions) error { 26 | if defaultManager != nil && defaultManager.IsRunning() { 27 | return nil 28 | } 29 | var err error 30 | defaultManager, err = NewManager(options) 31 | if err != nil { 32 | return err 33 | } 34 | return defaultManager.Run() 35 | } 36 | 37 | func Call(module, function string, args ...interface{}) []interface{} { 38 | f, err := GetFunc(module, function) 39 | if err != nil { 40 | return []interface{}{err} 41 | } 42 | 43 | return f(args...) 44 | } 45 | 46 | func GetPlugin(name string) (*Plugin, error) { 47 | return defaultManager.GetPlugin(name) 48 | } 49 | 50 | func GetPluginWithVersion(name string, version uint64) (*Plugin, error) { 51 | return defaultManager.GetPluginWithVersion(name, version) 52 | } 53 | 54 | func GetFunc(module, function string) (f func(...interface{}) []interface{}, err error) { 55 | return defaultManager.GetFunc(module, function) 56 | } 57 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 letian0805@gmail.com 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package hotplugin 22 | 23 | import ( 24 | "errors" 25 | "io" 26 | "log" 27 | "os" 28 | "strings" 29 | 30 | "github.com/fsnotify/fsnotify" 31 | ) 32 | 33 | type ManagerOptions struct { 34 | Dir string 35 | Suffix string 36 | } 37 | 38 | type Manager interface { 39 | Run() error 40 | IsRunning() bool 41 | GetPlugin(name string) (*Plugin, error) 42 | GetPluginWithVersion(name string, version uint64) (*Plugin, error) 43 | GetFunc(module, function string) (f func(...interface{}) []interface{}, err error) 44 | Call(module, function string, args ...interface{}) []interface{} 45 | OnLoaded(p *Plugin) 46 | OnReloaded(p *Plugin) 47 | OnUnloaded(p *Plugin) 48 | OnError(p *Plugin, err *PluginError) 49 | } 50 | 51 | type manager struct { 52 | running bool 53 | options ManagerOptions 54 | watcher *fsnotify.Watcher 55 | cache map[string]*Plugin 56 | loaded map[string]map[uint64]*Plugin 57 | } 58 | 59 | func NewManager(options ManagerOptions) (Manager, error) { 60 | watcher, err := fsnotify.NewWatcher() 61 | if err != nil { 62 | log.Print("error: ", err) 63 | return nil, err 64 | } 65 | m := &manager{ 66 | options: options, 67 | watcher: watcher, 68 | running: false, 69 | cache: make(map[string]*Plugin), 70 | loaded: make(map[string]map[uint64]*Plugin), 71 | } 72 | return m, nil 73 | } 74 | 75 | func (m *manager) pluginPath(path string) string { 76 | if !strings.Contains(path, m.options.Dir) { 77 | dir := []byte(m.options.Dir) 78 | if dir[len(dir)-1] != '/' { 79 | path = m.options.Dir + "/" + path 80 | } else { 81 | path = m.options.Dir + path 82 | } 83 | } 84 | return path 85 | } 86 | 87 | func (m *manager) OnReloaded(p *Plugin) { 88 | 89 | } 90 | 91 | func (m *manager) OnError(p *Plugin, err *PluginError) { 92 | 93 | } 94 | 95 | func (m *manager) OnUnloaded(p1 *Plugin) { 96 | name := p1.Name() 97 | version := p1.Version() 98 | log.Print(name, " loaded") 99 | if mp, ok := m.loaded[name]; ok { 100 | delete(mp, version) 101 | if len(mp) == 0 { 102 | delete(m.loaded, name) 103 | } 104 | } 105 | } 106 | 107 | func (m *manager) OnLoaded(p1 *Plugin) { 108 | name := p1.Name() 109 | version := p1.Version() 110 | log.Print(name, " loaded") 111 | if mp, ok := m.loaded[name]; ok { 112 | mp[version] = p1 113 | } else { 114 | mp := make(map[uint64]*Plugin) 115 | mp[version] = p1 116 | m.loaded[name] = mp 117 | } 118 | } 119 | 120 | func (m *manager) loadAll() error { 121 | if m.running { 122 | return nil 123 | } 124 | f, e := os.Open(m.options.Dir) 125 | if e != nil { 126 | return e 127 | } 128 | for { 129 | d, e := f.Readdir(100) 130 | if e != nil { 131 | if e == io.EOF { 132 | f.Seek(0, 0) 133 | break 134 | } 135 | return e 136 | } 137 | for i := 0; i < len(d); i++ { 138 | path := m.pluginPath(d[i].Name()) 139 | if !m.isPlugin(path) { 140 | continue 141 | } 142 | log.Println(path) 143 | p := NewPlugin(path, m) 144 | m.cache[path] = p 145 | p.Load() 146 | } 147 | } 148 | return nil 149 | } 150 | 151 | func (m *manager) isPlugin(path string) bool { 152 | return strings.HasSuffix(path, m.options.Suffix) 153 | } 154 | 155 | func (m *manager) Run() error { 156 | if m.running { 157 | return nil 158 | } 159 | e := m.loadAll() 160 | if e != nil { 161 | log.Println(e.Error()) 162 | return e 163 | } 164 | e = m.watcher.Add(m.options.Dir) 165 | if e != nil { 166 | log.Println(e.Error()) 167 | return e 168 | } 169 | c := make(chan int, 1) 170 | go func() { 171 | m.running = true 172 | c <- 1 173 | for { 174 | select { 175 | case e := <-m.watcher.Events: 176 | path := m.pluginPath(e.Name) 177 | log.Print(e) 178 | if !m.isPlugin(path) { 179 | continue 180 | } 181 | p, ok := m.cache[path] 182 | if !ok || p == nil { 183 | log.Println(path) 184 | p = NewPlugin(path, m) 185 | m.cache[path] = p 186 | } 187 | if e.Op&fsnotify.Write == fsnotify.Write { 188 | p.Reload() 189 | continue 190 | } 191 | if e.Op&fsnotify.Create == fsnotify.Create { 192 | p.Load() 193 | continue 194 | } 195 | if e.Op&fsnotify.Remove == fsnotify.Remove { 196 | p.Unload() 197 | continue 198 | } 199 | case err := <-m.watcher.Errors: 200 | log.Print(err) 201 | continue 202 | } 203 | } 204 | }() 205 | <-c 206 | close(c) 207 | return nil 208 | } 209 | 210 | func (m *manager) GetPlugin(name string) (*Plugin, error) { 211 | mp, ok := m.loaded[name] 212 | if !ok { 213 | return nil, errors.New("not found") 214 | } 215 | var latestVersion uint64 = 0 216 | var latestPlugin *Plugin = nil 217 | for v, p := range mp { 218 | if latestVersion < v { 219 | latestVersion = v 220 | latestPlugin = p 221 | } 222 | } 223 | return latestPlugin, nil 224 | } 225 | 226 | func (m *manager) GetPluginWithVersion(name string, version uint64) (*Plugin, error) { 227 | mp, ok := m.loaded[name] 228 | if !ok { 229 | return nil, errors.New("not found") 230 | } 231 | p, ok := mp[version] 232 | if !ok { 233 | return nil, errors.New("not found") 234 | } 235 | return p, nil 236 | } 237 | 238 | func (m *manager) GetFunc(module, function string) (f func(...interface{}) []interface{}, err error) { 239 | var p *Plugin = nil 240 | p, err = m.GetPlugin(module) 241 | if err != nil { 242 | return 243 | } 244 | if p == nil { 245 | err = errors.New("no plugin " + module) 246 | return 247 | } 248 | 249 | return p.GetFunc(function) 250 | } 251 | 252 | func (m *manager) Call(module, function string, args ...interface{}) []interface{} { 253 | f, err := m.GetFunc(module, function) 254 | if err != nil { 255 | return []interface{}{err} 256 | } 257 | 258 | return f(args...) 259 | } 260 | 261 | func (m *manager) IsRunning() bool { 262 | return m.running 263 | } 264 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 letian0805@gmail.com 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | package hotplugin 22 | 23 | import ( 24 | "errors" 25 | "fmt" 26 | "log" 27 | "plugin" 28 | "reflect" 29 | "sync" 30 | "sync/atomic" 31 | "time" 32 | ) 33 | 34 | const PluginTimeout = 100 * time.Millisecond 35 | 36 | type PluginStatus int32 37 | 38 | const ( 39 | PluginStatusNone PluginStatus = iota 40 | PluginStatusLoading 41 | PluginStatusLoaded 42 | PluginStatusReloading 43 | PluginStatusUnloading 44 | PluginStatusUnloaded 45 | ) 46 | 47 | type PluginError struct { 48 | Type int 49 | Err error 50 | } 51 | 52 | type PluginFunc func(...interface{}) []interface{} 53 | type pluginFuncInfo struct { 54 | fn PluginFunc 55 | rfv reflect.Value 56 | rft reflect.Type 57 | inTypes []reflect.Type 58 | outTypes []reflect.Type 59 | } 60 | 61 | type Plugin struct { 62 | sync.RWMutex 63 | m Manager 64 | name string 65 | version uint64 66 | path string 67 | plugin *plugin.Plugin 68 | status PluginStatus 69 | refs int 70 | cache map[string]*pluginFuncInfo 71 | } 72 | 73 | func NewPlugin(path string, m Manager) *Plugin { 74 | p := &Plugin{ 75 | m: m, 76 | path: path, 77 | status: PluginStatusNone, 78 | refs: 0, 79 | cache: make(map[string]*pluginFuncInfo), 80 | } 81 | return p 82 | } 83 | 84 | func (p *Plugin) Status() PluginStatus { 85 | return PluginStatus(atomic.LoadInt32((*int32)(&(p.status)))) 86 | } 87 | 88 | func (p *Plugin) setStatus(status PluginStatus) { 89 | atomic.StoreInt32((*int32)(&(p.status)), int32(status)) 90 | } 91 | 92 | func (p *Plugin) Name() string { 93 | return p.name 94 | } 95 | 96 | func (p *Plugin) Version() uint64 { 97 | return p.version 98 | } 99 | 100 | func (p *Plugin) Path() string { 101 | return p.path 102 | } 103 | 104 | func (p *Plugin) Load() error { 105 | p.Lock() 106 | defer p.Unlock() 107 | if p.Status() != PluginStatusNone && p.Status() != PluginStatusUnloaded { 108 | return nil 109 | } 110 | p.setStatus(PluginStatusLoading) 111 | path := p.path 112 | p1, e := plugin.Open(path) 113 | if e != nil { 114 | log.Print("load plugin ", path, " error: ", e) 115 | p.setStatus(PluginStatusNone) 116 | return e 117 | } 118 | p.plugin = p1 119 | f, e := p1.Lookup("Load") 120 | if e != nil { 121 | log.Print("load plugin ", path, " error: ", e) 122 | p.setStatus(PluginStatusNone) 123 | return e 124 | } 125 | register := func(name string, version uint64) error { 126 | p.name = name 127 | p.version = version 128 | s := fmt.Sprintf("load plugin: %s, version: 0x%x", p.name, p.version) 129 | p1, e1 := p.m.GetPluginWithVersion(name, version) 130 | if p1 != nil { 131 | e1 = errors.New("can't double load plugin") 132 | log.Println(s, ", error: ", e1.Error()) 133 | p.setStatus(PluginStatusNone) 134 | return e1 135 | } else { 136 | log.Println(s) 137 | p.setStatus(PluginStatusLoaded) 138 | p.m.OnLoaded(p) 139 | return nil 140 | } 141 | } 142 | e = f.(func(func(string, uint64) error) error)(register) 143 | 144 | return e 145 | } 146 | 147 | func (p *Plugin) Reload() error { 148 | if err := p.Unload(); err != nil { 149 | return err 150 | } 151 | if err := p.Load(); err != nil { 152 | return err 153 | } 154 | p.RLock() 155 | defer p.RUnlock() 156 | name := p.name 157 | version := p.version 158 | s := fmt.Sprintf("reload plugin: %s, version: 0x%x", name, version) 159 | log.Print(s) 160 | p.setStatus(PluginStatusLoaded) 161 | return nil 162 | } 163 | 164 | func (p *Plugin) Unload() error { 165 | p.Lock() 166 | defer p.Unlock() 167 | if p.Status() == PluginStatusUnloaded || 168 | p.Status() == PluginStatusUnloading || 169 | p.Status() == PluginStatusNone { 170 | return nil 171 | } 172 | p.cache = make(map[string]*pluginFuncInfo) 173 | name := p.name 174 | version := p.version 175 | s := fmt.Sprintf("unload plugin: %s, version: 0x%x", name, version) 176 | f, e := p.plugin.Lookup("Unload") 177 | if e != nil { 178 | log.Print(s, ", error: ", e) 179 | return e 180 | } 181 | err := f.(func() error)() 182 | log.Print(s) 183 | p.setStatus(PluginStatusUnloaded) 184 | return err 185 | } 186 | 187 | func (p *Plugin) Call(fun string, params ...interface{}) []interface{} { 188 | f, err := p.GetFunc(fun) 189 | if err != nil { 190 | return []interface{}{err} 191 | } 192 | return f(params...) 193 | } 194 | 195 | func (p *Plugin) GetFunc(fun string) (f func(...interface{}) []interface{}, err error) { 196 | p.Lock() 197 | defer p.Unlock() 198 | if p.plugin == nil { 199 | err = errors.New("plugin not loaded") 200 | return 201 | } 202 | info, ok := p.cache[fun] 203 | if ok { 204 | return info.fn, nil 205 | } 206 | f1, err := p.plugin.Lookup(fun) 207 | if err != nil { 208 | return nil, err 209 | } 210 | info = &pluginFuncInfo{} 211 | info.rfv = reflect.ValueOf(f1) 212 | info.rft = reflect.TypeOf(f1) 213 | li := info.rfv.Type().NumIn() 214 | lo := info.rfv.Type().NumOut() 215 | info.inTypes = make([]reflect.Type, li) 216 | info.outTypes = make([]reflect.Type, lo) 217 | for i := 0; i < li; i++ { 218 | info.inTypes[i] = info.rfv.Type().In(i) 219 | } 220 | for i := 0; i < lo; i++ { 221 | info.outTypes[i] = info.rfv.Type().Out(i) 222 | } 223 | f = func(params ...interface{}) []interface{} { 224 | out := make([]interface{}, len(info.outTypes)) 225 | if len(params) != len(info.inTypes) { 226 | err := errors.New("The number of params is not adapted.") 227 | out[len(out)-1] = err 228 | return out 229 | } 230 | in := make([]reflect.Value, len(params)) 231 | for k, param := range params { 232 | in[k] = reflect.ValueOf(param) 233 | if info.inTypes[k].Name() != in[k].Type().Name() { 234 | err := fmt.Sprintf("the type of params is not adapted, params[%d] require type %s", 235 | k, info.inTypes[k].Name()) 236 | err = fmt.Sprintf("failed to call [%s], %s", info.rft.Name(), err) 237 | log.Println(err) 238 | out[len(out)-1] = errors.New(err) 239 | return out 240 | } 241 | } 242 | result := info.rfv.Call(in) 243 | for i := 0; i < len(result); i++ { 244 | out[i] = result[i].Interface() 245 | } 246 | 247 | return out 248 | } 249 | info.fn = f 250 | p.cache[fun] = info 251 | return f, nil 252 | } 253 | --------------------------------------------------------------------------------