Allows admins to run code snippets without crashing the server as often.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

core.lua 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. --
  2. -- Minetest snippets mod: Attempt to prevent snippets from crashing the server
  3. --
  4. -- Make loadstring a local variable
  5. local loadstring
  6. if minetest.global_exists('loadstring') then
  7. loadstring = _G.loadstring
  8. else
  9. loadstring = assert(load)
  10. end
  11. local copy = table.copy
  12. local safe_funcs = {}
  13. local orig_funcs, running_snippet
  14. function snippets.get_current_snippet()
  15. return running_snippet
  16. end
  17. -- Apply "safe functions": These wrap normal registration functions so that
  18. -- snippets can't crash them as easily.
  19. local function apply_safe_funcs()
  20. if orig_funcs then return end
  21. orig_funcs = {}
  22. for k, v in pairs(safe_funcs) do
  23. if k ~= 'print' then
  24. orig_funcs[k] = minetest[k]
  25. minetest[k] = v
  26. end
  27. end
  28. orig_funcs.print, print = print, safe_funcs.print
  29. end
  30. local function remove_safe_funcs()
  31. if not orig_funcs then return end
  32. for k, v in pairs(orig_funcs) do
  33. minetest[k] = orig_funcs[k]
  34. end
  35. print = orig_funcs.print
  36. orig_funcs = nil
  37. end
  38. -- "Break out" of wrapped functions.
  39. local function wrap_unsafe(func)
  40. return function(...)
  41. if orig_funcs then
  42. remove_safe_funcs()
  43. local res = {func(...)}
  44. apply_safe_funcs()
  45. return (table.unpack or unpack)(res)
  46. else
  47. return func(...)
  48. end
  49. end
  50. end
  51. -- Logging
  52. snippets.registered_on_log = {}
  53. snippets.log_levels = {}
  54. function snippets.log_levels.error(n)
  55. return minetest.colorize('red', n)
  56. end
  57. function snippets.log_levels.warning(n)
  58. return minetest.colorize('yellow', n)
  59. end
  60. function snippets.log_levels.info(n)
  61. return n
  62. end
  63. snippets.log_levels.none = snippets.log_levels.info
  64. function snippets.log_levels.debug(n)
  65. return minetest.colorize('grey', n)
  66. end
  67. function snippets.log(level, msg)
  68. local snippet = running_snippet or 'snippets:anonymous'
  69. if msg == nil then level, msg = 'none', level end
  70. level, msg = tostring(level), tostring(msg)
  71. if level == 'warn' then
  72. level = 'warning'
  73. elseif not snippets.log_levels[level] then
  74. level = 'none'
  75. end
  76. for _, func in ipairs(snippets.registered_on_log) do
  77. if func(snippet, level, msg) then return end
  78. end
  79. end
  80. snippets.log = wrap_unsafe(snippets.log)
  81. function snippets.register_on_log(func)
  82. assert(type(func) == 'function')
  83. table.insert(snippets.registered_on_log, 1, func)
  84. end
  85. -- Create the default log action
  86. -- Only notify the player of errors or warnings
  87. snippets.register_on_log(function(snippet, level, msg)
  88. local rawmsg
  89. if level == 'warning' then
  90. rawmsg = 'Warning'
  91. elseif level == 'error' then
  92. rawmsg = 'Error'
  93. else
  94. return
  95. end
  96. rawmsg = snippets.log_levels[level](rawmsg .. ' in snippet "' .. snippet ..
  97. '": ' .. msg)
  98. local def = snippets.registered_snippets[snippet]
  99. if def and def.owner then
  100. minetest.chat_send_player(def.owner, rawmsg)
  101. else
  102. minetest.chat_send_all(rawmsg)
  103. end
  104. end)
  105. -- Create a safe print()
  106. function safe_funcs.print(...)
  107. local msg = ''
  108. for i = 1, select('#', ...) do
  109. if i > 1 then msg = msg .. '\t' end
  110. msg = msg .. tostring(select(i, ...))
  111. end
  112. snippets.log('none', msg)
  113. end
  114. -- Mostly copied from https://stackoverflow.com/a/26367080
  115. local function wrap_raw(snippet, func, ...)
  116. local old_running = running_snippet
  117. running_snippet = snippet
  118. local use_safe_funcs = not orig_funcs
  119. if use_safe_funcs then apply_safe_funcs() end
  120. local good, msg = pcall(func, ...)
  121. if use_safe_funcs then remove_safe_funcs() end
  122. if good then
  123. running_snippet = old_running
  124. return msg
  125. else
  126. snippets.log('error', msg)
  127. running_snippet = old_running
  128. end
  129. end
  130. local function wrap(snippet, func)
  131. if not snippet then return func end
  132. return function(...) return wrap_raw(snippet, func, ...) end
  133. end
  134. -- Expose the above function to the API.
  135. -- This will only wrap functions if called from inside a snippet.
  136. function snippets.wrap_callback(func)
  137. return wrap(running_snippet, func)
  138. end
  139. do
  140. local after_ = minetest.after
  141. function safe_funcs.after(after, func, ...)
  142. after = tonumber(after)
  143. assert(after and after == after, 'Invalid core.ater invocation')
  144. after_(after, wrap_raw, running_snippet, func, ...)
  145. end
  146. function snippets.wrap_register_on(orig)
  147. return function(func, ...)
  148. return orig(wrap(running_snippet, func), ...)
  149. end
  150. end
  151. for k, v in pairs(minetest) do
  152. if type(k) == 'string' and k:sub(1, 12) == 'register_on_' then
  153. safe_funcs[k] = snippets.wrap_register_on(v)
  154. end
  155. end
  156. end
  157. -- Register a snippet
  158. snippets.registered_snippets = {}
  159. function snippets.register_snippet(name, def)
  160. if def == nil and type(name) == 'table' then
  161. name, def = name.name, name
  162. elseif type(name) ~= 'string' then
  163. error('Invalid name passed to snippets.register_snippet!', 2)
  164. elseif type(def) == 'string' then
  165. def = {code=def}
  166. elseif type(def) ~= 'table' then
  167. error('Invalid definition passed to snippets.register_snippet!', 2)
  168. elseif def.owner and type(def.owner) ~= 'string' then
  169. error('Invalid owner passed to snippets.register_snippet!', 2)
  170. end
  171. def = table.copy(def)
  172. def.name = name
  173. if def.code then
  174. -- Automatically add "return"
  175. local msg
  176. def.func = loadstring('return ' .. def.code, name)
  177. if not def.func then
  178. def.func, msg = loadstring(def.code, name)
  179. end
  180. if def.func then
  181. if name ~= 'snippets:anonymous' then
  182. local old_def = snippets.registered_snippets[name]
  183. def.env = old_def and old_def.env
  184. end
  185. if not def.env then
  186. local g = {}
  187. def.env = setmetatable({}, {__index = function(self, key)
  188. local res = rawget(_G, key)
  189. if res == nil and not g[key] then
  190. snippets.log('warning', 'Undeclared global variable "'
  191. .. key .. '" accessed.')
  192. g[key] = true
  193. end
  194. return res
  195. end})
  196. end
  197. setfenv(def.func, def.env)
  198. else
  199. local r, s = running_snippet, snippets.registered_snippets[name]
  200. function def.func() end
  201. running_snippet, snippets.registered_snippets[name] = name, def
  202. snippets.log('error', 'Load error: ' .. tostring(msg))
  203. running_snippet, snippets.registered_snippets[name] = r, s
  204. end
  205. else
  206. def.persistent = nil
  207. end
  208. if not def.persistent then def.code = nil end
  209. if type(def.func) ~= 'function' then return false end
  210. snippets.registered_snippets[name] = def
  211. return true
  212. end
  213. snippets.register_snippet('snippets:anonymous', '')
  214. -- Run a snippet
  215. function snippets.run(snippet, ...)
  216. local def = snippets.registered_snippets[snippet]
  217. if not def then error('Invalid snippet specified!', 2) end
  218. return wrap_raw(snippet, def.func, ...)
  219. end
  220. -- Run code as player
  221. function snippets.exec_as_player(name, code)
  222. if minetest.is_player(name) then name = name:get_player_name() end
  223. local owner
  224. if name and name ~= '' then
  225. owner = name
  226. name = 'snippets:player_' .. tostring(name)
  227. else
  228. name = 'snippets:anonymous'
  229. end
  230. local def = {
  231. code = tostring(code),
  232. owner = owner,
  233. }
  234. if not snippets.register_snippet(name, def) then return end
  235. return snippets.run(name)
  236. end
  237. function snippets.exec(code) return snippets.exec_as_player(nil, code) end
  238. minetest.register_on_leaveplayer(function(player)
  239. snippets.registered_snippets['snippets:player_' ..
  240. player:get_player_name()] = nil
  241. end)
  242. -- In case console.lua isn't loaded
  243. function snippets.unregister_snippet(name)
  244. if snippets.registered_snippets[name] ~= nil then
  245. snippets.registered_snippets[name] = nil
  246. end
  247. end