diff --git a/Makefile b/Makefile index 903b7f6992..08cb021376 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/OpenRA.Game/Scripting/ScriptContext.cs b/OpenRA.Game/Scripting/ScriptContext.cs index e91bcc79c6..9029cd9be8 100644 --- a/OpenRA.Game/Scripting/ScriptContext.cs +++ b/OpenRA.Game/Scripting/ScriptContext.cs @@ -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 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)FatalError)) runtime.Globals["FatalError"] = fn; runtime.Globals["MaxUserScriptInstructions"] = MaxUserScriptInstructions; - using (var registerGlobal = (LuaFunction)runtime.Globals["RegisterSandboxedGlobal"]) + using (var fn = runtime.CreateFunctionFromDelegate((Action)LogDebugMessage)) + runtime.Globals["print"] = fn; + + // Register global tables + var bindings = Game.ModData.ObjectCreator.GetTypesImplementing(); + foreach (var b in bindings) { - using (var fn = runtime.CreateFunctionFromDelegate((Action)LogDebugMessage)) - registerGlobal.Call("print", fn).Dispose(); - - // Register global tables - var bindings = Game.ModData.ObjectCreator.GetTypesImplementing(); - 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() diff --git a/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs b/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs index 7cbba17722..f3d060d8cc 100644 --- a/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs +++ b/OpenRA.Mods.Common/Scripting/CallLuaFunc.cs @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Activities } catch (Exception ex) { - context.FatalError(ex.Message); + context.FatalError(ex); } Dispose(); diff --git a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs index 64a455ae4f..0b607f15bd 100644 --- a/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/MediaGlobal.cs @@ -172,7 +172,7 @@ namespace OpenRA.Mods.Common.Scripting } catch (LuaException e) { - Context.FatalError(e.Message); + Context.FatalError(e); } }; } diff --git a/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs b/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs index 01f4ab109a..0991e980dd 100644 --- a/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs +++ b/OpenRA.Mods.Common/Scripting/Global/TriggerGlobal.cs @@ -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); } } diff --git a/OpenRA.Mods.Common/Scripting/LuaScript.cs b/OpenRA.Mods.Common/Scripting/LuaScript.cs index 24158208db..117d82c3be 100644 --- a/OpenRA.Mods.Common/Scripting/LuaScript.cs +++ b/OpenRA.Mods.Common/Scripting/LuaScript.cs @@ -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(); - 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; } } diff --git a/OpenRA.Mods.Common/Scripting/ScriptTriggers.cs b/OpenRA.Mods.Common/Scripting/ScriptTriggers.cs index 7618c28288..ce5845cd7a 100644 --- a/OpenRA.Mods.Common/Scripting/ScriptTriggers.cs +++ b/OpenRA.Mods.Common/Scripting/ScriptTriggers.cs @@ -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; } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/ScriptErrorLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ScriptErrorLogic.cs new file mode 100644 index 0000000000..e853f6b623 --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/ScriptErrorLogic.cs @@ -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("SCRIPT_ERROR_MESSAGE_PANEL"); + var label = widget.Get("SCRIPT_ERROR_MESSAGE"); + var font = Game.Renderer.Fonts[label.Font]; + + var luaScript = world.WorldActor.TraitOrDefault(); + 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(); + } + } + } +} diff --git a/lua/sandbox.lua b/lua/sandbox.lua deleted file mode 100644 index 89374e9335..0000000000 --- a/lua/sandbox.lua +++ /dev/null @@ -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 diff --git a/lua/scriptwrapper.lua b/lua/scriptwrapper.lua deleted file mode 100644 index ffeb7851d9..0000000000 --- a/lua/scriptwrapper.lua +++ /dev/null @@ -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 diff --git a/lua/stacktraceplus.lua b/lua/stacktraceplus.lua deleted file mode 100644 index b129b9d6e9..0000000000 --- a/lua/stacktraceplus.lua +++ /dev/null @@ -1,436 +0,0 @@ ---[[ - The MIT License - - Copyright (c) 2010 Ignacio Burgueñ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 (": '%s'"):format(err) end -end - --- Private: --- Parses a line, looking for possible function definitions (in a very naï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 diff --git a/make.ps1 b/make.ps1 index 3516facd52..800da7909a 100644 --- a/make.ps1 +++ b/make.ps1 @@ -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 diff --git a/mods/cnc/chrome/ingame-infoscripterror.yaml b/mods/cnc/chrome/ingame-infoscripterror.yaml deleted file mode 100644 index efce6b2568..0000000000 --- a/mods/cnc/chrome/ingame-infoscripterror.yaml +++ /dev/null @@ -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. diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index e83fa957a6..9402de252f 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -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 diff --git a/mods/common/chrome/ingame-infoscripterror.yaml b/mods/common/chrome/ingame-infoscripterror.yaml index 963535cc5f..4931a84668 100644 --- a/mods/common/chrome/ingame-infoscripterror.yaml +++ b/mods/common/chrome/ingame-infoscripterror.yaml @@ -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