├── helper ├── hotfix_module_names.lua ├── test.lua ├── main.lua ├── README.md └── hotfix_helper.lua ├── .gitignore ├── rockspecs ├── hotfix-scm-1.rockspec └── hotfix-0.1-1.rockspec ├── lua └── hotfix │ ├── hotfix.lua │ └── internal │ ├── functions_replacer.lua │ └── module_updater.lua ├── README.md ├── test └── main.lua └── LICENSE /helper/hotfix_module_names.lua: -------------------------------------------------------------------------------- 1 | -- Module names need hotfix. 2 | -- hotfix_helper.lua will reload this module in check(). 3 | -- So it can be changed dynamically. 4 | 5 | local hotfix_module_names = { 6 | "test", 7 | } 8 | 9 | return hotfix_module_names 10 | -------------------------------------------------------------------------------- /helper/test.lua: -------------------------------------------------------------------------------- 1 | local test = {} 2 | test.count = 0 3 | count = 0 4 | local d_count = 0 5 | function test.func() 6 | count = count + 1 7 | d_count = d_count + 2 8 | test.count = test.count + 3 9 | print("test", count, d_count, test.count) 10 | return true 11 | end 12 | return test 13 | --[[ 14 | ]] 15 | -------------------------------------------------------------------------------- /helper/main.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local hotfix_helper = require("hotfix_helper") 4 | local test = require("test") 5 | 6 | local function sleep(sec) 7 | local end_time = os.time() + sec 8 | while os.time() < end_time do end 9 | end -- sleep() 10 | 11 | function M.run() 12 | hotfix_helper.init() 13 | while true do 14 | test.func() 15 | sleep(2) 16 | hotfix_helper.check() 17 | end 18 | end -- run() 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | /test/log.txt 43 | /test/test.lua 44 | -------------------------------------------------------------------------------- /rockspecs/hotfix-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotfix" 2 | version = "scm-1" 3 | source = { 4 | url = "git://github.com/jinq0123/hotfix", 5 | } 6 | description = { 7 | summary = "Lua 5.2/5.3 hotfix. Hot update functions and keep old data.", 8 | homepage = "https://github.com/jinq0123/hotfix", 9 | license = "Apache License 2.0", 10 | 11 | detailed = [[ 12 | hotfix reloads the module and updates the old module, keeping the old data. 13 | 14 | Usage: 15 | 16 | local hotfix = require("hotfix") 17 | hotfix.hotfix_module("mymodule.sub_module") 18 | 19 | The module is reloaded and the returned value is updated to package.loaded[module_name]. 20 | 21 | Functons are updated to new ones but old upvalues are kept. 22 | Old tables are kept and new fields are inserted. 23 | All references to old functions are replaced to new ones. 24 | ]], 25 | } 26 | 27 | dependencies = { 28 | "lua >= 5.2", 29 | } 30 | 31 | build = { 32 | type = "builtin", 33 | modules = { 34 | ["hotfix.hotfix"] = "lua/hotfix/hotfix.lua", 35 | ["hotfix.internal.functions_replacer"] = "lua/hotfix/internal/functions_replacer.lua", 36 | ["hotfix.internal.module_updater"] = "lua/hotfix/internal/module_updater.lua", 37 | }, 38 | copy_directories = { 39 | "helper", 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /rockspecs/hotfix-0.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotfix" 2 | version = "0.1-1" 3 | source = { 4 | url = "git://github.com/jinq0123/hotfix", 5 | tag = "v0.1", 6 | } 7 | description = { 8 | summary = "Lua 5.2/5.3 hotfix. Hot update functions and keep old data.", 9 | homepage = "https://github.com/jinq0123/hotfix", 10 | license = "Apache License 2.0", 11 | 12 | detailed = [[ 13 | hotfix reloads the module and updates the old module, keeping the old data. 14 | 15 | Usage: 16 | 17 | local hotfix = require("hotfix") 18 | hotfix.hotfix_module("mymodule.sub_module") 19 | 20 | The module is reloaded and the returned value is updated to package.loaded[module_name]. 21 | 22 | Functons are updated to new ones but old upvalues are kept. 23 | Old tables are kept and new fields are inserted. 24 | All references to old functions are replaced to new ones. 25 | ]], 26 | } 27 | 28 | dependencies = { 29 | "lua >= 5.2", 30 | } 31 | 32 | build = { 33 | type = "builtin", 34 | modules = { 35 | ["hotfix.hotfix"] = "lua/hotfix/hotfix.lua", 36 | ["hotfix.internal.functions_replacer"] = "lua/hotfix/internal/functions_replacer.lua", 37 | ["hotfix.internal.module_updater"] = "lua/hotfix/internal/module_updater.lua", 38 | }, 39 | copy_directories = { 40 | "helper", 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /helper/README.md: -------------------------------------------------------------------------------- 1 | # hotfix helper 2 | Example to use hotfix. 3 | 4 | hotfix_helper uses lfs to record file time and hotfix modified modules in check(). 5 | hotfix_helper reloads hotfix_module_names to get module names, 6 | which can add or remove dynamically. 7 | 8 | How to run 9 | ----------- 10 | Need lfs (https://github.com/keplerproject/luafilesystem) 11 | 12 |
13 | E:\Git\Lua\hotfix\helper>lua53pp.exe
14 | Lua 5.3.2  Copyright (C) 1994-2015 Lua.org, PUC-Rio
15 | > package.path = "../lua/?.lua;" .. package.path
16 | > require("main").run()
17 | test    1       2       3
18 | Hot fix module test (E:\Git\Lua\hotfix\helper\test.lua)
19 | Hot fix module: test
20 | Update test: new(table: 00552758) old(table: 00561CC0)
21 |   Update func: new(function: 005527A8) old(function: 00561D38)
22 |     Update _ENV: new(table: 0054F208) old(table: 0054F208)
23 |       Same
24 |     setupvalue d_count: (0) -> (2)
25 |     Update test: new(table: 00552758) old(table: 00561CC0)
26 |       Already updated
27 | test    1       4       6
28 | test    2       6       9
29 | test    3       8       12
30 | test    4       10      15
31 | Hot fix module test (E:\Git\Lua\hotfix\helper\test.lua)
32 | Hot fix module: test
33 | Update test: new(table: 0056C6D8) old(table: 00561CC0)
34 |   Update func: new(function: 005714A8) old(function: 005527A8)
35 |     Update _ENV: new(table: 0054F208) old(table: 0054F208)
36 |       Same
37 |     setupvalue d_count: (0) -> (10)
38 |     Update test: new(table: 0056C6D8) old(table: 00561CC0)
39 |       Already updated
40 | test XXX        1       12      18
41 | test XXX        2       14      21
42 | test XXX        3       16      24
43 | 
44 | -------------------------------------------------------------------------------- /helper/hotfix_helper.lua: -------------------------------------------------------------------------------- 1 | --- Hotfix helper which hotfixes modified modules. 2 | -- Using lfs to detect files' modification. 3 | 4 | local M = { } 5 | 6 | local lfs = require("lfs") 7 | local hotfix = require("hotfix.hotfix") 8 | 9 | -- Map file path to file time to detect modification. 10 | local path_to_time = { } 11 | 12 | -- global_objects which must not hotfix. 13 | local global_objects = { 14 | arg, 15 | assert, 16 | bit32, 17 | collectgarbage, 18 | coroutine, 19 | debug, 20 | dofile, 21 | error, 22 | getmetatable, 23 | io, 24 | ipairs, 25 | lfs, 26 | load, 27 | loadfile, 28 | loadstring, 29 | math, 30 | module, 31 | next, 32 | os, 33 | package, 34 | pairs, 35 | pcall, 36 | print, 37 | rawequal, 38 | rawget, 39 | rawlen, 40 | rawset, 41 | require, 42 | select, 43 | setmetatable, 44 | string, 45 | table, 46 | tonumber, 47 | tostring, 48 | type, 49 | unpack, 50 | utf8, 51 | xpcall, 52 | } 53 | 54 | --- Check modules and hotfix. 55 | function M.check() 56 | local MOD_NAME = "hotfix_module_names" 57 | if not package.searchpath(MOD_NAME, package.path) then return end 58 | package.loaded[MOD_NAME] = nil -- always reload it 59 | local module_names = require(MOD_NAME) 60 | 61 | for _, module_name in pairs(module_names) do 62 | local path, err = package.searchpath(module_name, package.path) 63 | -- Skip non-exist module. 64 | if not path then 65 | print(string.format("No such module: %s. %s", module_name, err)) 66 | goto continue 67 | end 68 | 69 | local file_time = lfs.attributes (path, "modification") 70 | if file_time == path_to_time[path] then goto continue end 71 | 72 | print(string.format("Hot fix module %s (%s)", module_name, path)) 73 | path_to_time[path] = file_time 74 | hotfix.hotfix_module(module_name) 75 | ::continue:: 76 | end -- for 77 | end -- check() 78 | 79 | function M.init() 80 | hotfix.log_debug = function(s) print(s) end 81 | hotfix.add_protect(global_objects) 82 | end 83 | 84 | return M 85 | -------------------------------------------------------------------------------- /lua/hotfix/hotfix.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Lua 5.2/5.3 hotfix. Hot update functions and keep old data. 3 | Author: Jin Qing ( http://blog.csdn.net/jq0123 ) 4 | --]] 5 | 6 | local M = {} 7 | 8 | local module_updater = require("hotfix.internal.module_updater") 9 | local functions_replacer = require("hotfix.internal.functions_replacer") 10 | 11 | -- Do not update and replace protected objects. 12 | local protected = {} 13 | 14 | -- To protect self. 15 | local function add_self_to_protect() 16 | M.add_protect{ 17 | M, 18 | M.hotfix_module, 19 | M.log_error, 20 | M.log_info, 21 | M.log_debug, 22 | M.add_protect, 23 | M.remove_protect, 24 | module_updater, 25 | module_updater.log_debug, 26 | module_updater.update_loaded_module, 27 | functions_replacer, 28 | functions_replacer.replace_all, 29 | } 30 | end -- add_self_to_protect 31 | 32 | -- Hotfix module with new module object. 33 | -- Update package.loaded[module_name] and replace all functions. 34 | -- module_obj is the newly loaded module object. 35 | local function hotfix_module_with_obj(module_name, module_obj) 36 | assert("string" == type(module_name)) 37 | add_self_to_protect() 38 | module_updater.log_debug = M.log_debug 39 | 40 | -- Step 1: Update package.loaded[module_name], recording updated functions. 41 | local updated_function_map = module_updater.update_loaded_module( 42 | module_name, protected, module_obj) 43 | -- Step 2: Replace old functions with new ones in module_obj, _G and registry. 44 | functions_replacer.replace_all(protected, updated_function_map, module_obj) 45 | end -- hotfix_module_with_obj() 46 | 47 | -- Hotfix module. 48 | -- Skip unloaded module. 49 | -- Usage: hotfix_module("mymodule.sub_module") 50 | -- Returns package.loaded[module_name]. 51 | function M.hotfix_module(module_name) 52 | assert("string" == type(module_name)) 53 | if not package.loaded[module_name] then 54 | M.log_debug("Skip unloaded module: " .. module_name) 55 | return package.loaded[module_name] 56 | end 57 | M.log_debug("Hot fix module: " .. module_name) 58 | 59 | local file_path = assert(package.searchpath(module_name, package.path)) 60 | local fp = assert(io.open(file_path)) 61 | local chunk = fp:read("*all") 62 | fp:close() 63 | 64 | -- Load chunk. 65 | local func = assert(load(chunk, '@'..file_path)) 66 | local ok, obj = assert(pcall(func)) 67 | if nil == obj then obj = true end -- obj may be false 68 | 69 | hotfix_module_with_obj(module_name, obj) 70 | return package.loaded[module_name] 71 | end 72 | 73 | -- User can set log functions. Default is no log. 74 | -- Like: require("hotfix").log_info = function(s) mylog:info(s) end 75 | function M.log_error(msg_str) end 76 | function M.log_info(msg_str) end 77 | function M.log_debug(msg_str) end 78 | 79 | -- Add objects to protect. 80 | -- Example: add_protect({table, math, print}) 81 | function M.add_protect(object_array) 82 | for _, obj in pairs(object_array) do 83 | protected[obj] = true 84 | end 85 | end -- add_protect() 86 | 87 | -- Remove objects in protected set. 88 | -- Example: remove_protect({table, math, print}) 89 | function M.remove_protect(object_array) 90 | for _, obj in pairs(object_array) do 91 | protected[obj] = nil 92 | end 93 | end -- remove_protect() 94 | 95 | return M 96 | -------------------------------------------------------------------------------- /lua/hotfix/internal/functions_replacer.lua: -------------------------------------------------------------------------------- 1 | --- Replace functions of table or upvalue. 2 | -- Search for the old functions and replace them with new ones. 3 | 4 | local M = {} 5 | 6 | -- Objects whose functions have been replaced already. 7 | -- Each objects need to be replaced once. 8 | local replaced_obj = {} 9 | 10 | -- Map old functions to new functions. 11 | -- Used to replace functions finally. 12 | -- Set to hotfix.updated_func_map. 13 | local updated_func_map = {} 14 | 15 | -- Do not update and replace protected objects. 16 | -- Set to hotfix.protected. 17 | local protected = {} 18 | 19 | local replace_functions -- forward declare 20 | 21 | -- Replace all updated functions in upvalues of function object. 22 | local function replace_functions_in_upvalues(function_object) 23 | local obj = function_object 24 | assert("function" == type(obj)) 25 | assert(not protected[obj]) 26 | assert(obj ~= updated_func_map) 27 | 28 | for i = 1, math.huge do 29 | local name, value = debug.getupvalue(obj, i) 30 | if not name then return end 31 | local new_func = updated_func_map[value] 32 | if new_func then 33 | assert("function" == type(value)) 34 | debug.setupvalue(obj, i, new_func) 35 | else 36 | replace_functions(value) 37 | end 38 | end -- for 39 | assert(false, "Can not reach here!") 40 | end -- replace_functions_in_upvalues() 41 | 42 | -- Replace all updated functions in the table. 43 | local function replace_functions_in_table(table_object) 44 | local obj = table_object 45 | assert("table" == type(obj)) 46 | assert(not protected[obj]) 47 | assert(obj ~= updated_func_map) 48 | 49 | replace_functions(debug.getmetatable(obj)) 50 | local new = {} -- to assign new fields 51 | for k, v in pairs(obj) do 52 | local new_k = updated_func_map[k] 53 | local new_v = updated_func_map[v] 54 | if new_k then 55 | obj[k] = nil -- delete field 56 | new[new_k] = new_v or v 57 | else 58 | obj[k] = new_v or v 59 | replace_functions(k) 60 | end 61 | if not new_v then replace_functions(v) end 62 | end -- for k, v 63 | for k, v in pairs(new) do obj[k] = v end 64 | end -- replace_functions_in_table() 65 | 66 | -- Replace all updated functions. 67 | -- Record all replaced objects in replaced_obj. 68 | function replace_functions(obj) 69 | if protected[obj] then return end 70 | local obj_type = type(obj) 71 | if "function" ~= obj_type and "table" ~= obj_type then return end 72 | if replaced_obj[obj] then return end 73 | replaced_obj[obj] = true 74 | assert(obj ~= updated_func_map) 75 | 76 | if "function" == obj_type then 77 | replace_functions_in_upvalues(obj) 78 | else -- table 79 | replace_functions_in_table(obj) 80 | end 81 | end -- replace_functions(obj) 82 | 83 | --- Replace all old functions with new ones. 84 | -- Replace in new_obj, _G and debug.getregistry(). 85 | -- a_protected is a list of protected object. 86 | -- an_updated_func_map is a map from old function to new function. 87 | -- new_obj is the newly loaded module. 88 | function M.replace_all(a_protected, an_updated_func_map, new_obj) 89 | protected = a_protected 90 | updated_func_map = an_updated_func_map 91 | assert(type(protected) == "table") 92 | assert(type(updated_func_map) == "table") 93 | if nil == next(updated_func_map) then 94 | return 95 | end 96 | 97 | replaced_obj = {} 98 | replace_functions(new_obj) -- new_obj may be not in _G 99 | replace_functions(_G) 100 | replace_functions(debug.getregistry()) 101 | replaced_obj = {} 102 | end -- M.replace_all() 103 | 104 | return M 105 | -------------------------------------------------------------------------------- /lua/hotfix/internal/module_updater.lua: -------------------------------------------------------------------------------- 1 | --- Updater to update loaded module. 2 | -- Updating a table is to update metatable and sub-table of the old table, 3 | -- and to update functions of the old table, keeping value fields. 4 | -- Updating a function is to copy upvalues of old function to new function. 5 | -- Functions will be replaced later after updating. 6 | 7 | local M = {} 8 | 9 | local update_table 10 | local update_func 11 | 12 | -- Updated signature set to prevent self-reference dead loop. 13 | local updated_sig = {} 14 | 15 | -- Map old function to new functions. 16 | local updated_func_map = {} 17 | 18 | -- Do not update and replace protected objects. 19 | -- Set to hotfix.protected. 20 | local protected = {} 21 | 22 | -- Set to hotfix.log_debug. 23 | function M.log_debug(msg_str) end 24 | 25 | -- Check if function or table has been updated. Return true if updated. 26 | local function check_updated(new_obj, old_obj, name, deep) 27 | local signature = string.format("new(%s) old(%s)", 28 | tostring(new_obj), tostring(old_obj)) 29 | M.log_debug(string.format("%sUpdate %s: %s", deep, name, signature)) 30 | 31 | if new_obj == old_obj then 32 | M.log_debug(deep .. " Same") 33 | return true 34 | end 35 | if updated_sig[signature] then 36 | M.log_debug(deep .. " Already updated") 37 | return true 38 | end 39 | updated_sig[signature] = true 40 | return false 41 | end 42 | 43 | -- Update new function with upvalues of old function. 44 | -- Parameter name and deep are only for log. 45 | function update_func(new_func, old_func, name, deep) 46 | assert("function" == type(new_func)) 47 | assert("function" == type(old_func)) 48 | if protected[old_func] then return end 49 | if check_updated(new_func, old_func, name, deep) then return end 50 | deep = deep .. " " 51 | updated_func_map[old_func] = new_func 52 | 53 | -- Get upvalues of old function. 54 | local old_upvalue_map = {} 55 | for i = 1, math.huge do 56 | local name, value = debug.getupvalue(old_func, i) 57 | if not name then break end 58 | old_upvalue_map[name] = value 59 | end 60 | 61 | local function log_dbg(name, from, to) 62 | M.log_debug(string.format("%ssetupvalue %s: (%s) -> (%s)", 63 | deep, name, tostring(from), tostring(to))) 64 | end 65 | 66 | -- Update new upvalues with old. 67 | for i = 1, math.huge do 68 | local name, value = debug.getupvalue(new_func, i) 69 | if not name then break end 70 | local old_value = old_upvalue_map[name] 71 | if old_value then 72 | local type_old_value = type(old_value) 73 | if type_old_value ~= type(value) then 74 | debug.setupvalue(new_func, i, old_value) 75 | log_dbg(name, value, old_value) 76 | elseif type_old_value == "function" then 77 | update_func(value, old_value, name, deep) 78 | elseif type_old_value == "table" then 79 | update_table(value, old_value, name, deep) 80 | debug.setupvalue(new_func, i, old_value) 81 | else 82 | debug.setupvalue(new_func, i, old_value) 83 | log_dbg(name, value, old_value) 84 | end 85 | end -- if old_value 86 | end -- for i 87 | end -- update_func() 88 | 89 | -- Compare 2 tables and update the old table. Keep the old data. 90 | function update_table(new_table, old_table, name, deep) 91 | assert("table" == type(new_table)) 92 | assert("table" == type(old_table)) 93 | if protected[old_table] then return end 94 | if check_updated(new_table, old_table, name, deep) then return end 95 | deep = deep .. " " 96 | 97 | -- Compare 2 tables, and update old table. 98 | for key, value in pairs(new_table) do 99 | local old_value = old_table[key] 100 | local type_value = type(value) 101 | if type_value ~= type(old_value) then 102 | old_table[key] = value 103 | M.log_debug(string.format("%sUpdate field %s: (%s) -> (%s)", 104 | deep, key, tostring(old_value), tostring(value))) 105 | elseif type_value == "function" then 106 | update_func(value, old_value, key, deep) 107 | elseif type_value == "table" then 108 | update_table(value, old_value, key, deep) 109 | end 110 | end -- for 111 | 112 | -- Update metatable. 113 | local old_meta = debug.getmetatable(old_table) 114 | local new_meta = debug.getmetatable(new_table) 115 | if type(old_meta) == "table" and type(new_meta) == "table" then 116 | update_table(new_meta, old_meta, name.."'s Meta", deep) 117 | end 118 | end -- update_table() 119 | 120 | -- Update new loaded object with package.loaded[module_name]. 121 | local function update_loaded_module2(module_name, new_obj) 122 | assert(nil ~= new_obj) 123 | assert("string" == type(module_name)) 124 | local old_obj = package.loaded[module_name] 125 | local new_type = type(new_obj) 126 | local old_type = type(old_obj) 127 | if new_type == old_type then 128 | if "table" == new_type then 129 | update_table(new_obj, old_obj, module_name, "") 130 | return 131 | end 132 | if "function" == new_type then 133 | update_func(new_obj, old_obj, module_name, "") 134 | return; 135 | end 136 | end -- if new_type == old_type 137 | M.log_debug(string.format("Directly replace module: old(%s) -> new(%s)", 138 | tostring(old_obj), tostring(new_obj))) 139 | package.loaded[module_name] = new_obj 140 | end -- update_loaded_module2() 141 | 142 | -- Update new loaded object with package.loaded[module_name]. 143 | -- Return an updated function map (updated_func_map). 144 | -- new_module_obj is the newly loaded module object. 145 | function M.update_loaded_module(module_name, protected_objects, new_module_obj) 146 | assert(type(module_name) == "string") 147 | assert(type(protected_objects) == "table") 148 | 149 | protected = protected_objects 150 | updated_func_map = {} 151 | updated_sig = {} 152 | update_loaded_module2(module_name, new_module_obj) 153 | updated_sig = {} 154 | return updated_func_map 155 | end -- update_loaded_module() 156 | 157 | return M 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hotfix 2 | Lua 5.2/5.3 hotfix. Hot update functions and keep old data. 3 | 4 | What does hotfix do 5 | --------------------- 6 | If we have a test.lua 7 | ```lua 8 | local M = {} 9 | 10 | M.count = 0 11 | 12 | function M.func() 13 | M.count = M.count + 1 14 | return "v1" 15 | end 16 | 17 | return M 18 | ``` 19 | 20 | Require test and call func(), then count will be 1. 21 | ``` 22 | > test = require("test") 23 | > test.func() 24 | v1 25 | > test.count 26 | 1 27 | ``` 28 | 29 | Change "v1" to "v2" in test.lua, then hotfix module test and call func() again. 30 | The result shows that func() has been updated, but the count is kept. 31 | ``` 32 | > hotfix = require("hotfix.hotfix") 33 | > hotfix.hotfix_module("test") 34 | table: 0000000002752060 35 | > test.func() 36 | v2 37 | > test.count 38 | 2 39 | ``` 40 | 41 | Install 42 | ------- 43 | Using [LuaRocks](https://luarocks.org): 44 | ``` 45 | luarocks install hotfix 46 | ``` 47 | 48 | Or manually copy `lua/hotfix` directory into your Lua module path. 49 | 50 | Usage 51 | ----- 52 | ```lua 53 | local hotfix = require("hotfix.hotfix") 54 | hotfix.hotfix_module("mymodule.sub_module") 55 | ``` 56 | 57 | [`helper/hotfix_helper.lua`](helper/hotfix_helper.lua) 58 | is an example to hotfix modified modules using `lfs`. 59 | Please see [helper/README.md](helper/README.md). 60 | 61 | `hotfix_module(module_name)` 62 | --------------------------- 63 | `hotfix_module()` uses `package.searchpath(module_name, package.path)` 64 | to search the path of module. 65 | The module is reloaded and the returned value is updated to `package.loaded[module_name]`. 66 | If the returned value is `nil`, then `package.loaded[module_name]` is assigned to `true`. 67 | `hotfix_module()` returns the final value of `package.loaded[module_name]`. 68 | 69 | `hotfix_module()` will skip unloaded module to avoid unexpected loading, 70 | and also to work around the issue of 71 | ["Three dots module name will be nil"](https://github.com/jinq0123/hotfix#three-dots-module-name-will-be-nil). 72 | 73 | Functons are updated to new ones but old upvalues are kept. 74 | Old tables are kept and new fields are inserted. 75 | All references to old functions are replaced to new ones. 76 | 77 | The module may change any global variables if it wants to. 78 | See ["Why not protect the global variables" below](#why-not-protect-the-global-variables). 79 | 80 | Local variable which is not referenced by `_G` is not updated. 81 | ```lua 82 | -- test.lua: return { function func() return "old" end } 83 | local test = require("test") -- referenced by _G.package.loaded["test"] 84 | local func = test.func -- is not upvalue nor is referenced by _G 85 | -- test.lua: return { function func() return "new" end } 86 | require("hotfix.hotfix").hotfix_module("test") 87 | test.func() -- "new" 88 | func() -- "old" 89 | ``` 90 | 91 | Why not protect the global variables 92 | ------------------------------------- 93 | We can protect the global variables on loading in some ways, but there are other problems. 94 | 95 | * [1] uses a read only `ENV` to load. 96 | ```lua 97 | local env = {} 98 | setmetatable(env, { __index = _G }) 99 | load(chunk, check_name, 't', env) 100 | ``` 101 | 102 | But it can not stop indirect write. 103 | Global variables may be changed. 104 | In the following example, `t` is OK but `math.sin` is changed. 105 | 106 |
107 | Lua 5.3.2  Copyright (C) 1994-2015 Lua.org, PUC-Rio
108 | > math.sin(123)
109 | -0.45990349068959
110 | > do
111 | >> local _ENV = setmetatable({}, {__index = _G})
112 | >> t = 123
113 | >> math.sin = print
114 | >> end
115 | > t
116 | nil
117 | > math.sin(123)
118 | 123
119 | 
120 | 121 | * [2] uses a fake `ENV` to load and ignores all operations. 122 | In this case, we can not init new local variables. 123 | ```lua 124 | local M = {} 125 | + local log = require("log") -- Can not require! 126 | function M.foo() 127 | + log.info("test") 128 | end 129 | return M 130 | ``` 131 | 132 | Another problem is the new function's `_ENV` is not the real `ENV`. 133 | Following test will fail because `set_global()` has a protected `ENV`. 134 | ```lua 135 | log("New upvalue which is a function set global...") 136 | run_test([[ 137 | local M = {} 138 | function M.foo() return 12345 end 139 | return M 140 | ]], 141 | function() assert(nil == global_test) end, 142 | [[ 143 | local M = {} 144 | local function set_global() global_test = 11111 end 145 | function M.foo() 146 | set_global() 147 | end 148 | return M 149 | ]], 150 | function() 151 | assert(nil == test.foo()) 152 | assert(11111 == global_test) -- FAIL! 153 | global_test = nil 154 | end) 155 | ``` 156 | 157 | How to run test 158 | ------------------ 159 | Run `main.lua` in test dir. 160 | `main.lua` will write a `test.lua` file and hotfix it. 161 | `main.lua` will write log to `log.txt`. 162 |
163 | D:\Jinq\Git\hotfix\test>..\..\..\tools\lua-5.3.2_Win64_bin\lua53
164 | Lua 5.3.2  Copyright (C) 1994-2015 Lua.org, PUC-Rio
165 | > require("main").run()
166 | main.lua:80: assertion failed!
167 | 
168 | 169 | Unexpected update 170 | ------------------- 171 | `log` function is changed from `print` to an empty function. 172 | The hotfix will replace all `print` to an empty function which is totally unexpected. 173 | ```lua 174 | local M = {} 175 | local log = print 176 | function M.foo() log("Old") end 177 | return M 178 | ``` 179 | ```lua 180 | local M = {} 181 | local log = function() end 182 | function M.foo() log("Old") end 183 | return M 184 | ``` 185 | `hotfix.add_protect{print}` can protect `print` function from being replaced. 186 | But it also means that `log` can not be updated. 187 | 188 | Known issue 189 | -------------- 190 | ### Can not load utf8 with BOM. 191 | ``` 192 | hotfix.lua:210: file.lua:1: unexpected symbol near '<\239>' 193 | ``` 194 | ### Three dots module name will be `nil`. 195 | ```lua 196 | --- test.lua. 197 | -- @module test 198 | local module_name = ... 199 | print(module_name) 200 | ``` 201 | `require("test")` will print "test", but hotfix which uses `load()` will print "nil". 202 | 203 | Reference 204 | --------- 205 | * [1] hotfix by tickbh 206 |
https://github.com/tickbh/td_rlua/blob/11523931b0dd271ad4c5e9c532a9d3bae252a264/td_rlua/src/hotfix.rs 207 |
http://www.cnblogs.com/tickbh/articles/5459120.html (In Chinese) 208 |
Lua 5.2/5.3. 209 | 210 | Can only update global functions. 211 | 212 | ```lua 213 | local M = {} 214 | + function M.foo() end -- Can not add M.foo(). 215 | return M 216 | ``` 217 | 218 | * [2] lua_hotupdate 219 |
https://github.com/asqbtcupid/lua_hotupdate 220 |
Lua 5.1. 221 | 222 | Using a fake `ENV`, the module's init statements result in noop. 223 | -------------------------------------------------------------------------------- /test/main.lua: -------------------------------------------------------------------------------- 1 | package.path = "../lua/?.lua;" .. package.path 2 | 3 | local M = {} 4 | 5 | local hotfix = require("hotfix.hotfix") 6 | local test -- = require("test") 7 | local tmp = {} -- to store temp data 8 | 9 | local function log(msg) 10 | local f = assert(io.open("log.txt", "a+")) 11 | f:write(msg.."\n") 12 | assert(f:close()) 13 | end -- log 14 | 15 | local function write_test_lua(s) 16 | local f = assert(io.open("test.lua", "w")) 17 | f:write(s) 18 | assert(f:close()) 19 | end -- write_test_lua() 20 | 21 | local function run_testX(old, prepare, new, check) 22 | log("Skip.") 23 | end 24 | 25 | local function run_test(old, prepare, new, check) 26 | assert("string" == type(old)) 27 | assert(not prepare or "function" == type(prepare)) 28 | assert("string" == type(new)) 29 | assert(not check or "function" == type(check)) 30 | 31 | -- Require old code and prepare. 32 | write_test_lua(old); 33 | package.loaded["test"] = nil 34 | test = require("test") 35 | if prepare then prepare() end 36 | 37 | -- Hot fix and check. 38 | write_test_lua(new) 39 | test = hotfix.hotfix_module("test") 40 | if check then check() end 41 | end -- run_test() 42 | 43 | function M.run() 44 | 45 | hotfix.log_error = log 46 | hotfix.log_info = log 47 | hotfix.log_debug = log 48 | 49 | log("--------------------") 50 | 51 | log("Test changing global function...") 52 | run_test([[ 53 | local a = "old" 54 | function g_get_a() 55 | return a 56 | end 57 | ]], nil, [[ 58 | local a = "new" 59 | function g_get_a() 60 | return a 61 | end 62 | ]], 63 | function() 64 | -- Global function is reset directly. 65 | assert("new" == g_get_a()) 66 | g_get_a = nil 67 | end) 68 | 69 | log("Test keeping upvalue data...") 70 | run_test([[ 71 | local a = "old" 72 | return function() return a end 73 | ]], nil, [[ 74 | local a = "new" 75 | return function() return a .. "_x" end 76 | ]], 77 | function() 78 | -- Old upvalue is kept. 79 | assert("old_x" == test()) 80 | end) 81 | 82 | log("Test adding functions...") 83 | run_test([[ 84 | local M = {} 85 | return M 86 | ]], nil, [[ 87 | local M = {} 88 | function g_foo() return 123 end 89 | function M.foo() return 1234 end 90 | return M 91 | ]], 92 | function() 93 | assert(123 == g_foo()) 94 | assert(1234 == test.foo()) 95 | g_foo = nil 96 | end) 97 | 98 | log("Hot fix function module...") -- Module returns a function. 99 | run_test( 100 | "return function() return 12345 end", 101 | function() tmp.f = test end, 102 | "return function() return 56789 end", 103 | function() 104 | assert(56789 == test()) 105 | assert(56789 == tmp.f()) 106 | end) 107 | 108 | log("Test upvalue self-reference...") 109 | local code = [[ 110 | local fun_a, fun_b 111 | function fun_a() return fun_b() end 112 | function fun_b() return fun_a() end 113 | return fun_b 114 | ]] 115 | run_test(code, nil, code, nil) -- no dead loop 116 | 117 | log("Test function table...") 118 | run_test([[ 119 | local M = {} 120 | function M.foo() return 12345 end 121 | return M 122 | ]], 123 | function() tmp.foo = test.foo end, 124 | [[ 125 | local M = {} 126 | function M.foo() return 67890 end 127 | return M 128 | ]], 129 | function() 130 | assert(67890 == test.foo()) 131 | assert(67890 == tmp.foo()) 132 | end) 133 | 134 | log("New upvalue which is a function set global...") 135 | run_test([[ 136 | local M = {} 137 | function M.foo() return 12345 end 138 | return M 139 | ]], 140 | function() assert(nil == global_test) end, 141 | [[ 142 | local M = {} 143 | local function set_global() global_test = 11111 end 144 | function M.foo() 145 | set_global() 146 | end 147 | return M 148 | ]], 149 | function() 150 | assert(nil == test.foo()) 151 | assert(11111 == global_test) 152 | global_test = nil 153 | end) 154 | 155 | log("Test table key...") 156 | run_test([[ 157 | local M = {} 158 | M[print] = function() return "old" end 159 | M[M] = "old" 160 | return M 161 | ]], 162 | function() assert(nil == global_test) end, 163 | [[ 164 | local M = {} 165 | M[print] = function() return "new" end 166 | M[M] = "new" 167 | return M 168 | ]], 169 | function() 170 | assert("new" == test[print]()) 171 | assert("old" == test[test]) 172 | end) 173 | 174 | log("Test table.fuction.table.function...") 175 | run_test([[ 176 | local M = {} 177 | local t = { tf = function() end } 178 | function M.mf() t.t = 123 end 179 | return M 180 | ]], nil, [[ 181 | local M = {} 182 | local t = { tf = function() end } 183 | function M.mf() t.test = 123 end 184 | return M 185 | ]], nil) 186 | 187 | log("Test same upvalue (issue #1) ...") 188 | run_test([[ 189 | local M = {} 190 | local l = {} 191 | 192 | function M.func1() 193 | end 194 | function M.func2() 195 | l[10] = 10 196 | return l 197 | end 198 | 199 | return M 200 | ]], 201 | function() assert(test.func2()[10] == 10) end, 202 | [[ 203 | local M = {} 204 | local l = {} 205 | 206 | function M.func1() 207 | l[10] = 10 208 | return l 209 | end 210 | function M.func2() 211 | l[10] = 10 212 | return l 213 | end 214 | 215 | return M 216 | ]], 217 | function() 218 | assert(tostring(test.func1()) == tostring(test.func2())) 219 | end) 220 | 221 | log("Test upvalue (issue #3) ...") 222 | run_test([[ 223 | local M = {} 224 | local t = {} 225 | 226 | function M.hello() return "hello" end 227 | 228 | t.hello = M.hello 229 | 230 | function M.func() 231 | return t.hello() 232 | end 233 | 234 | return M 235 | ]], 236 | function() assert(test.func() == "hello") end, 237 | [[ 238 | local M = {} 239 | local t = {} 240 | 241 | function M.hello() return "hello2" end 242 | 243 | t.hello = M.hello 244 | 245 | function M.func() 246 | return t.hello() 247 | end 248 | 249 | return M 250 | ]], 251 | function() 252 | assert(test.func() == "hello2") 253 | end) 254 | 255 | log("Test module returns false...") 256 | run_test([[ 257 | local M = {} 258 | return false 259 | ]], nil, 260 | [[ 261 | local M = {} 262 | return M 263 | ]], 264 | function() 265 | -- Because module is considered unloaded, and will not hotfix. 266 | assert(test == false) 267 | end) 268 | 269 | log("Test three dots module name...") 270 | run_test([[ 271 | local M = {} 272 | M.module_name = ... 273 | return M 274 | ]], 275 | function() assert(test.module_name == "test") end, 276 | [[ 277 | local M = {} 278 | M.module_name2 = ... 279 | return M 280 | ]], 281 | function() 282 | assert(test.module_name == "test") 283 | -- See https://github.com/jinq0123/hotfix#three-dots-module-name-will-be-nil 284 | -- assert(test.module_name2 == "test") 285 | assert(test.module_name2 == nil) 286 | end) 287 | 288 | -- Todo: Test metatable update 289 | -- Todo: Test registry update 290 | 291 | log("Test OK!") 292 | print("Test OK!") 293 | 294 | end -- run() 295 | 296 | return M 297 | 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------