├── 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 |