错误处理

单例类 Error 提供错误处理框架。

字段

name

说明:错误的名称,由玩家自行定义,类型为 string

message

说明:错误的详细消息,类型为 string

parameters

说明:错误的附加参数,类型为 table

注解:使用提前注册的错误处理函数进行错误处理时,parameters 中的字段将作为参数传递给对应于该名称错误的处理函数。

方法

throw

原型:Error:throw(init)

  • init:错误初始化列表,类型为 table

说明:根据 init 初始化错误信息,并将其抛出。抛出的错误对象类型为 string,且具有特定格式

注解:抛出对象为遵循某种格式规范的字符串,其中包含了 字段 中提及的所有字段内容,该字符串在通过 pcallxpcall 获取后应交由 catch 处理。

catch

原型:Error:catch(error_string)

  • error_string:错误对象,类型为 string,且具有 throw 提到的特定格式。

说明:捕获 error_string,并在解析后调用相应的错误处理函数进行处理。

注解:catch 根据错误的名称(name)来决定将该错误指派给哪个错误处理函数进行处理,并在处理时将 parameters 作为参数传递给错误处理函数。

fatal

原型:Error:fatal

说明:在发生灾难故障时,调用所有注册的灾难故障处理函数。

注解:灾难故障是指**调用 catch 捕获错误的过程中,又出现了新的错误。**具体可细分为下列情形:

  • 捕获非法的对象(catch 仅处理类型为 Error 的错误对象);
  • 捕获到的错误对象不存在对应名称的错误处理函数;
  • 在执行错误处理函数的过程中出现错误。

register_error_handler

原型:Error:register_error_handler(name, f)

  • name:错误名称,类型为 string
  • f:错误处理函数,类型为 function

说明:为错误名称 name 注册处理函数 f

注解:若捕获到 catch 无法处理的错误,或是错误处理函数在执行过程中产生新的错误,则 catch 会执行 dispose_fatal 并抛出错误。由 catch 抛出的错误旨在终止整个程序执行,原则上不应该捕获该错误。

unregister_error_handler

原型:Error:unregister_error_handler(name)

  • name:错误名称,类型为 string

说明:注销指定错误名称的处理函数。

register_fatal_disposal

原型:Error:register_fatal_disposal(f)

  • f:发生灾难故障时调用的处理函数,类型为 function

说明:注册发生灾难故障后的处理函数。

返回值:处理函数的索引,从 1 开始编号。若 f 类型非法,则返回 0

注解:此函数一般用于在发生故障后回收一些无法释放的资源(如已按下但尚未弹起的键盘按键)。

unregister_fatal_disposal

原型:Error:unregister_fatal_disposal(index)

  • index:处理函数在处理函数列表中的索引,类型为 integer

返回值:操作成功,则返回 true;否则,返回 false

示例

即时响应命令变更

执行器每隔一段时间从命令文件中取出由控制器下达的命令。有时,执行器执行某一命令(如创建新房间、批量合成配件)需要消耗较长时间,这种情况下若命令文件内容发生变更,执行器将不得不先执行完当前命令,再载入新的命令。通过 Lua 提供的异常机制,你可以通过“主动抛出错误”来销毁当前的调用栈,并将控制权交还给捕获该异常的调用方,进而达到强制改变执行流的效果。与 Runtime 提供的中断处理功能相结合,可以实现周期性读取命令文件并检查命令是否变更,并根据变更情况立即开始执行新的命令,而不必等到当前命令执行完毕才执行。

首先,我们希望在命令发生变化时,由更新命令的中断处理函数抛出一个名称为 "COMMAND_CHANGED" 的错误。为处理此错误,需要注册相应的错误处理函数:

Error:register_error_handler(
    "COMMAND_CHANGED",
    function (cmd)
        Console:information("命令变更为:%s。", cmd)
    end
)

其次,需要通过 Runtime:register_interrupt 注册一个回调函数,用于周期性读取命令,另请参阅 命令的解释与执行运行时 中的内容。当命令名称发生变更时,抛出一个名称为 "COMMAND_CHANGED" 的错误。

