Files
Radicade/2023Patch/RadicadePatch/LogFloodGate.cs
T
2026-10-16 23:33:13 -07:00

206 lines
7.3 KiB
C#

using HarmonyLib;
using System;
using System.Collections.Generic;
using UnityEngine;
internal static class LogFloodGate
{
internal const double WindowSeconds = 30.0;
internal static bool Enabled => !_disabled;
private static readonly bool _disabled = IsDisabled();
private static readonly Dictionary<string, Entry> Seen = new Dictionary<string, Entry>();
private static long _totalSuppressed;
private static bool _selfTestPending;
private static int _selfTestFired;
private sealed class Entry
{
public double WindowStart;
public int Suppressed;
public int ReportedAt;
}
private static bool IsDisabled()
{
try
{
var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE");
if (string.IsNullOrWhiteSpace(raw)) return false;
return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no";
}
catch { return false; }
}
internal static void Apply(Harmony harmony)
{
try
{
var target = AccessTools.Method(typeof(ODGGOALOLEG), "LogException",
new[] { typeof(Il2CppSystem.Exception), typeof(UnityEngine.Object) });
if (target == null)
{
Warn("[LogGate] ODGGOALOLEG.LogException(Il2CppSystem.Exception, UnityEngine.Object) not found - " +
"not armed; the per-frame exception flood stays as-is.");
return;
}
var prefix = AccessTools.Method(typeof(LogFloodGate), nameof(Prefix));
if (prefix == null)
{
Warn("[LogGate] patch method Prefix not found - not armed.");
return;
}
harmony.Patch(target, prefix: new HarmonyMethod(prefix));
}
catch (Exception e)
{
Warn($"[LogGate] failed to arm ({e.GetType().Name}: {e.Message}); the exception flood stays as-is.");
}
}
[HarmonyPrefix]
internal static bool Prefix(Il2CppSystem.Exception __0)
{
if (!Enabled) return true;
try
{
if (_selfTestPending && System.Threading.Interlocked.Exchange(ref _selfTestFired, 1) == 0)
{
_selfTestPending = false;
RunSelfTest();
}
var signature = Signature(__0);
if (signature == null) return true;
var now = Time.realtimeSinceStartup;
lock (Seen)
{
if (Seen.Count > 512) Evict(now);
if (!Seen.TryGetValue(signature, out var entry))
{
Seen[signature] = new Entry { WindowStart = now };
return true;
}
if (now - entry.WindowStart < WindowSeconds)
{
entry.Suppressed++;
_totalSuppressed++;
if (entry.Suppressed == 1)
Warn($"[LogGate] repeats of this exception are now suppressed for {WindowSeconds:0}s at a time: {signature}");
else if (entry.Suppressed - entry.ReportedAt >= 500)
{
entry.ReportedAt = entry.Suppressed;
Warn($"[LogGate] {entry.Suppressed} identical repeats of this exception suppressed so far " +
$"(session total {_totalSuppressed}): {signature}");
}
return false;
}
var dropped = entry.Suppressed;
entry.WindowStart = now;
entry.Suppressed = 0;
entry.ReportedAt = 0;
if (dropped > 0)
Warn($"[LogGate] {dropped} identical repeats were collapsed in the previous {WindowSeconds:0}s window " +
$"(session total {_totalSuppressed}); logging this occurrence in full: {signature}");
return true;
}
}
catch
{
return true;
}
}
private static void Evict(double now)
{
var stale = new List<string>();
foreach (var pair in Seen)
if (now - pair.Value.WindowStart > 600.0)
stale.Add(pair.Key);
foreach (var key in stale)
Seen.Remove(key);
}
private static string Signature(Il2CppSystem.Exception exception)
{
string type, message, stack;
try { type = exception.GetType().Name; } catch { type = "Exception"; }
try { message = exception.Message; } catch { message = "<message unreadable>"; }
try { stack = exception.StackTrace; } catch { stack = null; }
if (string.IsNullOrEmpty(message) && string.IsNullOrEmpty(stack)) return null;
return Trim(type + ": " + message, 150) + " @ " + FirstFrame(stack);
}
private static string FirstFrame(string stack)
{
if (string.IsNullOrEmpty(stack)) return "<no stack>";
var start = stack.IndexOf(" at ", StringComparison.Ordinal);
if (start < 0)
{
var newline = stack.IndexOf('\n');
return Trim(newline < 0 ? stack : stack.Substring(0, newline), 120);
}
var end = stack.IndexOf('\n', start);
return Trim(end < 0 ? stack.Substring(start) : stack.Substring(start, end - start), 120);
}
private static string Trim(string value, int max)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
value = value.Replace('\r', ' ').Replace('\n', ' ').Trim();
return value.Length <= max ? value : value.Substring(0, max) + "...";
}
private static void Warn(string message)
{
try { RadicadeEacBypassPlugin.LogSource.LogWarning(message); } catch { }
}
internal static void RequestSelfTestIfAsked()
{
try
{
var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE_SELFTEST");
if (string.IsNullOrWhiteSpace(raw)) return;
if (raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no") return;
_selfTestPending = true;
RadicadeEacBypassPlugin.LogSource.LogInfo(
"[LogGate] self-test armed: the next exception through the game handler fires five identical ones to prove the gate (RADICADE_LOG_GATE_SELFTEST).");
}
catch (Exception e)
{
Warn($"[LogGate] self-test could not be armed: {e.GetType().Name}: {e.Message}");
}
}
private static void RunSelfTest()
{
try
{
var before = _totalSuppressed;
var exception = new Il2CppSystem.Exception("radicade log gate self-test");
const int shots = 5;
for (var i = 0; i < shots; i++)
UnityEngine.Debug.LogException(exception);
var collapsed = _totalSuppressed - before;
Warn($"[LogGate] self-test: fired {shots} identical exceptions through the live game handler; {collapsed} were collapsed, " +
$"{shots - collapsed} reached the log " +
$"({(collapsed == shots - 1 ? "PASS" : "FAIL")} - the first of a burst always logs in full, the rest are suppressed).");
}
catch (Exception e)
{
Warn($"[LogGate] self-test failed to run: {e.GetType().Name}: {e.Message}");
}
}
}