Move plugins to Sol (#5622)
Co-authored-by: Nerixyz <nerixdev@outlook.de> Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -13,16 +13,20 @@
|
||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/SolTypes.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "singletons/Paths.hpp"
|
||||
# include "singletons/Settings.hpp"
|
||||
|
||||
extern "C" {
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
# include <lualib.h>
|
||||
}
|
||||
# include <QJsonDocument>
|
||||
# include <sol/overload.hpp>
|
||||
# include <sol/sol.hpp>
|
||||
# include <sol/types.hpp>
|
||||
# include <sol/variadic_args.hpp>
|
||||
# include <sol/variadic_results.hpp>
|
||||
|
||||
# include <memory>
|
||||
# include <utility>
|
||||
@@ -113,10 +117,11 @@ bool PluginController::tryLoadFromDir(const QDir &pluginDir)
|
||||
return true;
|
||||
}
|
||||
|
||||
void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||
const QDir &pluginDir)
|
||||
void PluginController::openLibrariesFor(Plugin *plugin)
|
||||
{
|
||||
auto *L = plugin->state_;
|
||||
lua::StackGuard guard(L);
|
||||
sol::state_view lua(L);
|
||||
// Stuff to change, remove or hide behind a permission system:
|
||||
static const std::vector<luaL_Reg> loadedlibs = {
|
||||
luaL_Reg{LUA_GNAME, luaopen_base},
|
||||
@@ -124,8 +129,6 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||
|
||||
luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
||||
// luaL_Reg{LUA_IOLIBNAME, luaopen_io},
|
||||
// - explicit fs access, needs wrapper with permissions, no usage ideas yet
|
||||
// luaL_Reg{LUA_OSLIBNAME, luaopen_os},
|
||||
// - fs access
|
||||
// - environ access
|
||||
@@ -147,155 +150,100 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
||||
luaL_requiref(L, LUA_IOLIBNAME, luaopen_io, int(false));
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg c2Lib[] = {
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"register_callback", lua::api::c2_register_callback},
|
||||
{"log", lua::api::c2_log},
|
||||
{"later", lua::api::c2_later},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
lua_pushglobaltable(L);
|
||||
auto gtable = lua_gettop(L);
|
||||
|
||||
// count of elements in C2LIB + LogLevel + EventType
|
||||
auto c2libIdx = lua::pushEmptyTable(L, 8);
|
||||
|
||||
luaL_setfuncs(L, c2Lib, 0);
|
||||
|
||||
lua::pushEnumTable<lua::api::LogLevel>(L);
|
||||
lua_setfield(L, c2libIdx, "LogLevel");
|
||||
|
||||
lua::pushEnumTable<lua::api::EventType>(L);
|
||||
lua_setfield(L, c2libIdx, "EventType");
|
||||
|
||||
lua::pushEnumTable<lua::api::LPlatform>(L);
|
||||
lua_setfield(L, c2libIdx, "Platform");
|
||||
|
||||
lua::pushEnumTable<Channel::Type>(L);
|
||||
lua_setfield(L, c2libIdx, "ChannelType");
|
||||
|
||||
lua::pushEnumTable<NetworkRequestType>(L);
|
||||
lua_setfield(L, c2libIdx, "HTTPMethod");
|
||||
|
||||
// Initialize metatables for objects
|
||||
lua::api::ChannelRef::createMetatable(L);
|
||||
lua_setfield(L, c2libIdx, "Channel");
|
||||
|
||||
lua::api::HTTPRequest::createMetatable(L);
|
||||
lua_setfield(L, c2libIdx, "HTTPRequest");
|
||||
|
||||
lua::api::HTTPResponse::createMetatable(L);
|
||||
lua_setfield(L, c2libIdx, "HTTPResponse");
|
||||
|
||||
lua_setfield(L, gtable, "c2");
|
||||
auto r = lua.registry();
|
||||
auto g = lua.globals();
|
||||
auto c2 = lua.create_table();
|
||||
g["c2"] = c2;
|
||||
|
||||
// ban functions
|
||||
// Note: this might not be fully secure? some kind of metatable fuckery might come up?
|
||||
|
||||
// possibly randomize this name at runtime to prevent some attacks?
|
||||
|
||||
# ifndef NDEBUG
|
||||
lua_getfield(L, gtable, "load");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "real_load");
|
||||
lua.registry()["real_load"] = lua.globals()["load"];
|
||||
# endif
|
||||
// See chatterino::lua::api::g_load implementation
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg replacementFuncs[] = {
|
||||
{"load", lua::api::g_load},
|
||||
{"print", lua::api::g_print},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
luaL_setfuncs(L, replacementFuncs, 0);
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "loadfile");
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, gtable, "dofile");
|
||||
g["loadfile"] = sol::nil;
|
||||
g["dofile"] = sol::nil;
|
||||
|
||||
// set up package lib
|
||||
lua_getfield(L, gtable, "package");
|
||||
|
||||
auto package = lua_gettop(L);
|
||||
lua_pushstring(L, "");
|
||||
lua_setfield(L, package, "cpath");
|
||||
|
||||
// we don't use path
|
||||
lua_pushstring(L, "");
|
||||
lua_setfield(L, package, "path");
|
||||
|
||||
{
|
||||
lua_getfield(L, gtable, "table");
|
||||
auto table = lua_gettop(L);
|
||||
lua_getfield(L, -1, "remove");
|
||||
lua_remove(L, table);
|
||||
}
|
||||
auto remove = lua_gettop(L);
|
||||
auto package = g["package"];
|
||||
package["cpath"] = "";
|
||||
package["path"] = "";
|
||||
|
||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||
for (int i = 0; i < 3; i++)
|
||||
sol::protected_function tbremove = g["table"]["remove"];
|
||||
|
||||
// remove searcher_Croot, searcher_C and searcher_Lua leaving only searcher_preload
|
||||
sol::table searchers = package["searchers"];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
tbremove(searchers);
|
||||
}
|
||||
searchers.add(&lua::api::searcherRelative);
|
||||
searchers.add(&lua::api::searcherAbsolute);
|
||||
}
|
||||
// set up io lib
|
||||
{
|
||||
lua_pushvalue(L, remove);
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pcall(L, 1, 0, 0);
|
||||
auto c2io = lua.create_table();
|
||||
auto realio = r[lua::api::REG_REAL_IO_NAME];
|
||||
c2io["type"] = realio["type"];
|
||||
g["io"] = c2io;
|
||||
// prevent plugins getting direct access to realio
|
||||
r[LUA_LOADED_TABLE]["io"] = c2io;
|
||||
|
||||
// Don't give plugins the option to shit into our stdio
|
||||
r["_IO_input"] = sol::nil;
|
||||
r["_IO_output"] = sol::nil;
|
||||
}
|
||||
lua_pop(L, 1); // get rid of remove
|
||||
PluginController::initSol(lua, plugin);
|
||||
}
|
||||
|
||||
lua_getfield(L, package, "searchers");
|
||||
lua_pushcclosure(L, lua::api::searcherRelative, 0);
|
||||
lua_seti(L, -2, 2);
|
||||
// TODO: investigate if `plugin` can ever point to an invalid plugin,
|
||||
// especially in cases when the plugin is errored.
|
||||
void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
|
||||
{
|
||||
auto g = lua.globals();
|
||||
// Do not capture plugin->state_ in lambdas, this makes the functions unusable in callbacks
|
||||
g.set_function("print", &lua::api::g_print);
|
||||
g.set_function("load", &lua::api::g_load);
|
||||
|
||||
lua::push(L, QString(pluginDir.absolutePath()));
|
||||
lua_pushcclosure(L, lua::api::searcherAbsolute, 1);
|
||||
lua_seti(L, -2, 3);
|
||||
lua_pop(L, 2); // remove package, package.searchers
|
||||
sol::table c2 = g["c2"];
|
||||
c2.set_function("register_command",
|
||||
[plugin](const QString &name, sol::protected_function cb) {
|
||||
return plugin->registerCommand(name, std::move(cb));
|
||||
});
|
||||
c2.set_function("register_callback", &lua::api::c2_register_callback);
|
||||
c2.set_function("log", &lua::api::c2_log);
|
||||
c2.set_function("later", &lua::api::c2_later);
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg ioLib[] = {
|
||||
{"close", lua::api::io_close},
|
||||
{"flush", lua::api::io_flush},
|
||||
{"input", lua::api::io_input},
|
||||
{"lines", lua::api::io_lines},
|
||||
{"open", lua::api::io_open},
|
||||
{"output", lua::api::io_output},
|
||||
{"popen", lua::api::io_popen}, // stub
|
||||
{"read", lua::api::io_read},
|
||||
{"tmpfile", lua::api::io_tmpfile}, // stub
|
||||
{"write", lua::api::io_write},
|
||||
// type = realio.type
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
// TODO: io.popen stub
|
||||
auto iolibIdx = lua::pushEmptyTable(L, 1);
|
||||
luaL_setfuncs(L, ioLib, 0);
|
||||
lua::api::ChannelRef::createUserType(c2);
|
||||
lua::api::HTTPResponse::createUserType(c2);
|
||||
lua::api::HTTPRequest::createUserType(c2);
|
||||
c2["ChannelType"] = lua::createEnumTable<Channel::Type>(lua);
|
||||
c2["HTTPMethod"] = lua::createEnumTable<NetworkRequestType>(lua);
|
||||
c2["EventType"] = lua::createEnumTable<lua::api::EventType>(lua);
|
||||
c2["LogLevel"] = lua::createEnumTable<lua::api::LogLevel>(lua);
|
||||
|
||||
// set ourio.type = realio.type
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, lua::api::REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "type");
|
||||
lua_remove(L, -2); // remove realio
|
||||
lua_setfield(L, iolibIdx, "type");
|
||||
lua_pop(L, 1); // still have iolib on top of stack
|
||||
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_setfield(L, gtable, "io");
|
||||
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, lua::api::REG_C2_IO_NAME);
|
||||
|
||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE);
|
||||
lua_pushvalue(L, iolibIdx);
|
||||
lua_setfield(L, -2, "io");
|
||||
|
||||
lua_pop(L, 3); // remove gtable, iolib, LOADED
|
||||
|
||||
// Don't give plugins the option to shit into our stdio
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||
|
||||
lua_pushnil(L);
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
sol::table io = g["io"];
|
||||
io.set_function(
|
||||
"open", sol::overload(&lua::api::io_open, &lua::api::io_open_modeless));
|
||||
io.set_function("lines", sol::overload(&lua::api::io_lines,
|
||||
&lua::api::io_lines_noargs));
|
||||
io.set_function("input", sol::overload(&lua::api::io_input_argless,
|
||||
&lua::api::io_input_name,
|
||||
&lua::api::io_input_file));
|
||||
io.set_function("output", sol::overload(&lua::api::io_output_argless,
|
||||
&lua::api::io_output_name,
|
||||
&lua::api::io_output_file));
|
||||
io.set_function("close", sol::overload(&lua::api::io_close_argless,
|
||||
&lua::api::io_close_file));
|
||||
io.set_function("flush", sol::overload(&lua::api::io_flush_argless,
|
||||
&lua::api::io_flush_file));
|
||||
io.set_function("read", &lua::api::io_read);
|
||||
io.set_function("write", &lua::api::io_write);
|
||||
io.set_function("popen", &lua::api::io_popen);
|
||||
io.set_function("tmpfile", &lua::api::io_tmpfile);
|
||||
}
|
||||
|
||||
void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||
@@ -314,7 +262,7 @@ void PluginController::load(const QFileInfo &index, const QDir &pluginDir,
|
||||
<< " because safe mode is enabled.";
|
||||
return;
|
||||
}
|
||||
PluginController::openLibrariesFor(l, meta, pluginDir);
|
||||
PluginController::openLibrariesFor(temp);
|
||||
|
||||
if (!PluginController::isPluginEnabled(pluginName) ||
|
||||
!getSettings()->pluginsEnabled)
|
||||
@@ -345,17 +293,13 @@ bool PluginController::reload(const QString &id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (it->second->state_ != nullptr)
|
||||
{
|
||||
lua_close(it->second->state_);
|
||||
it->second->state_ = nullptr;
|
||||
}
|
||||
|
||||
for (const auto &[cmd, _] : it->second->ownedCommands)
|
||||
{
|
||||
getApp()->getCommands()->unregisterPluginCommand(cmd);
|
||||
}
|
||||
it->second->ownedCommands.clear();
|
||||
QDir loadDir = it->second->loadDirectory_;
|
||||
// Since Plugin owns the state, it will clean up everything related to it
|
||||
this->plugins_.erase(id);
|
||||
this->tryLoadFromDir(loadDir);
|
||||
return true;
|
||||
@@ -369,27 +313,36 @@ QString PluginController::tryExecPluginCommand(const QString &commandName,
|
||||
if (auto it = plugin->ownedCommands.find(commandName);
|
||||
it != plugin->ownedCommands.end())
|
||||
{
|
||||
const auto &funcName = it->second;
|
||||
sol::state_view lua(plugin->state_);
|
||||
sol::table args = lua.create_table_with(
|
||||
"words", ctx.words, //
|
||||
"channel", lua::api::ChannelRef(ctx.channel) //
|
||||
);
|
||||
|
||||
auto *L = plugin->state_;
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, funcName.toStdString().c_str());
|
||||
lua::push(L, ctx);
|
||||
|
||||
auto res = lua_pcall(L, 1, 0, 0);
|
||||
if (res != LUA_OK)
|
||||
auto result =
|
||||
lua::tryCall<std::optional<QString>>(it->second, args);
|
||||
if (!result)
|
||||
{
|
||||
ctx.channel->addSystemMessage("Lua error: " +
|
||||
lua::humanErrorText(L, res));
|
||||
return "";
|
||||
ctx.channel->addSystemMessage(
|
||||
QStringView(
|
||||
u"Failed to evaluate command from plugin %1: %2")
|
||||
.arg(plugin->meta.name, result.error()));
|
||||
return {};
|
||||
}
|
||||
return "";
|
||||
|
||||
auto opt = result.value();
|
||||
if (!opt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return *opt;
|
||||
}
|
||||
}
|
||||
qCCritical(chatterinoLua)
|
||||
<< "Something's seriously up, no plugin owns command" << commandName
|
||||
<< "yet a call to execute it came in";
|
||||
assert(false && "missing plugin command owner");
|
||||
return "";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool PluginController::isPluginEnabled(const QString &id)
|
||||
@@ -435,32 +388,31 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
|
||||
continue;
|
||||
}
|
||||
|
||||
lua::StackGuard guard(pl->state_);
|
||||
|
||||
auto opt = pl->getCompletionCallback();
|
||||
if (opt)
|
||||
{
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Processing custom completions from plugin" << name;
|
||||
auto &cb = *opt;
|
||||
auto errOrList = cb(lua::api::CompletionEvent{
|
||||
.query = query,
|
||||
.full_text_content = fullTextContent,
|
||||
.cursor_position = cursorPosition,
|
||||
.is_first_word = isFirstWord,
|
||||
});
|
||||
if (std::holds_alternative<int>(errOrList))
|
||||
sol::state_view view(pl->state_);
|
||||
auto errOrList = lua::tryCall<sol::table>(
|
||||
cb,
|
||||
toTable(pl->state_, lua::api::CompletionEvent{
|
||||
.query = query,
|
||||
.full_text_content = fullTextContent,
|
||||
.cursor_position = cursorPosition,
|
||||
.is_first_word = isFirstWord,
|
||||
}));
|
||||
if (!errOrList.has_value())
|
||||
{
|
||||
guard.handled();
|
||||
int err = std::get<int>(errOrList);
|
||||
qCDebug(chatterinoLua)
|
||||
<< "Got error from plugin " << pl->meta.name
|
||||
<< " while refreshing tab completion: "
|
||||
<< lua::humanErrorText(pl->state_, err);
|
||||
<< errOrList.get_unexpected().error();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto list = std::get<lua::api::CompletionList>(errOrList);
|
||||
auto list = lua::api::CompletionList(*errOrList);
|
||||
if (list.hideOthers)
|
||||
{
|
||||
results = QStringList(list.values.begin(), list.values.end());
|
||||
|
||||
Reference in New Issue
Block a user