TLBB 服务端执行 Lua 脚本出错时,luaerror.log 原本只能看到:
attempt to call a nil value
ScriptError 4:[1] ()
这类日志只能说明调用了一个空函数,却看不到函数名、脚本路径和出错行号。脚本数量较多时,几乎无法直接定位问题。
修改后,日志会变成:
error: attempt to call global `x888902_sYuanbaoShopCheckOp' (a nil value)
stack traceback:
1: function `x888902_OpenYuanbaoShop' at line 28
[file `/event/prize/yuanbaoshop.lua']
ScriptError 4:[1] (@/event/prize/yuanbaoshop.lua)
现在可以直接确认:
- 未定义的函数是
x888902_sYuanbaoShopCheckOp
- 调用者是
x888902_OpenYuanbaoShop
- 出错位置是第 28 行
- 所属脚本是
/event/prize/yuanbaoshop.lua
原因分析
问题不在 Lua 无法生成调用栈,而是原有代码把关键的信息丢掉了。
脚本加载流程大致为:
LuaInterface::LoadLua_File
-> FoxLuaScript::LoadBuffer
-> Lua_CompileBuffer
原代码编译 Lua chunk 时,把脚本名称传成了 NULL:
Lua_CompileBuffer(m_LuaState, (char*)pBuffer, dwLen, NULL);
Lua 因此不知道当前代码来自哪个文件,运行错误时自然无法输出有效的脚本路径。
另一方面,Lua 已经通过 _ERRORMESSAGE 生成了包含函数名、行号和调用栈的完整错误信息,但 _ALERT 只把它输出到了标准错误:
fputs(luaL_check_string(L, 1), stderr);
写入 luaerror.log 的反而是 lua_error() 中较早产生的简略信息。最终就只剩下一句没有上下文的 attempt to call a nil value。
一、加载脚本时保存真实路径
在 LuaInterface::LoadLua_File() 中,将脚本路径保存为 Lua chunk 名称:
CHAR chunkName[sizeof(mLua.m_szScriptName)];
memset(chunkName, 0, sizeof(chunkName));
chunkName[0] = '@';
strncpy(chunkName + 1, filename, sizeof(chunkName) - 2);
// Lua只把chunk名称用于报错定位,不影响脚本的加载和执行结果。
mLua.SetScriptName(chunkName);
BOOL bRet = mLua.LoadBuffer((BYTE*)szIn, nSize);
名称前面的 @ 是 Lua 的文件来源标记。比如:
@/event/prize/yuanbaoshop.lua
Lua 会将它识别为脚本文件,而不是一段匿名字符串。
这里使用固定长度缓冲区并预留字符串结束符,避免脚本路径过长时越界。
二、编译时把路径传给 Lua
修改 FoxLuaScript::LoadBuffer():
BOOL FoxLuaScript::LoadBuffer(PBYTE pBuffer, DWORD dwLen)
{
const char* pChunkName =
m_szScriptName[0] != '\0' ? m_szScriptName : NULL;
if (Lua_CompileBuffer(
m_LuaState,
(char*)pBuffer,
dwLen,
pChunkName) != 0)
{
ScriptError(LUA_SCRIPT_COMPILE_ERROR);
return FALSE;
}
return TRUE;
}
原来这里固定传入 NULL,即使上层知道脚本路径,Lua 调试信息中仍然没有文件来源。
改为传入 m_szScriptName 后,Lua 在编译函数时会记录:
- chunk 对应的脚本文件
- 每个函数所在的行号
- 运行错误发生时的调用关系
原有的 ScriptError() 也会自动带上脚本名称:
ScriptError 4:[1] (@/event/prize/yuanbaoshop.lua)
不需要修改它的接口。
三、保存 Lua 已生成的完整调用栈
修改 lbaselib.c 中的 _ALERT:
static int luaB__ALERT(lua_State *L)
{
const char *message = luaL_check_string(L, 1);
FILE *fp;
fputs(message, stderr);
/* 错误回调仍在Lua栈内,此处才能保留完整的函数名和行号。 */
fp = fopen("./Log/luaerror.log", "a");
if (fp != NULL)
{
fputs(message, fp);
fclose(fp);
}
return 0;
}
关键点是必须在 _ALERT 中写日志。
错误到达这里时,Lua 的 _ERRORMESSAGE 已经把原始异常和调用栈整理成完整文本,例如:
error: attempt to call global `x888902_sYuanbaoShopCheckOp' (a nil value)
stack traceback:
1: function `x888902_OpenYuanbaoShop' at line 28
[file `/event/prize/yuanbaoshop.lua']
如果在更底层过早记录,只能拿到未经整理的原始错误内容。
四、删除重复的简略日志
原来的 lua_error() 会先写一次简略错误:
FILE* fp = fopen("./Log/luaerror.log", "a");
if (fp)
{
fprintf(fp, "%s\n", s);
fclose(fp);
}
现在完整错误已经统一在 _ALERT 中写入,因此删除这段代码:
LUA_API void lua_error(lua_State *L, const char *s)
{
if (s)
message(L, s);
luaD_breakrun(L, LUA_ERRRUN);
}
否则同一个异常会同时出现简略日志和完整日志,既重复又容易干扰排查。
最终错误链路
修改后的处理流程为:
加载脚本
-> 保存真实脚本路径
-> 使用路径作为 Lua chunk 名称
-> Lua 记录函数来源和行号
-> 运行时发生异常
-> _ERRORMESSAGE 生成完整 traceback
-> _ALERT 写入 luaerror.log
-> ScriptError 补充脚本执行状态
这次修改没有改变 Lua 函数参数、返回值、执行顺序和错误恢复流程,也不需要修改任何业务脚本。它只补齐了原本已经存在、但没有正确传递和落盘的调试信息。
部署时需要成套更新相关模块:
Windows:
Server.exe
LuaLib.dll
FoxScriptEngine.dll
Linux:
Server
libLuaLib.so
libFoxScriptEngine.so
更新后,再遇到 Lua 空函数、脚本调用链错误或者运行时异常,就可以直接根据 luaerror.log 中的脚本路径、函数名和行号定位问题。