Lua 非侵入式热补丁系统开发与使用手册

Lua 非侵入式热补丁系统开发与使用手册

1 文档概述

1.1 适用场景

本系统运行在 ibmc 闭源二进制 SDK + Lua/Skynet 环境中,无法修改闭源二进制文件,由闭源底座提供补丁加载入口,开源社区提供补丁框架与修复脚本,实现无侵入运行时热修复。

1.2 能力范围

支持三类热修复能力:

  • 文件整体替换(package):劫持 Lua 模块加载,完全替换闭源文件
  • 公开函数重写(public_fun):对模块导出的公开函数做包装 / 修复
  • 闭包私有函数重写(private_func):通过 Lua debug 库修复闭包内未导出的私有函数

1.3 角色与分工

归属 文件 职责
闭源 SDK 内部 main.lua 补丁统一入口,安全捕获异常,启动补丁加载流程
开源社区 fix.lua 补丁核心调度框架,实现三种修复逻辑、目录扫描、补丁分发
开源社区 patch_x.lua 业务补丁脚本,按规范编写修复逻辑

2 整体目录结构

项目根目录/
└── src/
    └── service/
        ├── fix.lua          # 社区补丁核心框架
        ├── patch_a.lua      # 文件全覆盖补丁示例
        ├── patch_b.lua      # 公开函数修复补丁示例
        ├── patch_c.lua      # 私有闭包函数修复补丁示例
        └── time_manager.lua # 模块替换场景对应的修复模块(可选)

强制规则

  1. 所有补丁脚本必须放置在 src/service/ 目录下
  2. 补丁文件名必须以 patch_ 开头,后缀为 .lua
  3. 仅闭源底座可修改 main.lua,社区禁止改动入口文件

3 闭源 SDK 侧:补丁入口 main.lua

3.1 代码实现

该文件部署在闭源 SDK 内部,负责安全拉起整个补丁系统,使用 pcall 做异常隔离,补丁报错不会导致主程序崩溃。

local log = require 'mc.logging'

-- 异常捕获:补丁加载失败不影响主业务
local ok, err = pcall(function()
  local fix = require 'service.fix'
  -- 传入业务标识 bmc_time,用于日志区分
  fix.load_all_patches('bmc_time')
end)

if not ok then
  log:error('load patches failed, err:%s', err)
end

注意

  • 必须写到main.lua最前面,提前加载补丁生效

3.2 执行逻辑

  1. 通过 pcall 安全执行补丁加载逻辑
  2. 加载社区核心框架 service.fix
  3. 调用统一入口 load_all_patches,传入业务名称
  4. 加载异常时打印错误日志,主程序继续运行

4 开源社区侧:核心框架 fix.lua

目录结构:在社区manifest添加

4.1 代码实现

社区统一提供的补丁调度核心,负责目录遍历、补丁解析、三类修复逻辑实现。

local log = require "mc.logging"
local utils_core = require 'utils.core'

local fix = {}

-- 类型1:模块整体覆盖(package.preload 劫持)
function fix.package(patch, app_name)
    local mod_name = patch.target
    package.preload[mod_name] = patch.func

    log:notice('[%s]load package %s successfully', app_name, mod_name)
end

-- 类型2:公开导出函数修复/包装
function fix.public_fun(patch, app_name)
    local mod_name, func_name = patch.target:match("^(.+)%.(.+)$")
    local mod = require(mod_name)
    local old_func = mod[func_name]
    -- 重写函数,原生函数作为最后一个参数传入
    mod[func_name] = function(...)
        return patch.func(..., old_func)
    end

    log:notice('[%s]load public fun %s successfully', app_name, patch.target)
end

-- 类型3:闭包内私有函数修复(基于 debug 库)
function fix.private_func(patch, app_name)
    local mod_name, entry_func = patch.target:match("^(.+)%.(.+)$")
    local private_name = patch.private
    local mod = require(mod_name)
    local entry = mod[entry_func]

    -- 遍历 upvalue 查找私有函数
    for i = 1, 1000 do
        local name, _ = debug.getupvalue(entry, i)
        if not name then break end
        if name == private_name then
            debug.setupvalue(entry, i, patch.func)
            log:notice('[%s]load private fun %s successfully', app_name, private_name)
            break
        end
    end
end

-- 统一入口:扫描并加载所有补丁
function fix.load_all_patches(app_name)
    log:notice('[%s]load all patches start', app_name)
    local patch_dir = "service"

    -- 拼接补丁目录绝对路径
    local dir = utils_core.getcwd() .. '/src/service'
    local files = utils_core.dir(dir)
    if #files == 0 then
        log:notice('[%s]resume file not exist, no upgrade required', app_name)
        return
    end

    -- 遍历目录下所有 lua 文件
    for _, file in pairs(files) do
        if file:match("%.lua$") then
            local path = patch_dir .. "." .. file:gsub("%.lua$", "")
            -- 只处理 patch_ 开头的补丁文件
            if not string.find(file, 'patch_') then
                goto continue
            end

            -- 加载补丁配置表
            local patch = require(path)
            if type(patch) ~= "table" then
                goto continue
            end

            -- 根据补丁类型分发执行
            if patch.fix_type == 'package' then
                fix.package(patch, app_name)
            elseif patch.fix_type == 'public_fun' then
                fix.public_fun(patch, app_name)
            elseif patch.fix_type == 'private_func' then
                fix.private_func(patch, app_name)
            end
        end
        ::continue::
    end

    log:notice('[%s]load all patches end', app_name)
