Refactor error/exception/message handling

This adds:
- LuaCsSetup.ExceptionHandler so we can decide how we want to handle
exceptions for unit tests
- LuaCsSetup.MessageHandler so we can redirect logs to the XUnit output
  helper
This commit is contained in:
peelz
2022-08-06 18:10:54 -04:00
parent 3de2d8e550
commit dd1b404c9b
14 changed files with 184 additions and 190 deletions

View File

@@ -3270,7 +3270,7 @@ namespace Barotrauma
}
catch(Exception ex)
{
GameMain.LuaCs.HandleException(ex);
GameMain.LuaCs.HandleException(ex, LuaCsMessageOrigin.LuaMod);
}
}));
commands.Add(new Command("cl_cs", "cs_cl: runs a string on the client", (string[] args) =>

View File

@@ -20,7 +20,7 @@ namespace Barotrauma
{
class GameMain : Game
{
public static LuaCsSetup LuaCs;
internal static LuaCsSetup LuaCs;
public static bool ShowFPS = false;
public static bool ShowPerf = false;

View File

@@ -1249,7 +1249,7 @@ namespace Barotrauma
}
catch (Exception ex)
{
GameMain.LuaCs.HandleException(ex);
GameMain.LuaCs.HandleException(ex, LuaCsMessageOrigin.LuaMod);
}
}));
commands.Add(new Command("cs", "cs: runs a string", (string[] args) =>
@@ -1319,13 +1319,13 @@ namespace Barotrauma
}
catch (UnauthorizedAccessException e)
{
GameMain.LuaCs.PrintError("You seem to already have Client Side Lua installed, if you are trying to reinstall, make sure uninstall it first (mainmenu button located top left).");
GameMain.LuaCs.PrintError("You seem to already have Client Side Lua installed, if you are trying to reinstall, make sure uninstall it first (mainmenu button located top left).", LuaCsMessageOrigin.LuaCs);
return;
}
catch (Exception e)
{
GameMain.LuaCs.HandleException(e);
GameMain.LuaCs.HandleException(e, LuaCsMessageOrigin.LuaCs);
return;
}

View File

@@ -37,7 +37,7 @@ namespace Barotrauma
}
catch (Exception e)
{
GameMain.LuaCs.HandleException(e, null, LuaCsSetup.ExceptionType.CSharp);
GameMain.LuaCs.HandleException(e, LuaCsMessageOrigin.CSharpMod);
}
LoadedMods.Remove(this);

View File

