Skip to content

Instantly share code, notes, and snippets.

@fnuecke
Created January 21, 2015 20:59
Show Gist options
  • Save fnuecke/6dadb1e2598b4ff153cc to your computer and use it in GitHub Desktop.
Save fnuecke/6dadb1e2598b4ff153cc to your computer and use it in GitHub Desktop.
--[[ This script automatically saves the global environment periodically
and restores it when this script is run. Recommended usage is with
the standalone Lua interpreter like this:
lua -i autosave.lua
It was coded for and tested with Eris (https://github.com/fnuecke/eris).
This approach is quite limited and could be improved in a number of ways:
- Userdata cannot be persisted automatically, so if, for example, some
files are open the session cannot be saved / restored.
- There's the (unlikely) possibility of the script trying to save while
already doing so, if the save takes longer than `autosaveInterval`
and the garbage collector is triggered in the save logic.
- I could not figure out a way to check whether Lua is shutting down in
the autosave() function. It gets called when Lua closes, but it may
decide not to save if the last save was earlier than the set interval.
]]
-- [[ Config ]]
-- The name of the file to persist the global state to.
local stateFile = ".lua.eris"
-- The interval in seconds in which to save the global state. Avoids
-- excessive saves if the garbage collector hyperventilates.
local autosaveInterval = 10
-- [[ General persistence bootstrapping ]]
-- Stuff we cannot persist directly. Basically all of the built-in
-- C functions and userdata. We need a mapping to and from 'permanent'
-- values, that act as placeholders for these values.
local perms, uperms = {}, {}
do
local processed = {} -- list of seen tables, avoids cycles.
local function flattenAndStore(path, t)
-- Get the list of keys so we can sort it. This is needed because
-- permanent values have to be unique, so we need to pick a name for
-- them in a deterministic fashion (which pairs isn't).
local keys = {}
for k, v in pairs(t) do
if type(k) ~= "string" and type(k) ~= "number" then
io.stderr:write("Cannot generate permanent value for global with non-string key at " .. path .. ". Things may not go well.\n")
else
table.insert(keys, k)
end
end
table.sort(keys)
for _, k in ipairs(keys) do
local name = path .. "." .. k
local v = t[k]
-- This avoids duplicate permanent value entries, see above.
if perms[v] == nil then
local vt = type(v)
if "function" == vt or "userdata" == vt then
perms[v] = name
uperms[name] = v
elseif "table" == vt and not processed[v] then
processed[v] = true
flattenAndStore(name, v)
end
end
end
end
flattenAndStore("_G", _G)
end
-- [[ Autosave background task ]]
local lastSave = os.time()
local function autosave()
if os.difftime(os.time(), lastSave) > autosaveInterval then
lastSave = os.time()
local data, reason = eris.persist(perms, _ENV)
if not data then
io.stderr:write("Failed to persist environment: " .. tostring(reason) .. "\n")
else
local f, reason = io.open(stateFile, "wb")
if not f then
io.stderr:write("Failed to save environment: " .. tostring(reason) .. "\n")
else
f:write(data)
f:close()
end
end
end
-- Check again later. This basically uses the garbage collector as a
-- scheduler (this method is called when the table is collected). It
-- is a concept I first saw posted by Roberto on the list a while
-- back, in the context of sandboxing I think.
setmetatable({}, {__gc=autosave})
end
autosave()
-- [[ Startup / load autosave ]]
local f = io.open(stateFile, "rb")
if f then
-- Got a previous state, try to restore it.
local data = f:read("*a")
f:close()
local env, reason = eris.unpersist(uperms, data)
if not env then
io.stderr:write("Failed to unpersist environment: " .. tostring(reason) .. "\n")
else
for k, v in pairs(env) do
_ENV[k] = env[k]
setmetatable(_ENV, getmetatable(env))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment