Ctrl K
ComunidadGuías…

engine/plugin.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package engine

import (
	"encoding/json"
	"io"
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"
	"path/filepath"
	"strings"

	"github.com/natefinch/pie"
	"github.com/xyproto/algernon/platformdep"
	lua "github.com/xyproto/gopher-lua"
	"github.com/xyproto/vt"
)

type luaPlugin struct {
	client *rpc.Client
}

const namespace = "Lua"

func (lp *luaPlugin) LuaCode(pluginPath string) (luacode string, err error) {
	return luacode, lp.client.Call(namespace+".Code", pluginPath, &luacode)
}

func (lp *luaPlugin) LuaHelp() (luahelp string, err error) {
	return luahelp, lp.client.Call(namespace+".Help", "", &luahelp)
}

// startPlugin returns an rpc.Client for the plugin at path.
// The persistent cache is always checked first, so CallPlugin automatically
// reuses any client stored by an earlier Plugin(..., true) call.
// If keepRunning is true and no cached client exists, the new client is stored
// for reuse across requests. The second return value is true when the caller
// owns the client and must close it.
func (ac *Config) startPlugin(path string, keepRunning bool, logto io.Writer) (*rpc.Client, bool, error) {
	// Check the cache. Reading a nil map is safe in Go (returns zero value).
	ac.pluginClientsMu.Lock()
	c, ok := ac.pluginClients[path]
	ac.pluginClientsMu.Unlock()
	if ok {
		return c, false, nil
	}

	c, err := pie.StartProviderCodec(jsonrpc.NewClientCodec, logto, path)
	if err != nil {
		return nil, false, err
	}

	if keepRunning {
		ac.pluginClientsMu.Lock()
		if existing, ok := ac.pluginClients[path]; ok {
			// A concurrent goroutine already stored a client for this path.
			ac.pluginClientsMu.Unlock()
			c.Close()
			return existing, false, nil
		}
		if ac.pluginClients == nil {
			ac.pluginClients = make(map[string]*rpc.Client)
		}
		ac.pluginClients[path] = c
		ac.pluginClientsMu.Unlock()
		return c, false, nil
	}

	return c, true, nil
}

// closePluginClients closes all cached plugin clients. Called at shutdown.
func (ac *Config) closePluginClients() {
	ac.pluginClientsMu.Lock()
	defer ac.pluginClientsMu.Unlock()
	for _, c := range ac.pluginClients {
		c.Close()
	}
	ac.pluginClients = nil
}

// LoadPluginFunctions takes a Lua state and a TextOutput
// (the TextOutput struct should be nil if not in a REPL)
func (ac *Config) LoadPluginFunctions(L *lua.LState, o *vt.TextOutput) {
	// Expose the functionality of a given plugin (executable file).
	// If on Windows, ".exe" (platformdep.ExeExt) is added to the path.
	// Returns true if successful.
	L.SetGlobal("Plugin", L.NewFunction(func(L *lua.LState) int {
		path := L.ToString(1)
		givenPath := path
		path += platformdep.ExeExt
		if !ac.fs.Exists(path) {
			path = filepath.Join(ac.serverDirOrFilename, path)
		}

		keepRunning := L.GetTop() >= 2 && L.ToBool(2)

		client, owned, err := ac.startPlugin(path, keepRunning, os.Stderr)
		if err != nil {
			if o != nil {
				o.Err("[Plugin] Could not run plugin!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LBool(false))
			return 1
		}
		if owned {
			defer client.Close()
		}

		p := &luaPlugin{client}

		luacode, err := p.LuaCode(givenPath)
		if err != nil {
			if o != nil {
				o.Err("[Plugin] Could not call the LuaCode function!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LBool(false))
			return 1
		}

		luahelp, err := p.LuaHelp()
		if err != nil {
			if o != nil {
				o.Err("[Plugin] Could not call the LuaHelp function!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LBool(false))
			return 1
		}

		luacode = strings.TrimSpace(luacode)
		if err := L.DoString(luacode); err != nil {
			if o != nil {
				o.Err("[Plugin] Error in Lua code provided by plugin!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LBool(false))
			return 1
		}

		if o != nil {
			o.Println(highlight(strings.TrimSpace(luahelp)))
		}

		L.Push(lua.LBool(true))
		return 1
	}))

	// Retrieve the code from the Lua.Code function of the plugin
	L.SetGlobal("PluginCode", L.NewFunction(func(L *lua.LState) int {
		path := L.ToString(1)
		givenPath := path
		path += platformdep.ExeExt
		if !ac.fs.Exists(path) {
			path = filepath.Join(ac.serverDirOrFilename, path)
		}

		keepRunning := L.GetTop() >= 2 && L.ToBool(2)

		client, owned, err := ac.startPlugin(path, keepRunning, os.Stderr)
		if err != nil {
			if o != nil {
				o.Err("[PluginCode] Could not run plugin!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LString(""))
			return 1
		}
		if owned {
			defer client.Close()
		}

		p := &luaPlugin{client}

		luacode, err := p.LuaCode(givenPath)
		if err != nil {
			if o != nil {
				o.Err("[PluginCode] Could not call the LuaCode function!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LString(""))
			return 1
		}

		L.Push(lua.LString(luacode))
		return 1
	}))

	// Call a function exposed by a plugin (executable file).
	// Returns either an empty string (fail) or the JSON reply as a string (success).
	// Uses a persistent client if one was registered by Plugin(..., true).
	L.SetGlobal("CallPlugin", L.NewFunction(func(L *lua.LState) int {
		if L.GetTop() < 2 {
			if o != nil {
				o.Err("[CallPlugin] Needs at least 2 arguments")
			}
			L.Push(lua.LString(""))
			return 1
		}

		path := L.ToString(1)
		path += platformdep.ExeExt
		if !ac.fs.Exists(path) {
			path = filepath.Join(ac.serverDirOrFilename, path)
		}

		fn := L.ToString(2)

		var args []lua.LValue
		for i := 3; i <= L.GetTop(); i++ {
			args = append(args, L.Get(i))
		}

		logto := os.Stderr
		if o != nil {
			logto = os.Stdout
		}

		client, owned, err := ac.startPlugin(path, false, logto)
		if err != nil {
			if o != nil {
				o.Err("[CallPlugin] Could not run plugin!")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LString(""))
			return 1
		}
		if owned {
			defer client.Close()
		}

		jsonargs, err := json.Marshal(args)
		if err != nil {
			if o != nil {
				o.Err("[CallPlugin] Error when marshalling arguments to JSON")
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LString(""))
			return 1
		}

		var jsonreply []byte
		if err := client.Call(namespace+"."+fn, jsonargs, &jsonreply); err != nil {
			if o != nil {
				o.Err("[CallPlugin] Error when calling function!")
				o.Err("Function: " + namespace + "." + fn)
				o.Err("JSON Arguments: " + string(jsonargs))
				o.Err("Error: " + err.Error())
			}
			L.Push(lua.LString(""))
			return 1
		}

		L.Push(lua.LString(jsonreply))
		return 1
	}))
}