package gui // This is based off of the excellent example and documentation here: // https://github.com/vladimirvivien/go-plugin-example // There truly are great people in this world. // It's a pleasure to be here with all of you import ( "embed" "errors" "os" "path/filepath" "plugin" "go.wit.com/log" "go.wit.com/widget" ) var err error type Symbol any type aplug struct { count int name string filename string plug *plugin.Plugin // set this to true if the plugin dies // TODO: remove the plugin from the pool dead bool // this tells the toolkit plugin how to send events // back here // // This is how we are passed information like a user clicking a button // or a user changing a dropdown menu or a checkbox // // From this channel, the information is then passed into the main program // Custom() function // Callback func(chan widget.Action) // This is how actions are sent to the toolkit. // For example: // If a program is using GTK, when a program tries to make a new button // "Open GIMP", then it would pass an action via this channel into the toolkit // plugin and the toolkit plugin would add a button to the parent widget // // each toolkit has it's own goroutine and each one is sent this // add button request // pluginChan chan widget.Action PluginChannel func() chan widget.Action frozenChan chan widget.Action FrozenChannel func() chan widget.Action } var allPlugins []*aplug func (n *Node) LoadToolkitNew(name string) error { log.Log(PLUG, "LoadToolkit() START for name =", name) plug := initPlugin(name) if plug == nil { return errors.New("initPlugin(" + name + ") failed") } plug.dead = false log.Log(PLUG, "LoadToolkit() sending Toolkit Init action to the plugin channel") var a widget.Action a.ActionType = widget.ToolkitInit plug.pluginChan <- a // sleep(.5) // temp hack until chan communication is setup // TODO: find a new way to do this that is locking, safe and accurate me.rootNode.redraw(plug) log.Log(PLUG, "LoadToolkit() END for name =", name) return nil } func (n *Node) LoadToolkit(name string) (*Node, error) { log.Log(PLUG, "LoadToolkit() START for name =", name) plug := initPlugin(name) if plug == nil { return n, errors.New("initPlugin(" + name + ") failed") } plug.dead = false log.Log(PLUG, "LoadToolkit() sending Toolkit Init action to the plugin channel") var a widget.Action a.ActionType = widget.ToolkitInit plug.pluginChan <- a // sleep(.5) // temp hack until chan communication is setup // TODO: find a new way to do this that is locking, safe and accurate me.rootNode.redraw(plug) log.Log(PLUG, "LoadToolkit() END for name =", name) return n, nil } // loads and initializes a toolkit (andlabs/ui, gocui, etc) // attempts to locate the .so file func initPlugin(name string) *aplug { log.Log(PLUG, "initPlugin() START") for _, aplug := range allPlugins { log.Log(PLUG, "initPlugin() already loaded toolkit plugin =", aplug.name) if aplug.name == name { log.Log(WARN, "initPlugin() SKIPPING", name, "as you can't load it twice") return nil } } return searchPaths(name) } // newPlug.PluginChannel = getPluginChannel(newPlug, "PluginChannel") func getPluginChannel(p *aplug, funcName string) func() chan widget.Action { var newfunc func() chan widget.Action var ok bool var test plugin.Symbol test, err = p.plug.Lookup(funcName) if err != nil { log.Error(err, "DID NOT FIND: name =", test) return nil } newfunc, ok = test.(func() chan widget.Action) if !ok { log.Log(PLUG, "function name =", funcName, "names didn't map correctly. Fix the plugin name =", p.name) return nil } return newfunc } func sendCallback(p *aplug, funcName string) func(chan widget.Action) { var newfunc func(chan widget.Action) var ok bool var test plugin.Symbol test, err = p.plug.Lookup(funcName) if err != nil { log.Log(WARN, "sendCallback() err =", err) log.Log(WARN, "sendCallback() DID NOT FIND: name =", funcName) return nil } newfunc, ok = test.(func(chan widget.Action)) if !ok { log.Log(PLUG, "function name =", funcName, "names didn't map correctly. Fix the plugin name =", p.name) return nil } return newfunc } /* This searches in the following order for the plugin .so files: /usr/lib/go-gui-toolkits/ ~/go/src/go.wit.com/toolkits/ /usr/lib/local/go-gui-toolkits/ TODO: plugin.Open() seem to always fail after the first attempt fails */ func searchPaths(name string) *aplug { var filename string var pfile []byte var err error var p *aplug // try the filename from the command line first if argGui.GuiFile != "" { p = initToolkit(name, argGui.GuiFile) if p != nil { log.Log(NOW, "gui.Init() loaded ok!", argGui.GuiFile) return p } } // check for custom toolkit builds. This looks: // TODO: use forge to find the "go.work" dir // TODO: fix GO as language to support building plugins homeDir, err := os.UserHomeDir() if err != nil { log.Error(err, "os.UserHomeDir() error", err) } else { // first look in the toolkit build directory filename = homeDir + "/go/src/go.wit.com/toolkits/" + name + "/" + name + ".so" p = initToolkit(name, filename) if p != nil { log.Log(NOW, "gui.Init() loaded", filename, "ok!") return p } // this is the "default" location when built and installed locally filename = homeDir + "/go/lib/go-gui/" + name + ".so" p = initToolkit(name, filename) if p != nil { log.Log(NOW, "gui.Init() loaded", filename, "ok!") return p } } // try /usr/local/ filename = "/usr/local/lib/go-gui/" + name + ".so" p = initToolkit(name, filename) if p != nil { log.Log(NOW, "gui.Init() loaded", filename, "ok!") return p } // try to load the embedded plugin file // TODO: fix the filename used in /tmp/ resname := filepath.Join("resources/", name+".so") pfile, err = me.resFS.ReadFile(resname) if err == nil { tmpname := filepath.Join("/tmp/", name+".so") log.Log(WARN, "searchPaths() using toolkit embedded in executable") log.Log(WARN, "searchPaths() resource file", tmpname, resname, len(pfile)) f, _ := os.OpenFile(tmpname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) f.Write(pfile) f.Close() p := initToolkit(name, tmpname) if p != nil { log.Log(NOW, "gui.Init() loaded", resname, "ok!") return p } } else { log.Log(INFO, "searchPaths()", resname, "was not embedded in the binary") } // attempt to write out the file from the internal resource // is this the same as above. todo: sort this out. filename = "resources/" + name + ".so" p = initToolkit(name, filename) if p != nil { log.Log(NOW, "gui.Init() loaded", filename, "ok!") return p } // This is the 'default' location // if you are a linux distribution package maintainer, please put the tookits here filename = "/usr/lib/go-gui-toolkits/" + name + ".so" p = initToolkit(name, filename) if p != nil { log.Log(NOW, "gui.Init() loaded", filename, "ok!") return p } return nil } // load module // 1. open the shared object file to load the symbols func initToolkit(name string, filename string) *aplug { // smart plugin test. remove all other code besides this if err := checkPluginViaSubprocess(filename); err != nil { // log.Printf("initToolkit() subprocess load plugin failed: %v\n", err) return nil } // the plugin will probably work. show the banner colorBanner() plug, err := checkPlug(filename) if err != nil { // turn on PLUG debugging if something goes wrong PLUG.SetBool(true) log.Log(PLUG, "plugin.Open() err =", err) log.Log(PLUG, "initToolkit() FAILED =", filename) return nil } log.Log(PLUG, "initToolkit() SUCCESS loading plugin =", filename) var newPlug *aplug newPlug = new(aplug) newPlug.name = name newPlug.filename = filename newPlug.plug = plug // this tells the toolkit plugin how to send user events back to us // for things like: the user clicked on the 'Check IPv6' newPlug.Callback = sendCallback(newPlug, "Callback") // this let's us know where to send requests to the toolkit // for things like: add a new button called 'Check IPv6' newPlug.PluginChannel = getPluginChannel(newPlug, "PluginChannel") // add it to the list of plugins allPlugins = append(allPlugins, newPlug) // set the communication to the plugins newPlug.pluginChan = newPlug.PluginChannel() if newPlug.pluginChan == nil { log.Log(WARN, "initToolkit() ERROR PluginChannel() returned nil for plugin:", newPlug.name, filename) return nil } newPlug.Callback(me.guiChan) // test to see if this can be used to make fyne work newPlug.FrozenChannel = getPluginChannel(newPlug, "FrozenChannel") // set the communication to the plugins newPlug.frozenChan = newPlug.FrozenChannel() if newPlug.frozenChan == nil { log.Log(WARN, "initToolkit() ERROR FrozenChannel() returned nil for plugin:", newPlug.name) } log.Log(PLUG, "initToolkit() END", newPlug.name, filename) return newPlug } func (n *Node) InitEmbed(resFS embed.FS) *Node { me.resFS = resFS return n } func (n *Node) LoadToolkitEmbed(name string, b []byte) *Node { for _, aplug := range allPlugins { log.Log(PLUG, "LoadToolkitEmbed() already loaded toolkit plugin =", aplug.name) if aplug.name == name { log.Log(NOW, "LoadToolkitEmbed() SKIPPING", name, "as you can't load it twice") return n } } f, err := os.CreateTemp("", "sample."+name+".so") if err != nil { log.Error(err, "LoadToolkitEmbed() SKIPPING", name, "as you can't load it twice") return n } defer os.Remove(f.Name()) f.Write(b) p := initToolkit(name, f.Name()) if p == nil { log.Log(WARN, "LoadToolkitEmbed() embedded go file failed", name) } return n } func (n *Node) ListToolkits() { for _, aplug := range allPlugins { log.Log(WARN, "ListToolkits() has plugin =", aplug.name, "i =", aplug.count) } } // cleanly close all toolkits func (n *Node) Close() { Exit() } func Exit() { log.Log(PLUG, "Closing all toolkits") for _, plug := range allPlugins { log.Log(PLUG, "Exit() found plugin", plug.name) var a widget.Action a.ActionType = widget.ToolkitClose plug.pluginChan <- a } allPlugins = nil } func (n *Node) CloseToolkit(name string) bool { log.Log(PLUG, "CloseToolkit() for name =", name) for i, plug := range allPlugins { log.Log(PLUG, "CloseToolkit() found", plug.name) if plug.name == name { log.Log(PLUG, "CloseToolkit() sending close", name) var a widget.Action a.ActionType = widget.ToolkitClose plug.pluginChan <- a allPlugins = append(allPlugins[:i], allPlugins[i+1:]...) return true } } return false }