@@ -14,7 +14,6 @@ using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using Sigil;
using Sigil.NonGeneric;
using static Barotrauma.LuaCsSetup;
namespace Barotrauma
{
@@ -797,7 +796,8 @@ namespace Barotrauma
{
argsSb.Append(arg + " ");
}
GameMain.LuaCs.HandleException(e, $"Error in Hook '{name}'->'{key}', with args '{argsSb}':\n{e}", ExceptionType.Both);
luaCs.PrintError($"Error in Hook '{name}'->'{key}', with args '{argsSb}':\n{e}", LuaCsMessageOrigin.Unknown);
luaCs.HandleException(e, LuaCsMessageOrigin.Unknown);
}
}
foreach (var key in hooksToRemove)
@@ -1116,12 +1116,11 @@ namespace Barotrauma
il.LoadField(luaCsField);
il.If((il) =>
{
// IL: LuaCs.HandleException(exception, "", ExceptionType.Lua);
// IL: LuaCs.HandleException(exception, LuaCsMessageOrigin.LuaMod);
il.LoadField(luaCsField);
il.LoadLocal(exception);
il.LoadConstant("");
il.LoadConstant((int)ExceptionType.Lua); // underlying enum type is int
il.Call(typeof(LuaCsSetup).GetMethod(nameof(LuaCsSetup.HandleException)));
il.LoadConstant((int)LuaCsMessageOrigin.LuaMod); // underlying enum type is int
il.Call(typeof(LuaCsSetup).GetMethod(nameof(LuaCsSetup.HandleException), BindingFlags.NonPublic | BindingFlags.Instance));
});
il.EndCatchBlock(catchBlock);
@@ -1168,7 +1167,7 @@ namespace Barotrauma
{
if (methodPatches.Prefixes.Remove(identifier))
{
PrintLogMessage($"Replacing existing prefix: {identifier}");
luaCs.PrintMessage($"Replacing existing prefix: {identifier}");
}
methodPatches.Prefixes.Add(identifier, new LuaCsPatch
@@ -1181,7 +1180,7 @@ namespace Barotrauma
{
if (methodPatches.Postfixes.Remove(identifier))
{
PrintLogMessage($"Replacing existing postfix: {identifier}");
luaCs.PrintMessage($"Replacing existing postfix: {identifier}");
}
methodPatches.Postfixes.Add(identifier, new LuaCsPatch

View File

@@ -84,7 +84,8 @@ namespace Barotrauma
}
catch (Exception ex)
{
GameMain.LuaCs.HandleException(ex, $"Error in {__originalMethod.Name}:", exceptionType: LuaCsSetup.ExceptionType.Both);
GameMain.LuaCs.PrintError($"Error in {__originalMethod.Name}:", LuaCsMessageOrigin.Unknown);
GameMain.LuaCs.HandleException(ex, LuaCsMessageOrigin.Unknown);
}
}
@@ -125,7 +126,7 @@ namespace Barotrauma
{
if (identifier == null || method == null || patch == null)
{
GameMain.LuaCs.HandleException(new ArgumentNullException("Identifier, Method and Patch arguments must not be null."), exceptionType: ExceptionType.Both);
luaCs.HandleException(new ArgumentNullException("Identifier, Method and Patch arguments must not be null."), LuaCsMessageOrigin.Unknown);
return;
}
ValidatePatchTarget(method);

View File

@@ -73,7 +73,7 @@ namespace Barotrauma
{
if (luaModInterface.Any(i => i.Equals(modName)))
{
GameMain.LuaCs.HandleException(new ArgumentException($"'{modName}' entry already registered"), exceptionType: ExceptionType.Lua);
GameMain.LuaCs.HandleException(new ArgumentException($"'{modName}' entry already registered"), LuaCsMessageOrigin.LuaMod);
return null;
}
@@ -86,7 +86,7 @@ namespace Barotrauma
{
if (csModInterface.Any(i => i.Equals(mod)))
{
GameMain.LuaCs.HandleException(new ArgumentException($"'{mod.GetType().FullName}' entry already registered"), exceptionType: ExceptionType.CSharp);
GameMain.LuaCs.HandleException(new ArgumentException($"'{mod.GetType().FullName}' entry already registered"), LuaCsMessageOrigin.CSharpMod);
return null;
}

View File

@@ -93,9 +93,11 @@ namespace Barotrauma
{
LuaCsNetReceives[netMessageName](netMessage, client);
}
catch(Exception e)
catch (Exception e)
{
GameMain.LuaCs.HandleException(e, $"Exception thrown inside NetMessageReceive({netMessageName})", LuaCsSetup.ExceptionType.CSharp);
// TODO: make LuaCsNetworking hold a reference to LuaCsSetup instead of using this global
GameMain.LuaCs.PrintError($"Exception thrown inside NetMessageReceive({netMessageName})", LuaCsMessageOrigin.Unknown);
GameMain.LuaCs.HandleException(e, LuaCsMessageOrigin.Unknown);
}
}
}
@@ -120,7 +122,8 @@ namespace Barotrauma
}
catch (Exception e)
{
GameMain.LuaCs.HandleException(e, $"Exception thrown inside NetMessageReceive({netMessageName})", LuaCsSetup.ExceptionType.CSharp);
GameMain.LuaCs.PrintError($"Exception thrown inside NetMessageReceive({netMessageName})", LuaCsMessageOrigin.Unknown);
GameMain.LuaCs.HandleException(e, LuaCsMessageOrigin.Unknown);
}
}
}

View File

