mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
206 lines
7.3 KiB
C#
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}");
|
|
}
|
|
}
|
|
}
|