Replace sandbox wrapper scripts.

This commit is contained in:
Matthias Mailänder
2023-05-08 21:04:45 +02:00
committed by abcdefg30
parent e8dd85419f
commit 445b736885
15 changed files with 188 additions and 763 deletions

View File

@@ -130,7 +130,7 @@ endif
check-scripts:
@echo
@echo "Checking for Lua syntax errors..."
@find lua/ mods/*/{maps,scripts}/ -iname "*.lua" -print0 | xargs -0n1 luac -p
@find mods/*/maps/ mods/*/scripts/ -iname "*.lua" -print0 | xargs -0n1 luac -p
test: all
@echo

View File

@@ -12,7 +12,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Eluant;
@@ -143,6 +142,8 @@ namespace OpenRA.Scripting
public readonly Cache<ActorInfo, Type[]> ActorCommands;
public readonly Type[] PlayerCommands;
public string ErrorMessage;
bool disposed;
public ScriptContext(World world, WorldRenderer worldRenderer,
@@ -165,64 +166,110 @@ namespace OpenRA.Scripting
.ToArray();
PlayerCommands = FilterCommands(world.Map.Rules.Actors[SystemActors.Player], knownPlayerCommands);
runtime.Globals["EngineDir"] = Platform.EngineDir;
runtime.DoBuffer(File.Open(Path.Combine(Platform.EngineDir, "lua", "scriptwrapper.lua"), FileMode.Open, FileAccess.Read).ReadAllText(), "scriptwrapper.lua").Dispose();
tick = (LuaFunction)runtime.Globals["Tick"];
// Safe functions for http://lua-users.org/wiki/SandBoxes
// assert, error have been removed as well as albeit safe
var allowedGlobals = new string[]
{
"ipairs", "next", "pairs",
"pcall", "select", "tonumber", "tostring", "type", "unpack", "xpcall",
"math", "string", "table"
};
foreach (var fieldName in runtime.Globals.Keys)
{
if (!allowedGlobals.Contains(fieldName.ToString()))
runtime.Globals[fieldName] = null;
}
var forbiddenMath = new string[]
{
"random", // not desync safe, unsuitable
"randomseed" // maybe unsafe as it affects the host RNG
};
var mathGlobal = (LuaTable)runtime.Globals["math"];
foreach (var mathFunction in mathGlobal.Keys)
{
if (forbiddenMath.Contains(mathFunction.ToString()))
mathGlobal[mathFunction] = null;
}
// Register globals
runtime.Globals["EngineDir"] = Platform.EngineDir;
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)FatalError))
runtime.Globals["FatalError"] = fn;
runtime.Globals["MaxUserScriptInstructions"] = MaxUserScriptInstructions;
using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"])
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)LogDebugMessage))
runtime.Globals["print"] = fn;
// Register global tables
var bindings = Game.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>();
foreach (var b in bindings)
{
using (var fn = runtime.CreateFunctionFromDelegate((Action<string>)LogDebugMessage))
registerGlobal.Call("print", fn).Dispose();
// Register global tables
var bindings = Game.ModData.ObjectCreator.GetTypesImplementing<ScriptGlobal>();
foreach (var b in bindings)
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var ctor = b.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
{
var p = c.GetParameters();
return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext);
});
var p = c.GetParameters();
return p.Length == 1 && p.First().ParameterType == typeof(ScriptContext);
});
if (ctor == null)
throw new InvalidOperationException($"{b.Name} must define a constructor that takes a ScriptContext context parameter");
if (ctor == null)
throw new InvalidOperationException($"{b.Name} must define a constructor that takes a {nameof(ScriptContext)} context parameter");
var binding = (ScriptGlobal)ctor.Invoke(new[] { this });
using (var obj = binding.ToLuaValue(this))
registerGlobal.Call(binding.Name, obj).Dispose();
}
var binding = (ScriptGlobal)ctor.Invoke(new[] { this });
using (var obj = binding.ToLuaValue(this))
runtime.Globals.Add(binding.Name, obj);
}
// System functions do not count towards the memory limit
runtime.MaxMemoryUse = runtime.MemoryUse + MaxUserScriptMemory;
using (var loadScript = (LuaFunction)runtime.Globals["ExecuteSandboxedScript"])
try
{
foreach (var s in scripts)
loadScript.Call(s, world.Map.Open(s).ReadAllText()).Dispose();
foreach (var script in scripts)
runtime.DoBuffer(world.Map.Open(script).ReadAllText(), script).Dispose();
}
catch (Exception e)
{
FatalError(e);
return;
}
tick = (LuaFunction)runtime.Globals["Tick"];
}
void LogDebugMessage(string message)
{
Console.WriteLine("Lua debug: {0}", message);
Console.WriteLine($"Lua debug: {message}");
Log.Write("lua", message);
}
public bool FatalErrorOccurred { get; private set; }
public void FatalError(string message)
public void FatalError(Exception e)
{
ErrorMessage = e.Message;
Console.WriteLine($"Fatal Lua Error: {e.Message}");
Console.WriteLine(e.StackTrace);
Log.Write("lua", $"Fatal Lua Error: {e.Message}");
Log.Write("lua", e.StackTrace);
FatalErrorOccurred = true;
World.AddFrameEndTask(w => World.EndGame());
}
void FatalError(string message)
{
var stacktrace = new StackTrace().ToString();
Console.WriteLine($"Fatal Lua Error: {message}");
Console.WriteLine(stacktrace);
Log.Write("lua", $"Fatal Lua Error: {message}");
Log.Write("lua", message);
Log.Write("lua", stacktrace);
FatalErrorOccurred = true;
@@ -232,14 +279,11 @@ namespace OpenRA.Scripting
public void RegisterMapActor(string name, Actor a)
{
using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"])
{
if (runtime.Globals.ContainsKey(name))
throw new LuaException($"The global name '{name}' is reserved, and may not be used by a map actor");
if (runtime.Globals.ContainsKey(name))
throw new LuaException($"The global name '{name}' is reserved, and may not be used by a map actor");
using (var obj = a.ToLuaValue(this))
registerGlobal.Call(name, obj).Dispose();
}
using (var obj = a.ToLuaValue(this))
runtime.Globals.Add(name, obj);
}
public void WorldLoaded()
@@ -247,8 +291,15 @@ namespace OpenRA.Scripting
if (FatalErrorOccurred)
return;
using (var worldLoaded = (LuaFunction)runtime.Globals["WorldLoaded"])
worldLoaded.Call().Dispose();
try
{
using (var worldLoaded = (LuaFunction)runtime.Globals["WorldLoaded"])
worldLoaded.Call().Dispose();
}
catch (LuaException e)
{
FatalError(e);
}
}
public void Tick()
@@ -256,8 +307,15 @@ namespace OpenRA.Scripting
if (FatalErrorOccurred || disposed)
return;
using (new PerfSample("tick_lua"))
tick.Call().Dispose();
try
{
using (new PerfSample("tick_lua"))
tick.Call().Dispose();
}
catch (LuaException e)
{
FatalError(e);
}
}
public void Dispose()

View File

@@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Activities
}
catch (Exception ex)
{
context.FatalError(ex.Message);
context.FatalError(ex);
}
Dispose();

View File

@@ -172,7 +172,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (LuaException e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
};
}

View File

@@ -45,7 +45,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -133,7 +133,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -228,7 +228,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -243,7 +243,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -282,7 +282,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -310,7 +310,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -339,7 +339,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -366,7 +366,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -399,7 +399,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}
@@ -426,7 +426,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception e)
{
Context.FatalError(e.Message);
Context.FatalError(e);
}
}

View File

@@ -29,7 +29,7 @@ namespace OpenRA.Mods.Common.Scripting
public class LuaScript : ITick, IWorldLoaded, INotifyActorDisposing
{
readonly LuaScriptInfo info;
ScriptContext context;
public ScriptContext Context;
bool disposed;
public LuaScript(LuaScriptInfo info)
@@ -40,13 +40,13 @@ namespace OpenRA.Mods.Common.Scripting
void IWorldLoaded.WorldLoaded(World world, WorldRenderer worldRenderer)
{
var scripts = info.Scripts ?? Enumerable.Empty<string>();
context = new ScriptContext(world, worldRenderer, scripts);
context.WorldLoaded();
Context = new ScriptContext(world, worldRenderer, scripts);
Context.WorldLoaded();
}
void ITick.Tick(Actor self)
{
context.Tick();
Context.Tick();
}
void INotifyActorDisposing.Disposing(Actor self)
@@ -54,11 +54,11 @@ namespace OpenRA.Mods.Common.Scripting
if (disposed)
return;
context?.Dispose();
Context?.Dispose();
disposed = true;
}
public bool FatalErrorOccurred => context.FatalErrorOccurred;
public bool FatalErrorOccurred => Context.FatalErrorOccurred;
}
}

View File

@@ -102,7 +102,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -123,7 +123,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -144,7 +144,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -168,7 +168,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -191,7 +191,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -211,7 +211,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -232,7 +232,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -253,7 +253,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -274,7 +274,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -296,7 +296,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -319,7 +319,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -338,7 +338,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -361,7 +361,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -385,7 +385,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -408,7 +408,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -431,7 +431,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -446,7 +446,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -467,7 +467,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -488,7 +488,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}
@@ -507,7 +507,7 @@ namespace OpenRA.Mods.Common.Scripting
}
catch (Exception ex)
{
f.Context.FatalError(ex.Message);
f.Context.FatalError(ex);
return;
}
}

View File

@@ -0,0 +1,37 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using OpenRA.Mods.Common.Scripting;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
class ScriptErrorLogic : ChromeLogic
{
[ObjectCreator.UseCtor]
public ScriptErrorLogic(Widget widget, World world)
{
var panel = widget.Get<ScrollPanelWidget>("SCRIPT_ERROR_MESSAGE_PANEL");
var label = widget.Get<LabelWidget>("SCRIPT_ERROR_MESSAGE");
var font = Game.Renderer.Fonts[label.Font];
var luaScript = world.WorldActor.TraitOrDefault<LuaScript>();
if (luaScript != null)
{
var text = WidgetUtils.WrapText(luaScript.Context.ErrorMessage, label.Bounds.Width, font);
label.Text = text;
label.Bounds.Height = font.Measure(text).Y;
panel.ScrollToTop();
panel.Layout.AdjustChildren();
}
}
}
}

View File

@@ -1,163 +0,0 @@
local sandbox = {
_VERSION = "sandbox 0.5",
_DESCRIPTION = "A pure-lua solution for running untrusted Lua code.",
_URL = "https://github.com/kikito/sandbox.lua",
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-- The base environment is merged with the given env option (or an empty table, if no env provided)
--
local BASE_ENV = {}
-- List of non-safe packages/functions:
--
-- * string.rep: can be used to allocate millions of bytes in 1 operation
-- * {set|get}metatable: can be used to modify the metatable of global objects (strings, integers)
-- * collectgarbage: can affect performance of other systems
-- * dofile: can access the server filesystem
-- * _G: It has access to everything. It can be mocked to other things though.
-- * load{file|string}: All unsafe because they can grant acces to global env
-- * raw{get|set|equal}: Potentially unsafe
-- * module|require|module: Can modify the host settings
-- * string.dump: Can display confidential server info (implementation of functions)
-- * string.rep: Can allocate millions of bytes in one go
-- * math.randomseed: Can affect the host sytem
-- * io.*, os.*: Most stuff there is non-save
-- Safe packages/functions below
([[
_VERSION assert error ipairs next pairs
pcall select tonumber tostring type unpack xpcall
coroutine.create coroutine.resume coroutine.running coroutine.status
coroutine.wrap coroutine.yield
math.abs math.acos math.asin math.atan math.atan2 math.ceil
math.cos math.cosh math.deg math.exp math.fmod math.floor
math.frexp math.huge math.ldexp math.log math.log10 math.max
math.min math.modf math.pi math.pow math.rad
math.sin math.sinh math.sqrt math.tan math.tanh
os.clock os.difftime os.time
string.byte string.char string.find string.format string.gmatch
string.gsub string.len string.lower string.match string.reverse
string.sub string.upper
table.insert table.maxn table.remove table.sort
]]):gsub('%S+', function(id)
local module, method = id:match('([^%.]+)%.([^%.]+)')
if module then
BASE_ENV[module] = BASE_ENV[module] or {}
BASE_ENV[module][method] = _G[module][method]
else
BASE_ENV[id] = _G[id]
end
end)
local function protect_module(module, module_name)
return setmetatable({}, {
__index = module,
__newindex = function(_, attr_name, _)
error('Can not modify ' .. module_name .. '.' .. attr_name .. '. Protected by the sandbox.')
end
})
end
('coroutine math os string table'):gsub('%S+', function(module_name)
BASE_ENV[module_name] = protect_module(BASE_ENV[module_name], module_name)
end)
-- auxiliary functions/variables
local string_rep = string.rep
local function merge(dest, source)
for k,v in pairs(source) do
dest[k] = dest[k] or v
end
return dest
end
local function sethook(f, key, quota)
if type(debug) ~= 'table' or type(debug.sethook) ~= 'function' then return end
debug.sethook(f, key, quota)
end
local function cleanup()
sethook()
string.rep = string_rep
end
-- Public interface: sandbox.protect
function sandbox.protect(f, options)
if type(f) == 'string' then f = assert(loadstring(f)) end
options = options or {}
local quota = false
if options.quota ~= false then
quota = options.quota or 500000
end
local env = merge(options.env or {}, BASE_ENV)
env._G = env._G or env
setfenv(f, env)
return function(...)
if quota then
local timeout = function()
cleanup()
error('Quota exceeded: ' .. tostring(quota))
end
sethook(timeout, "", quota)
end
string.rep = nil
local ok, result = pcall(f, ...)
cleanup()
if not ok then error(result) end
return result
end
end
-- Public interface: sandbox.run
function sandbox.run(f, options, ...)
return sandbox.protect(f, options)(...)
end
-- make sandbox(f) == sandbox.protect(f)
setmetatable(sandbox, {__call = function(_,f,o) return sandbox.protect(f,o) end})
return sandbox

View File

@@ -1,52 +0,0 @@
--[[
Copyright (c) The OpenRA Developers and Contributors
This file is part of OpenRA, which is free software. It is made
available to you under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version. For more
information, see COPYING.
]]
environment = {}
-- Reset package path
package.path = EngineDir .. "/lua/?.lua"
-- Note: sandbox has been customized to remove math.random
local sandbox = require('sandbox')
local stp = require('stacktraceplus')
local PrintStackTrace = function(msg)
return stp.stacktrace("", 2) .. "\nError message\n===============\n" .. msg .. "\n==============="
end
local TryRunSandboxed = function(fn)
local success, err = xpcall(function() sandbox.run(fn, {env = environment, quota = MaxUserScriptInstructions}) end, PrintStackTrace)
if not success then
FatalError(err)
end
end
WorldLoaded = function()
if environment.WorldLoaded ~= nil then
TryRunSandboxed(environment.WorldLoaded)
end
end
Tick = function()
if environment.Tick ~= nil then
TryRunSandboxed(environment.Tick)
end
end
ExecuteSandboxedScript = function(file, contents)
local script, err = loadstring(contents, file)
if (script == nil) then
FatalError("Error parsing " .. file .. ". Reason: " .. err)
else
TryRunSandboxed(script)
end
end
RegisterSandboxedGlobal = function(key, value)
environment[key] = value
end

View File

@@ -1,436 +0,0 @@
--[[
The MIT License
Copyright (c) 2010 Ignacio Burgue<75>o
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Source: https://github.com/ignacio/StackTracePlus
]]
-- tables
local _G = _G
local string, io, debug, coroutine = string, io, debug, coroutine
-- functions
local tostring, print, require = tostring, print, require
local next, assert = next, assert
local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs
local error = error
assert(debug, "debug table must be available at this point")
local io_open = io.open
local string_gmatch = string.gmatch
local string_sub = string.sub
local table_concat = table.concat
local _M = {
max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)'
}
-- this tables should be weak so the elements in them won't become uncollectable
local m_known_tables = { [_G] = "_G (global table)" }
local function add_known_module(name, desc)
local ok, mod = pcall(require, name)
if ok then
m_known_tables[mod] = desc
end
end
add_known_module("string", "string module")
add_known_module("io", "io module")
add_known_module("os", "os module")
add_known_module("table", "table module")
add_known_module("math", "math module")
add_known_module("package", "package module")
add_known_module("debug", "debug module")
add_known_module("coroutine", "coroutine module")
-- lua5.2
add_known_module("bit32", "bit32 module")
-- luajit
add_known_module("bit", "bit module")
add_known_module("jit", "jit module")
local m_user_known_tables = {}
local m_known_functions = {}
for _, name in ipairs{
-- Lua 5.2, 5.1
"assert",
"collectgarbage",
"dofile",
"error",
"getmetatable",
"ipairs",
"load",
"loadfile",
"next",
"pairs",
"pcall",
"print",
"rawequal",
"rawget",
"rawlen",
"rawset",
"require",
"select",
"setmetatable",
"tonumber",
"tostring",
"type",
"xpcall",
-- Lua 5.1
"gcinfo",
"getfenv",
"loadstring",
"module",
"newproxy",
"setfenv",
"unpack",
-- TODO: add table.* etc functions
} do
if _G[name] then
m_known_functions[_G[name]] = name
end
end
local m_user_known_functions = {}
local function safe_tostring (value)
local ok, err = pcall(tostring, value)
if ok then return err else return ("<failed to get printable value>: '%s'"):format(err) end
end
-- Private:
-- Parses a line, looking for possible function definitions (in a very na<6E>ve way)
-- Returns '(anonymous)' if no function name was found in the line
local function ParseLine(line)
assert(type(line) == "string")
--print(line)
local match = line:match("^%s*function%s+(%w+)")
if match then
--print("+++++++++++++function", match)
return match
end
match = line:match("^%s*local%s+function%s+(%w+)")
if match then
--print("++++++++++++local", match)
return match
end
match = line:match("^%s*local%s+(%w+)%s+=%s+function")
if match then
--print("++++++++++++local func", match)
return match
end
match = line:match("%s*function%s*%(") -- this is an anonymous function
if match then
--print("+++++++++++++function2", match)
return "(anonymous)"
end
return "(anonymous)"
end
-- Private:
-- Tries to guess a function's name when the debug info structure does not have it.
-- It parses either the file or the string where the function is defined.
-- Returns '?' if the line where the function is defined is not found
local function GuessFunctionName(info)
--print("guessing function name")
if type(info.source) == "string" and info.source:sub(1,1) == "@" then
local file, err = io_open(info.source:sub(2), "r")
if not file then
print("file not found: "..tostring(err)) -- whoops!
return "?"
end
local line
for i = 1, info.linedefined do
line = file:read("*l")
end
if not line then
print("line not found") -- whoops!
return "?"
end
return ParseLine(line)
else
local line
local lineNumber = 0
for l in string_gmatch(info.source, "([^\n]+)\n-") do
lineNumber = lineNumber + 1
if lineNumber == info.linedefined then
line = l
break
end
end
if not line then
print("line not found") -- whoops!
return "?"
end
return ParseLine(line)
end
end
---
-- Dumper instances are used to analyze stacks and collect its information.
--
local Dumper = {}
Dumper.new = function(thread)
local t = { lines = {} }
for k,v in pairs(Dumper) do t[k] = v end
t.dumping_same_thread = (thread == coroutine.running())
-- if a thread was supplied, bind it to debug.info and debug.get
-- we also need to skip this additional level we are introducing in the callstack (only if we are running
-- in the same thread we're inspecting)
if type(thread) == "thread" then
t.getinfo = function(level, what)
if t.dumping_same_thread and type(level) == "number" then
level = level + 1
end
return debug.getinfo(thread, level, what)
end
t.getlocal = function(level, loc)
if t.dumping_same_thread then
level = level + 1
end
return debug.getlocal(thread, level, loc)
end
else
t.getinfo = debug.getinfo
t.getlocal = debug.getlocal
end
return t
end
-- helpers for collecting strings to be used when assembling the final trace
function Dumper:add (text)
self.lines[#self.lines + 1] = text
end
function Dumper:add_f (fmt, ...)
self:add(fmt:format(...))
end
function Dumper:concat_lines ()
return table_concat(self.lines)
end
---
-- Private:
-- Iterates over the local variables of a given function.
--
-- @param level The stack level where the function is.
--
function Dumper:DumpLocals (level)
local prefix = "\t "
local i = 1
if self.dumping_same_thread then
level = level + 1
end
local name, value = self.getlocal(level, i)
if not name then
return
end
self:add("\tLocal variables:\r\n")
while name do
if type(value) == "number" then
self:add_f("%s%s = number: %g\r\n", prefix, name, value)
elseif type(value) == "boolean" then
self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value))
elseif type(value) == "string" then
self:add_f("%s%s = string: %q\r\n", prefix, name, value)
elseif type(value) == "userdata" then
self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value))
elseif type(value) == "nil" then
self:add_f("%s%s = nil\r\n", prefix, name)
elseif type(value) == "table" then
if m_known_tables[value] then
self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value])
elseif m_user_known_tables[value] then
self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value])
else
local txt = "{"
for k,v in pairs(value) do
txt = txt..safe_tostring(k)..":"..safe_tostring(v)
if #txt > _M.max_tb_output_len then
txt = txt.." (more...)"
break
end
if next(value, k) then txt = txt..", " end
end
self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt.."}")
end
elseif type(value) == "function" then
local info = self.getinfo(value, "nS")
local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value]
if info.what == "C" then
self:add_f("%s%s = C %s\r\n", prefix, name, (fun_name and ("function: " .. fun_name) or tostring(value)))
else
local source = info.short_src
if source:sub(2,7) == "string" then
source = source:sub(9)
end
--for k,v in pairs(info) do print(k,v) end
fun_name = fun_name or GuessFunctionName(info)
self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name, info.linedefined, source)
end
elseif type(value) == "thread" then
self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value))
end
i = i + 1
name, value = self.getlocal(level, i)
end
end
---
-- Public:
-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc.
-- This function is suitable to be used as an error handler with pcall or xpcall
--
-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread)
-- @param message An optional error string or object.
-- @param level An optional number telling at which level to start the traceback (default is 1)
--
-- Returns a string with the stack trace and a string with the original error.
--
function _M.stacktrace(thread, message, level)
if type(thread) ~= "thread" then
-- shift parameters left
thread, message, level = nil, thread, message
end
thread = thread or coroutine.running()
level = level or 1
local dumper = Dumper.new(thread)
local original_error
if type(message) == "table" then
dumper:add("an error object {\r\n")
local first = true
for k,v in pairs(message) do
if first then
dumper:add(" ")
first = false
else
dumper:add(",\r\n ")
end
dumper:add(safe_tostring(k))
dumper:add(": ")
dumper:add(safe_tostring(v))
end
dumper:add("\r\n}")
original_error = dumper:concat_lines()
elseif type(message) == "string" then
dumper:add(message)
original_error = message
end
dumper:add("\r\n")
dumper:add[[
Stack Traceback
===============
]]
--print(error_message)
local level_to_show = level
if dumper.dumping_same_thread then level = level + 1 end
local info = dumper.getinfo(level, "nSlf")
while info do
if info.what == "main" then
if string_sub(info.source, 1, 1) == "@" then
dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show, string_sub(info.source, 2), info.currentline)
else
dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.short_src, info.currentline)
end
elseif info.what == "C" then
--print(info.namewhat, info.name)
--for k,v in pairs(info) do print(k,v, type(v)) end
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or tostring(info.func)
dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name)
--dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value)))
elseif info.what == "tail" then
--print("tail")
--for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name)
dumper:add_f("(%d) tail call\r\n", level_to_show)
dumper:DumpLocals(level)
elseif info.what == "Lua" then
local source = info.short_src
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name
if source:sub(2, 7) == "string" then
source = source:sub(9)
end
local was_guessed = false
if not function_name or function_name == "?" then
--for k,v in pairs(info) do print(k,v, type(v)) end
function_name = GuessFunctionName(info)
was_guessed = true
end
-- test if we have a file name
local function_type = (info.namewhat == "") and "function" or info.namewhat
if info.source and info.source:sub(1, 1) == "@" then
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
elseif info.source and info.source:sub(1,1) == '#' then
dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type, function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
else
dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type, function_name, info.currentline, source)
end
dumper:DumpLocals(level)
else
dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what)
end
level = level + 1
level_to_show = level_to_show + 1
info = dumper.getinfo(level, "nSlf")
end
return dumper:concat_lines(), original_error
end
--
-- Adds a table to the list of known tables
function _M.add_known_table(tab, description)
if m_known_tables[tab] then
error("Cannot override an already known table")
end
m_user_known_tables[tab] = description
end
--
-- Adds a function to the list of known functions
function _M.add_known_function(fun, description)
if m_known_functions[fun] then
error("Cannot override an already known function")
end
m_user_known_functions[fun] = description
end
return _M

View File

@@ -141,10 +141,6 @@ function Check-Scripts-Command
{
luac -p $script
}
foreach ($script in ls "lua/*.lua")
{
luac -p $script
}
foreach ($script in ls "mods/*/scripts/*.lua")
{
luac -p $script

View File

@@ -1,27 +0,0 @@
Container@SCRIPT_ERROR_PANEL:
Height: PARENT_BOTTOM
Width: PARENT_RIGHT
Children:
Label@DESCA:
X: 15
Y: 15
Width: PARENT_RIGHT - 30
Height: 20
Font: Bold
Align: Center
Text: The map script has encountered a fatal error
Label@DESCB:
X: 15
Y: 45
Width: PARENT_RIGHT - 30
Height: 20
Font: Regular
WordWrap: true
Text: The details of the error have been saved to lua.log in the logs directory.
Label@DESCC:
X: 15
Y: 65
Width: PARENT_RIGHT - 30
Height: 20
Font: Regular
Text: Please send this file to the map author so that they can fix this issue.

View File

@@ -123,7 +123,7 @@ ChromeLayout:
cnc|chrome/ingame-infochat.yaml
cnc|chrome/ingame-info.yaml
cnc|chrome/ingame-infobriefing.yaml
cnc|chrome/ingame-infoscripterror.yaml
common|chrome/ingame-infoscripterror.yaml
cnc|chrome/ingame-infoobjectives.yaml
cnc|chrome/ingame-infostats.yaml
cnc|chrome/ingame-info-lobby-options.yaml

View File

@@ -1,6 +1,7 @@
Container@SCRIPT_ERROR_PANEL:
Height: PARENT_BOTTOM
Width: PARENT_RIGHT
Logic: ScriptErrorLogic
Children:
Label@DESCA:
X: 15
@@ -16,7 +17,7 @@ Container@SCRIPT_ERROR_PANEL:
Width: PARENT_RIGHT - 30
Height: 20
Font: Regular
WordWrap: true
Align: Center
Text: The details of the error have been saved to lua.log in the logs directory.
Label@DESCC:
X: 15
@@ -24,4 +25,15 @@ Container@SCRIPT_ERROR_PANEL:
Width: PARENT_RIGHT - 30
Height: 20
Font: Regular
Align: Center
Text: Please send this file to the map author so that they can fix this issue.
ScrollPanel@SCRIPT_ERROR_MESSAGE_PANEL:
X: 20
Y: 96
Width: PARENT_RIGHT - 40
Height: 300
Children:
Label@SCRIPT_ERROR_MESSAGE:
X: 4
Y: 2
Width: PARENT_RIGHT - 32