@@ -25,6 +25,18 @@ namespace Barotrauma
public LuaCsSetupConfig() { }
}
internal delegate void LuaCsMessageLogger(string prefix, object o);
internal delegate void LuaCsExceptionHandler(Exception ex, LuaCsMessageOrigin origin);
internal enum LuaCsMessageOrigin
{
LuaCs,
Unknown,
LuaMod,
CSharpMod,
}
partial class LuaCsSetup
{
public const string LuaSetupFile = "Lua/LuaSetup.lua";
@@ -56,7 +68,7 @@ namespace Barotrauma
/// </summary>
public void RecreateCsScript()
{
GameMain.LuaCs.CsScript = new CsScriptRunner(GameMain.LuaCs.CsScript.setup);
CsScript = new CsScriptRunner(CsScript.setup);
lua.Globals["CsScript"] = CsScript;
}
@@ -77,7 +89,10 @@ namespace Barotrauma
public LuaCsSetup()
{
Hook = new LuaCsHook(this);
MessageLogger = DefaultMessageLogger;
ExceptionHandler = DefaultExceptionHandler;
Hook = new LuaCsHook(this);
ModStore = new LuaCsModStore();
Game = new LuaGame();
@@ -137,123 +152,141 @@ namespace Barotrauma
return null;
}
public enum ExceptionType
private void DefaultExceptionHandler(Exception ex, LuaCsMessageOrigin origin)
{
Lua,
CSharp,
Both
switch (ex)
{
case NetRuntimeException netRuntimeException:
if (netRuntimeException.DecoratedMessage == null)
{
PrintError(netRuntimeException, origin);
}
else
{
// FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace...
PrintError($"{netRuntimeException.DecoratedMessage}: {netRuntimeException}", origin);
}
break;
case InterpreterException interpreterException:
if (interpreterException.DecoratedMessage == null)
{
PrintError(interpreterException, origin);
}
else
{
PrintError(interpreterException.DecoratedMessage, origin);
}
break;
default:
var msg = ex.StackTrace != null
? ex.ToString()
: $"{ex}\n{Environment.StackTrace}";
PrintError(msg, origin);
break;
}
}
public void HandleException(Exception ex, string extra = "", ExceptionType exceptionType = ExceptionType.Lua)
{
if (!string.IsNullOrWhiteSpace(extra))
if (exceptionType == ExceptionType.Lua) PrintError(extra);
else if (exceptionType == ExceptionType.CSharp) PrintCsError(extra);
else PrintBothError(extra);
if (ex is NetRuntimeException netRuntimeException)
{
if (netRuntimeException.DecoratedMessage == null)
{
PrintError(netRuntimeException);
}
else
{
PrintError(netRuntimeException.DecoratedMessage + ": " + netRuntimeException.ToString());
}
}
else if (ex is InterpreterException interpreterException)
{
if (interpreterException.DecoratedMessage == null)
{
PrintError(interpreterException);
}
else
{
PrintError(interpreterException.DecoratedMessage);
}
}
else
{
string msg = ex.StackTrace != null
? ex.ToString()
: $"{ex}\n{Environment.StackTrace}";
internal LuaCsExceptionHandler ExceptionHandler { get; set; }
if (exceptionType == ExceptionType.Lua) { PrintError(msg); }
else if (exceptionType == ExceptionType.CSharp) { PrintCsError(msg); }
else { PrintBothError(msg); }
}
}
private static void PrintErrorBase(string prefix, object message, string empty)
internal void HandleException(Exception ex, LuaCsMessageOrigin origin)
{
if (message == null) { message = empty; }
string str = message.ToString();
this.ExceptionHandler?.Invoke(ex, origin);
}
for (int i = 0; i < str.Length; i += 1024)
{
string subStr = str.Substring(i, Math.Min(1024, str.Length - i));
string errorMsg = subStr;
if (i == 0) errorMsg = prefix + errorMsg;
DebugConsole.ThrowError(errorMsg);
#if SERVER
if (GameMain.Server != null)
{
foreach (var c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(ChatMessage.Create("", errorMsg, ChatMessageType.Console, null, textColor: Color.Red), c);
}
GameServer.Log(errorMsg, ServerLog.MessageType.Error);
}
#endif
}
}
#if SERVER
public void PrintError(object message) => PrintErrorBase("[SV LUA ERROR] ", message, "nil");
public static void PrintCsError(object message) => PrintErrorBase("[SV CS ERROR] ", message, "Null");
public static void PrintBothError(object message) => PrintErrorBase("[SV ERROR] ", message, "Null");
#else
public void PrintError(object message) => PrintErrorBase("[CL LUA ERROR] ", message, "nil");
public static void PrintCsError(object message) => PrintErrorBase("[CL CS ERROR] ", message, "Null");
public static void PrintBothError(object message) => PrintErrorBase("[CL ERROR] ", message, "Null");
#endif
private static void PrintMessageBase(string prefix, object message, string empty)
private static void PrintErrorBase(string prefix, object message, string empty)
{
if (message == null) { message = empty; }
string str = message.ToString();
message ??= empty;
var str = message.ToString();
for (int i = 0; i < str.Length; i += 1024)
{
string subStr = str.Substring(i, Math.Min(1024, str.Length - i));
for (int i = 0; i < str.Length; i += 1024)
{
var subStr = str.Substring(i, Math.Min(1024, str.Length - i));
var errorMsg = subStr;
if (i == 0) errorMsg = prefix + errorMsg;
DebugConsole.ThrowError(errorMsg);
#if SERVER
if (GameMain.Server != null)
{
foreach (var c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(ChatMessage.Create("", subStr, ChatMessageType.Console, null, textColor: Color.MediumPurple), c);
}
if (GameMain.Server != null)
{
foreach (var c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(ChatMessage.Create("", errorMsg, ChatMessageType.Console, null, textColor: Color.Red), c);
}
GameServer.Log(prefix + subStr, ServerLog.MessageType.ServerMessage);
}
GameServer.Log(errorMsg, ServerLog.MessageType.Error);
}
#endif
}
}
}
#if SERVER
DebugConsole.NewMessage(message.ToString(), Color.MediumPurple);
private const string LOG_PREFIX = "SV";
#else
DebugConsole.NewMessage(message.ToString(), Color.Purple);
private const string LOG_PREFIX = "CL";
#endif
}
private void PrintMessage(object message) => PrintMessageBase("[LUA] ", message, "nil");
public static void PrintCsMessage(object message) => PrintMessageBase("[CS] ", message, "Null");
public static void PrintLogMessage(object message) => PrintMessageBase("[LuaCs LOG] ", message, "Null");
// TODO: deprecate this (in an effort to get rid of as much global state as possible)
public void PrintError(object o, LuaCsMessageOrigin origin)
{
switch (origin)
{
case LuaCsMessageOrigin.LuaCs:
PrintGenericError(o);
break;
case LuaCsMessageOrigin.LuaMod:
PrintLuaError(o);
break;
case LuaCsMessageOrigin.CSharpMod:
PrintCsError(o);
break;
}
}
private static void PrintLuaError(object o) => PrintErrorBase($"[{LOG_PREFIX} LUA ERROR] ", o, "nil");
// TODO: deprecate this
// XXX: this is only public so that we don't break backward compat with C# mods
public static void PrintCsError(object o) => PrintErrorBase($"[{LOG_PREFIX} CS ERROR] ", o, "Null");
private static void PrintGenericError(object o) => PrintErrorBase($"[{LOG_PREFIX} ERROR] ", o, "Null");
internal LuaCsMessageLogger MessageLogger { get; set; }
private static void DefaultMessageLogger(string prefix, object o)
{
var message = o.ToString();
for (int i = 0; i < message.Length; i += 1024)
{
var subStr = message.Substring(i, Math.Min(1024, message.Length - i));
#if SERVER
if (GameMain.Server != null)
{
foreach (var c in GameMain.Server.ConnectedClients)
{
GameMain.Server.SendDirectChatMessage(ChatMessage.Create("", subStr, ChatMessageType.Console, null, textColor: Color.MediumPurple), c);
}
GameServer.Log(prefix + subStr, ServerLog.MessageType.ServerMessage);
}
#endif
}
#if SERVER
DebugConsole.NewMessage(message.ToString(), Color.MediumPurple);
#else
DebugConsole.NewMessage(message.ToString(), Color.Purple);
#endif
}
private void PrintMessageBase(string prefix, object message, string empty) => MessageLogger?.Invoke(prefix, message ?? empty);
internal void PrintMessage(object message) => PrintMessageBase("[LuaCs] ", message, "nil");
// TODO: deprecate this (in an effort to get rid of as much global state as possible)
public static void PrintCsMessage(object message) => GameMain.LuaCs.PrintMessage(message);
private DynValue DoFile(string file, Table globalContext = null, string codeStringFriendly = null)
{
@@ -295,7 +328,7 @@ namespace Barotrauma
}
catch (Exception e)
{
HandleException(e);
HandleException(e, LuaCsMessageOrigin.LuaMod);
}
return null;
}
@@ -410,7 +443,7 @@ namespace Barotrauma
UserData.RegisterType<LuaCsPerformanceCounter>();
UserData.RegisterType<IUserDataDescriptor>();
lua.Globals["printerror"] = (Action<object>)PrintError;
lua.Globals["printerror"] = (Action<object>)PrintLuaError;
lua.Globals["setmodulepaths"] = (Action<string[]>)SetModulePaths;
@@ -465,13 +498,13 @@ namespace Barotrauma
}
catch (Exception ex)
{
HandleException(ex, exceptionType: ExceptionType.CSharp);
HandleException(ex, LuaCsMessageOrigin.CSharpMod);
}
});
}
catch (Exception ex)
{
HandleException(ex, exceptionType: ExceptionType.CSharp);
HandleException(ex, LuaCsMessageOrigin.CSharpMod);
}
}
@@ -491,7 +524,7 @@ namespace Barotrauma
}
catch (Exception e)
{
HandleException(e);
HandleException(e, LuaCsMessageOrigin.LuaMod);
}
}
else if (luaPackage != null)
@@ -507,20 +540,15 @@ namespace Barotrauma
}
catch (Exception e)
{
HandleException(e);
HandleException(e, LuaCsMessageOrigin.LuaMod);
}
}
else
{
PrintError("LuaSetup.lua not found! Lua/LuaSetup.lua, no Lua scripts will be executed or work.");
PrintLuaError("LuaSetup.lua not found! Lua/LuaSetup.lua, no Lua scripts will be executed or work.");
}
executionNumber++;
}
}
}
}
}