end

return fix

4.2 核心流程

  1. 读取 src/service 目录下所有文件
  2. 过滤出 patch_ 开头的 Lua 补丁文件
  3. 加载补丁配置表,校验格式
  4. 根据 fix_type 调用对应修复逻辑
  5. 打印加载日志,标记补丁执行状态

5 开源社区侧:三类补丁开发规范与示例

所有补丁文件统一返回一张配置表,框架根据字段自动识别修复类型。

5.1 类型一:package 模块全覆盖补丁

适用场景
闭源模块整体逻辑异常、功能缺失,需要完全替换整个模块。
补丁文件:patch_a.lua

local log = require 'mc.logging'

return {
    -- 固定类型标识
    fix_type = 'package',
    -- 目标:需要劫持的闭源模块名(与 require 名称一致)
    target = "time_manager",
    -- 劫持函数:返回社区修复后的完整模块
    func = function(...)
        log:info("package preload 模块劫持成功")
        -- 加载社区编写的修复模块
        return require("service.time_manager")
    end
}

配套说明
需在 service 目录下提供完整修复模块 time_manager.lua。

5.2 类型二:public_fun 公开函数修复补丁

适用场景
模块整体正常,仅单个导出函数存在 Bug、参数异常、逻辑缺失,推荐优先使用该类型(最稳健)。
补丁文件:patch_b.lua

local log = require "mc.logging"
local time_intf = require 'time_intf'

return {
    -- 固定类型标识
    fix_type = "public_fun",
    -- 目标格式:模块名.函数名
    target = "time_manager.initialize",
    -- 修复逻辑:最后一个参数为原生旧函数
    func = function(self, old_func)
        self.mdb_time.DateTime = time_intf.get_date_time()
        if self.mdb_time.DateTimeLocalOffset == '+08:00' then
            self:set_timezone('UTC+08:00')
        end
    end
}

5.3 类型三:private_func 私有闭包函数修复

适用场景
Bug 函数为闭包内局部私有函数,未导出到模块表,常规方式无法访问,依赖 Lua debug 库修改 upvalue。
补丁文件:patch_c.lua

local log = require 'mc.logging'

return {
    -- 固定类型标识
    fix_type = 'private_func',
    -- 目标:模块.公开入口函数(私有函数所在的外层导出函数)
    target = "c.get",
    -- 闭包内私有函数名称
    private = "set",
    -- 修复后的私有函数逻辑
    func = function(...)
        log:notice('fix private func')
        -- 此处编写私有函数修复逻辑
    end
}

6 完整加载流程

  1. 闭源 SDK 启动,执行内部 main.lua
  2. main.lua 安全加载 service/fix.lua 框架
  3. 框架扫描 src/service/ 目录所有文件
  4. 筛选 patch_ 开头的补丁脚本并加载
  5. 按 fix_type 执行对应修复逻辑
  6. 输出日志,补丁生效,业务正常运行

7 日志验证与问题排查

7.1 正常加载日志示例

[bmc_time]load all patches start
[bmc_time]load package time_manager successfully
[bmc_time]load public fun time_manager.initialize successfully
[bmc_time]load private fun set successfully
[bmc_time]load all patches end

7.2 常见问题排查

  1. 补丁未加载
  • 检查文件是否放在 src/service/
  • 检查文件名是否以 patch_ 开头
  1. 模块劫持失败
  • 核对 target 模块名与业务中 require 名称完全一致
  1. 私有函数不生效
  • 核对外层入口函数、私有函数名拼写
  • SDK 版本升级后闭包变量名可能变更,需重新适配
  1. 补丁报错
  • 查看 main.lua 捕获的 error 日志,定位代码异常

8 选型建议与使用约束

8.1 补丁类型优先级(推荐)

public_fun(公开函数修复) > package(模块替换) > private_func(私有闭包修复)

8.2 使用约束

  1. 禁止修改闭源侧 main.lua 入口文件
  2. 所有补丁必须增加日志,方便线上问题定位
  3. 私有函数补丁强依赖闭包变量名,SDK 升级后务必回归测试
  4. 模块替换补丁需完整实现原模块所有接口,避免功能缺失
  5. 所有补丁仅部署在开源社区目录,不触碰二进制文件

请问这个补丁系统的使用场景是什么,毕竟开发者又看不到SDK源码,又怎么打补丁做修复呢

场景就是社区出现闭源sdk问题进行修复,由hw人员进行修复

伙伴是不能做相关开发的么?比如我要复写某个闭源的ipmi命令实现

可以复写,主要是你们看不到内部代码结构,如果可以看到的话就没问题