Runtime.last_command_update_timepoint = 0
Runtime:register_interrupt(
    function ()
        if (Runtime:get_running_time() - Runtime.last_command_update_timepoint < 100)
        then
            return
        end
        Command:update() -- 更新命令
        Runtime.last_command_update_timepoint = Runtime:get_running_time()
        -- 命令类型发生变化,需要立即停止当前执行
        if ((Command:get_status() & Command.TYPE_CHANGED) == Command.TYPE_CHANGED)
        then
            Mouse:reset()
            Keyboard:reset()
            Player:reset()
            Error:throw{
                name = "COMMAND_CHANGED",
                message = "命令变更",
                parameters = { Command:claim() }
            } -- 主动触发运行时错误
        end
    end
)

下面是一个最简的解释执行命令的函数:

local function interpret()
    local cmd = Command:claim() -- 领取命令

    -- 解释并执行命令

    Command:finish() -- 将命令标记为完成
    Runtime:sleep(1000) -- 命令完成后,进入 100 ms 的间隔,期间会由先前注册的中断回调函数检查命令更新情况
end

然后,通过下面的方式调用 interpret

while (true)
do
    local status, error = pcall(interpret)
    if (not status) -- 执行过程中发生错误
    then
        Error:catch(error)
    end
end

这样,在执行过程中若发生 "COMMAND_CHANGED" 错误,则会销毁从主函数到当前正在执行函数的调用栈,随后立即执行错误处理。错误处理完成后,重新开始解释并执行新的命令。这样,就可以实现命令变更的即时响应。

需要指出,这样的处理方法还带来了一个额外的好处。例如,在罗技提供的编程框架中,若按下一个按键,并在随后的执行过程中出现了未被处理的错误导致程序终止,该按键并不会被恢复为弹起状态,这往往会导致非常严重的后果。但是,若我们注册了针对这种灾难性错误的处理函数(比如,回弹所有已经按下但尚未弹起的按键),并在主函数体中捕获由入口函数抛出的错误(假定并未注册对这种错误的处理方式,亦或是这种错误无法被 catch 处理),则 catch 会先执行 fatal,回弹所有未弹起的按键,然后再终止程序运行。

其他细节

当用户在某个函数中使用 error 抛出一个错误,但并未使用 pcallxpcall 对其进行处理时,LGHUB Agent 将在顶层对其进行捕获,并终止程序运行。

就 Lua 规范而言,error 可以抛出任意类型的对象,即允许抛出的对象类型并不局限于 stringnumber。然而,LGHUB Agent 针对这一点的处理存在严重漏洞。例如,使用 error({ name = "ERROR"}) 抛出一个 table 类型的对象,且不对其进行任何处理,这时,LGHUB Agent 将捕获到该对象,并尝试以某种方式将其字符串化,并进行拼接操作,但由于捕获到的对象为 table,该操作会失败,进而导致 LGHUB Agent 崩溃。

需要指出,即便是在抛出的对象中提供 __tostring 并正确元表仍不能解决上述问题。在下图中,我们看到,标准的 Lua 解释器能够正确处理这一点。

与此对比的是下面的例子:

警告:该例子非常危险!

e = {
  name = "LGHUB_AGENT_FATAL_ERROR",
  message = "LGHUB Agent 灾难性故障",
  __name = "LGHUB_AGENT_FATAL_ERROR",
  __tostring = function (self)
    return self.message
  end
}
setmetatable(e, e)
error(e) -- LGHUB Agent 将捕获到此错误对象

运行上面的例子,LGHUB Agent 将崩溃,在 LGHUB 中将提示即将重新启动。但是,由于 LGHUB Agent 启动后又会再次重新运行上述代码,又会再一次崩溃,LGHUB 便陷入了无限重启 LGHUB Agent 的循环。

LGHUB Agent 崩溃
LGHUB Agent 崩溃
LGHUB 再也无法正确启动 LGHUB Agent
LGHUB 再也无法正确启动 LGHUB Agent

要解决上面的问题,需要找到 LGHUB Agent 启动后自动执行的脚本文件,它们位于 %LOCALAPPDATA%\LGHUB\scripts\ 目录下,该目录下有一些以 GUID 命名的文件夹,其中包含了名为 script.lua 的文件。找到正确的文件后,注释掉其中触发上述灾难性故障的 error(e) 并保存。然后,重启 LGHUB,LGHUB Agent 即可正常运行。

LGHUB 中导入的所有脚本
LGHUB 中导入的所有脚本
注释掉 error(e)
注释掉 error(e)

基于上述原因,Error 中提供的 throw 被设计为只抛出字符串,以避免发生上述灾难性故障。