View File

@@ -68,7 +68,7 @@ namespace Barotrauma
}
catch (Exception e)
{
GameMain.LuaCs.HandleException(e, "", LuaCsSetup.ExceptionType.CSharp);
GameMain.LuaCs.HandleException(e, LuaCsMessageOrigin.CSharpMod);
}
timedActionsToRemove.Add(timedAction);

View File

@@ -79,7 +79,7 @@ namespace Barotrauma
return false;
}
public static bool IsPathAllowedException(string path, bool write = true, LuaCsSetup.ExceptionType exceptionType = LuaCsSetup.ExceptionType.Both)
public static bool IsPathAllowedException(string path, bool write = true, LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown)
{
if (write)
{
@@ -106,9 +106,9 @@ namespace Barotrauma
}
public static bool IsPathAllowedLuaException(string path, bool write = true) =>
IsPathAllowedException(path, write, LuaCsSetup.ExceptionType.Lua);
IsPathAllowedException(path, write, LuaCsMessageOrigin.LuaMod);
public static bool IsPathAllowedCsException(string path, bool write = true) =>
IsPathAllowedException(path, write, LuaCsSetup.ExceptionType.CSharp);
IsPathAllowedException(path, write, LuaCsMessageOrigin.CSharpMod);
public static string Read(string path)
{

View File

@@ -1,8 +1,7 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using System;
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using Xunit;
using Xunit.Abstractions;
@@ -14,9 +13,6 @@ namespace TestProject.LuaCs
public HookPatchTests(ITestOutputHelper output)
{
Console.SetOut(new TestOutputTextWriterAdapter(output));
Trace.Listeners.Add(new TestOutputTraceListenerAdapter(output));
UserData.RegisterType<TestValueType>();
UserData.RegisterType<IBogusInterface>();
UserData.RegisterType<InterfaceImplementingType>();
@@ -27,6 +23,18 @@ namespace TestProject.LuaCs
UserData.RegisterType<PatchTarget5>();
UserData.RegisterType<PatchTarget6>();
luaCs.MessageLogger = (prefix, o) =>
{
o ??= "null";
output.WriteLine(prefix + o);
};
luaCs.ExceptionHandler = (ex, _) =>
{
// Pretend we never caught the exception in the first place
// (this allows us to preserve the stack trace)
var di = ExceptionDispatchInfo.Capture(ex);
di.Throw();
};
luaCs.Initialize();
luaCs.Lua.Globals["TestValueType"] = UserData.CreateStatic<TestValueType>();
luaCs.Lua.Globals["InterfaceImplementingType"] = UserData.CreateStatic<InterfaceImplementingType>();

View File

@@ -1,25 +0,0 @@
using System;
using System.IO;
using System.Text;
using Xunit.Abstractions;
namespace TestProject.LuaCs
{
internal class TestOutputTextWriterAdapter : TextWriter
{
private readonly ITestOutputHelper output;
public TestOutputTextWriterAdapter(ITestOutputHelper output)
{
this.output = output;
}
public override Encoding Encoding => Encoding.UTF8;
public override void WriteLine(string? message) => output.WriteLine(message);
public override void WriteLine(string? format, params object?[] args) => output.WriteLine(format, args);
public override void Write(char value) => throw new NotImplementedException();
}
}

View File

@@ -1,20 +0,0 @@
using System;
using System.Diagnostics;
using Xunit.Abstractions;
namespace TestProject.LuaCs
{
internal class TestOutputTraceListenerAdapter : TraceListener
{
private readonly ITestOutputHelper output;
public TestOutputTraceListenerAdapter(ITestOutputHelper output)
{
this.output = output;
}
public override void Write(string? message) => throw new NotImplementedException();
public override void WriteLine(string? message) => output.WriteLine(message);
}
}