feat(plugins): add debug.traceback (#6652)

Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
This commit is contained in:
nerix
2025-12-28 13:03:43 +01:00
committed by GitHub
parent a9ab584cbb
commit e2ba7dff03
7 changed files with 178 additions and 22 deletions
+1
View File
@@ -20,6 +20,7 @@
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605) - Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632) - Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632)
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625) - Minor: Added support for BetterTTV Pro subscriber badges. (#6625)
- Minor: Added `debug.traceback` for plugins. (#6652)
- Minor: Added title and duration options for `/clip` command. (#6669) - Minor: Added title and duration options for `/clip` command. (#6669)
- Minor: Added Markdown support to user notes. (#6490) - Minor: Added Markdown support to user notes. (#6490)
- Bugfix: Moderation checks now include the lead moderator badge. (#6642) - Bugfix: Moderation checks now include the lead moderator badge. (#6642)
+25
View File
@@ -850,6 +850,31 @@ and [`file:write()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:write).
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write) See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write)
### Debug API
To aid debugging, Chatterino provides Lua's `debug.traceback` function.
Other functions from the `debug` library are not exposed.
#### `traceback([thread,] [message [, level]])`
Returns a stack trace of `thread` or the current thread with an optional `message` prepended.
`level` can be used to skip some frames. By default, it's 1 (skipping the current function).
This can be used as a message handler in `xpcall`:
```lua
local function main()
c2.ThisDoesNotExistAndWillError()
end
local ok, result = xpcall(main, debug.traceback)
if not ok then
print(result)
end
```
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-debug.traceback)
### Changed globals ### Changed globals
#### `load(chunk [, chunkname [, mode [, env]]])` #### `load(chunk [, chunkname [, mode [, env]]])`
+2
View File
@@ -254,6 +254,8 @@ set(SOURCE_FILES
controllers/plugins/api/Accounts.hpp controllers/plugins/api/Accounts.hpp
controllers/plugins/api/ChannelRef.cpp controllers/plugins/api/ChannelRef.cpp
controllers/plugins/api/ChannelRef.hpp controllers/plugins/api/ChannelRef.hpp
controllers/plugins/api/DebugLibrary.cpp
controllers/plugins/api/DebugLibrary.hpp
controllers/plugins/api/EventType.hpp controllers/plugins/api/EventType.hpp
controllers/plugins/api/HTTPRequest.cpp controllers/plugins/api/HTTPRequest.cpp
controllers/plugins/api/HTTPRequest.hpp controllers/plugins/api/HTTPRequest.hpp
@@ -9,6 +9,7 @@
# include "controllers/commands/CommandController.hpp" # include "controllers/commands/CommandController.hpp"
# include "controllers/plugins/api/Accounts.hpp" # include "controllers/plugins/api/Accounts.hpp"
# include "controllers/plugins/api/ChannelRef.hpp" # include "controllers/plugins/api/ChannelRef.hpp"
# include "controllers/plugins/api/DebugLibrary.hpp"
# include "controllers/plugins/api/HTTPRequest.hpp" # include "controllers/plugins/api/HTTPRequest.hpp"
# include "controllers/plugins/api/HTTPResponse.hpp" # include "controllers/plugins/api/HTTPResponse.hpp"
# include "controllers/plugins/api/IOWrapper.hpp" # include "controllers/plugins/api/IOWrapper.hpp"
@@ -207,6 +208,13 @@ void PluginController::openLibrariesFor(Plugin *plugin)
r["_IO_input"] = sol::nil; r["_IO_input"] = sol::nil;
r["_IO_output"] = sol::nil; r["_IO_output"] = sol::nil;
} }
// set up debug lib
{
auto debuglib = lua.create_table();
g["debug"] = debuglib;
debuglib.set_function("traceback", lua::api::debugTraceback);
}
PluginController::initSol(lua, plugin); PluginController::initSol(lua, plugin);
} }
@@ -0,0 +1,46 @@
#include "controllers/plugins/api/DebugLibrary.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include <sol/sol.hpp>
namespace chatterino::lua::api {
/// Signature in Lua: ([thread,] [message [, level]])
int debugTraceback(lua_State *L)
{
int argOffset = 1;
lua_State *targetThread = L;
// If the first argument is a thread, take that as our target
if (lua_isthread(L, argOffset))
{
targetThread = lua_tothread(L, argOffset);
argOffset += 1;
}
// The message is an optional string. If it's not a string/number/nil, we
// return the message as-is and don't create a traceback.
const char *msg = lua_tostring(L, argOffset);
if (!msg && !lua_isnoneornil(L, argOffset))
{
lua_pushvalue(L, argOffset);
return 1;
}
argOffset += 1;
int defaultLevel = 0;
if (L == targetThread)
{
defaultLevel = 1;
}
int level = static_cast<int>(luaL_optinteger(L, argOffset, defaultLevel));
// push a traceback with `msg` at the start
luaL_traceback(L, targetThread, msg, level);
return 1;
}
} // namespace chatterino::lua::api
#endif
@@ -0,0 +1,14 @@
#pragma once
#ifdef CHATTERINO_HAVE_PLUGINS
# include <sol/forward.hpp>
namespace chatterino::lua::api {
/// A reimplementation of Lua's debug.traceback (ldblib.c).
/// Creates a traceback of a thread.
/// It's intended to be used as a message handler in `xpcall`.
/// See https://www.lua.org/manual/5.4/manual.html#pdf-debug.traceback
int debugTraceback(lua_State *L);
} // namespace chatterino::lua::api
#endif
+82 -22
View File
@@ -157,6 +157,24 @@ std::string luaTestPath(const QString &category, const QString &entry)
return luaTestBaseDir(category).filePath(entry + ".lua").toStdString(); return luaTestBaseDir(category).filePath(entry + ".lua").toStdString();
} }
bool runLuaTest(const QString &category, const QString &entry,
sol::state_view lua)
{
auto loadResult = lua.load_file(luaTestPath(category, entry));
auto pfn = loadResult.get<sol::protected_function>();
pfn.set_error_handler(lua["debug"]["traceback"]);
auto pfr = pfn.call();
EXPECT_TRUE(pfr.valid());
if (!pfr.valid())
{
qDebug().noquote() << "Test" << entry << "failed:";
sol::error err = pfr;
qDebug().noquote() << err.what();
return false;
}
return true;
}
} // namespace } // namespace
namespace chatterino { namespace chatterino {
@@ -1528,23 +1546,15 @@ TEST_P(PluginJsonTest, Run)
auto reg = lua->registry().size(); auto reg = lua->registry().size();
auto globals = lua->globals().size(); auto globals = lua->globals().size();
auto pfr = lua->safe_script_file(luaTestPath("json", GetParam())); runLuaTest("json", GetParam(), *this->lua);
EXPECT_TRUE(pfr.valid());
if (!pfr.valid())
{
qDebug() << "Test" << GetParam() << "failed:";
sol::error err = pfr;
qDebug() << err.what();
return;
}
for (size_t i = 0; i < 5; i++) for (size_t i = 0; i < 5; i++)
{ {
lua->collect_garbage(); lua->collect_garbage();
} }
// make sure we don't leak anything to globals or the registry // make sure we don't leak anything to globals or the registry
// but give the registry some room of 1 slot // but give the registry some room of 3 slots (two from getting the debug library)
EXPECT_LE(lua->registry().size(), reg + 1); EXPECT_LE(lua->registry().size(), reg + 3);
EXPECT_EQ(lua->globals().size(), globals); EXPECT_EQ(lua->globals().size(), globals);
} }
@@ -1557,17 +1567,8 @@ class PluginMessageTest : public PluginTest,
}; };
TEST_P(PluginMessageTest, Run) TEST_P(PluginMessageTest, Run)
{ {
configure(); this->configure();
runLuaTest("message", GetParam(), *this->lua);
auto pfr = lua->safe_script_file(luaTestPath("message", GetParam()));
EXPECT_TRUE(pfr.valid());
if (!pfr.valid())
{
qDebug() << "Test" << GetParam() << "failed:";
sol::error err = pfr;
qDebug() << err.what();
return;
}
} }
INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest, INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest,
@@ -1593,4 +1594,63 @@ TEST_F(PluginTest, testAccounts)
ASSERT_TRUE(res.valid()) << res.get<sol::error>().what(); ASSERT_TRUE(res.valid()) << res.get<sol::error>().what();
} }
TEST_F(PluginTest, debugTraceback)
{
configure();
QString traceback = lua->script(R"lua(
local function other()
error("oh no")
end
local function main()
local function inner()
other()
end
inner()
end
local ok, res = xpcall(main, debug.traceback)
return res
)lua")
.get<QString>();
ASSERT_TRUE(traceback.contains("[C]: in function 'error'"));
ASSERT_TRUE(traceback.contains("[string \"...\"]:3: in upvalue 'other'"));
ASSERT_TRUE(traceback.contains("[string \"...\"]:7: in local 'inner'"));
ASSERT_TRUE(traceback.contains(
"[string \"...\"]:9: in function <[string \"...\"]:5>"));
ASSERT_TRUE(traceback.contains("[C]: in function 'xpcall'"));
ASSERT_TRUE(traceback.contains("[string \"...\"]:12: in main chunk"));
traceback = lua->script(R"lua(
return debug.traceback()
)lua")
.get<QString>();
ASSERT_TRUE(traceback.contains("[string \"...\"]:2: in main chunk"));
traceback = lua->script(R"lua(
return debug.traceback("my message")
)lua")
.get<QString>();
ASSERT_TRUE(traceback.contains("my message"));
ASSERT_TRUE(traceback.contains("[string \"...\"]:2: in main chunk"));
traceback = lua->script(R"lua(
local coro = coroutine.create(function ()
coroutine.yield()
end)
coroutine.resume(coro)
return debug.traceback(coro)
)lua")
.get<QString>();
ASSERT_TRUE(traceback.contains("[C]: in function 'coroutine.yield'"));
ASSERT_TRUE(traceback.contains(
"[string \"...\"]:3: in function <[string \"...\"]:2>"));
auto msg = lua->script(R"lua(
return debug.traceback(c2.Message.new({id = "who would do this"}))
)lua")
.get<std::shared_ptr<Message>>();
ASSERT_EQ(msg->id, "who would do this");
}
#endif #endif