I've written a script to hot-reload already require
ed modules. It work s only partially however...
My approach to this task is quite simple. I changed Lua's require
function so that it remembers modules that it loaded together with a timestamp and its file path. Then I use a shell script to observe the modification time of those files and re-require them if they changed. I simply dofile()
and if no errors happen, I take the return value and (re-)assign it at package.loaded[<module>]
. So far so good.
All of this works perfect when I use global variables, e.g. foo = require "foobar"
, but when I use local assignments, like local foo = require "foobar"
, my hotswapper failes (partially)!
It seems that the package gets swapped out like intended, however the local variable (from the assignment above) still holds an old reference or the old value that it got when require was called the first time.
My idea was to use Lua's debug.getlocal
and debug.setlocal
functions to find all local variables (upvalues in stack) and update their values/references.
BUT I get an error that the upvalue I want to change is "out of range"... Could somebody help me please? What should I do or how could I work around this?
The complete code is over at Gist, the important/relevant snippets however are...
local_upvalues()
at line 27, which collects all available upvalueslocal function local_upvalues()
local upvalues = {}
local failures = 0
local thread = 0
while true do
thread = thread + 1
local index = 0
while true do
index = index + 1
local success, name, value = pcall(debug.getlocal, thread, index)
if success and name ~= nil then
table.insert(upvalues, {
name = name,
value = value,
thread = thread,
index = index
})
else
if index == 1 then failures = failures + 1 end
break
end
end
if failures > 1 then break end
end
return upvalues
end
debug.setlocal()
at line 89, which tries to update the upvalue that holds the absolete module reference -- update module references of local upvalues
for count, upvalue in ipairs(local_upvalues()) do
if upvalue.value == package.loaded[resource] then
-- print(upvalue.name, "updated from", upvalue.value, "to", message)
table.foreach(debug.getinfo(1), print)
print(upvalue.name, upvalue.thread, upvalue.index)
debug.setlocal(upvalue.thread, upvalue.index, message)
end
end
package.loaded[resource] = message -- update the absolete module
You can use a metatable with __index
. Rather then returning package.loaded[resource]
or _require(resource)
return:
_require(resource)
return setmetatable({}, --create a dummy table
{
__index = function(_, k)
return package.loaded[resource][k] -- pass all index requests to the real resource.
end
})
And
package.loaded[resource] = message -- update the absolete module
print(string.format("%s %s hot-swap of module '%s'",
os.date("%d.%m.%Y %H:%M:%S"),
stateless and "stateless" or "stateful",
hotswap.registry[resource].url
))
return setmetatable({},
{
__index = function(_, k)
return package.loaded[resource][k]
end
})
Doing this you shouldn't need to look up the upvalues at all, as this will force any local
require results to always reference the up-to-date resource.
There are likely cases where this will not work well, or otherwise break a module, but with some tweaking it can.