Files
Drew Mochak 6d82b2e5fc Don't kill the chainloader when Unity log callbacks are stripped (#1389)
UnityLogSource's static constructor falls back to
Application.RegisterLogCallback when Application.logMessageReceived is
absent. Unity's managed code stripper can remove both, in which case
GetMethod returns null and the unguarded Invoke throws a
NullReferenceException out of the static constructor.

That exception propagates through chainloader initialisation, so the
chainloader never starts and no plugins load. The failure is close to
undiagnosable: LogOutput.log ends at "Preloader finished" with no error,
and the exception only appears on stdout.

Unity log forwarding is optional, so warn and continue instead of taking
the whole loader down.

Seen on Beacon Pines (Unity 2021.3.45f1, Mono), whose
UnityEngine.Application has neither member.
2026-09-01 06:56:05 +02:00

115 lines
3.7 KiB
C#

using System;
using System.Reflection;
using BepInEx.Logging;
using UnityEngine;
using Logger = BepInEx.Logging.Logger;
namespace BepInEx.Unity.Mono.Logging;
/// <summary>
/// Logs entries using Unity specific outputs.
/// </summary>
public class UnityLogSource : ILogSource
{
private bool disposed;
/// <summary>
/// Creates a new Unity log source.
/// </summary>
public UnityLogSource()
{
InternalUnityLogMessage += UnityLogMessageHandler;
}
/// <inheritdoc />
public string SourceName { get; } = "Unity Log";
/// <inheritdoc />
public event EventHandler<LogEventArgs> LogEvent;
/// <inheritdoc />
public void Dispose()
{
if (!disposed)
{
InternalUnityLogMessage -= UnityLogMessageHandler;
disposed = true;
}
}
private void UnityLogMessageHandler(object sender, LogEventArgs eventArgs)
{
var newEventArgs = new LogEventArgs(eventArgs.Data, eventArgs.Level, this);
LogEvent?.Invoke(this, newEventArgs);
}
#region Static Unity handler
private static event EventHandler<LogEventArgs> InternalUnityLogMessage;
static UnityLogSource()
{
var callback = new Application.LogCallback(OnUnityLogMessageReceived);
var logEvent =
typeof(Application).GetEvent("logMessageReceived", BindingFlags.Public | BindingFlags.Static);
if (logEvent != null)
{
logEvent.AddEventHandler(null, callback);
//UnsubscribeAction = () => logEvent.RemoveEventHandler(null, callback);
}
else
{
var registerLogCallback =
typeof(Application).GetMethod("RegisterLogCallback", BindingFlags.Public | BindingFlags.Static);
// Unity's managed code stripper can remove both Application.logMessageReceived and the
// legacy Application.RegisterLogCallback. Invoking a null MethodInfo here throws a
// NullReferenceException out of this static constructor, which propagates through
// Chainloader.Initialize and silently prevents the chainloader (and therefore every
// plugin) from starting. Unity log forwarding is optional, so degrade instead of
// taking the whole loader down with us.
if (registerLogCallback == null)
{
Logger.Log(LogLevel.Warning,
"Unity log forwarding is unavailable: neither " +
"Application.logMessageReceived nor Application.RegisterLogCallback exist " +
"in this build (they were most likely stripped). Unity log messages will " +
"not appear in the BepInEx log.");
return;
}
registerLogCallback.Invoke(null, new object[] { callback });
//UnsubscribeAction = () => registerLogCallback.Invoke(null, new object[] { null });
}
}
private static void OnUnityLogMessageReceived(string message, string stackTrace, LogType type)
{
LogLevel logLevel;
switch (type)
{
case LogType.Error:
case LogType.Assert:
case LogType.Exception:
logLevel = LogLevel.Error;
break;
case LogType.Warning:
logLevel = LogLevel.Warning;
break;
case LogType.Log:
default:
logLevel = LogLevel.Info;
break;
}
if (type == LogType.Exception)
message += $"\nStack trace:\n{stackTrace}";
InternalUnityLogMessage?.Invoke(null, new LogEventArgs(message, logLevel, null));
}
#endregion
}