using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityTcp.Editor.Helpers; // For Response class
namespace UnityTcp.Editor.Tools
{
///
/// Handles reading and clearing Unity Editor console log entries.
/// Uses reflection to access internal LogEntry methods/properties.
/// To read only the output of an operation, clear the console first and read after.
///
public static class ReadConsole
{
private const int MaxConsoleEntries = 1000;
// Reflection members for accessing internal LogEntry data
// private static MethodInfo _getEntriesMethod; // Removed as it's unused and fails reflection
private static MethodInfo _startGettingEntriesMethod;
private static MethodInfo _endGettingEntriesMethod; // Renamed from _stopGettingEntriesMethod, trying End...
private static MethodInfo _clearMethod;
private static MethodInfo _getCountMethod;
private static MethodInfo _getEntryMethod;
private static PropertyInfo _consoleFlagsProperty;
private static MethodInfo _setConsoleFlagMethod;
private static MethodInfo _setFilteringTextMethod;
private static MethodInfo _getFilteringTextMethod;
private static FieldInfo _modeField;
private static FieldInfo _messageField;
private static FieldInfo _fileField;
private static FieldInfo _lineField;
private static FieldInfo _instanceIdField;
// Note: Timestamp is not directly available in LogEntry; need to parse message or find alternative?
private const int ConsoleFlagCollapse = 1 << 0;
private const int ConsoleFlagLogLevelLog = 1 << 7;
private const int ConsoleFlagLogLevelWarning = 1 << 8;
private const int ConsoleFlagLogLevelError = 1 << 9;
// Static constructor for reflection setup
static ReadConsole()
{
try
{
Type logEntriesType = typeof(EditorApplication).Assembly.GetType(
"UnityEditor.LogEntries"
);
if (logEntriesType == null)
throw new Exception("Could not find internal type UnityEditor.LogEntries");
// Include NonPublic binding flags as internal APIs might change accessibility
BindingFlags staticFlags =
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
BindingFlags instanceFlags =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
_startGettingEntriesMethod = logEntriesType.GetMethod(
"StartGettingEntries",
staticFlags
);
if (_startGettingEntriesMethod == null)
throw new Exception("Failed to reflect LogEntries.StartGettingEntries");
// Try reflecting EndGettingEntries based on warning message
_endGettingEntriesMethod = logEntriesType.GetMethod(
"EndGettingEntries",
staticFlags
);
if (_endGettingEntriesMethod == null)
throw new Exception("Failed to reflect LogEntries.EndGettingEntries");
_clearMethod = logEntriesType.GetMethod("Clear", staticFlags);
if (_clearMethod == null)
throw new Exception("Failed to reflect LogEntries.Clear");
_getCountMethod = logEntriesType.GetMethod("GetCount", staticFlags);
if (_getCountMethod == null)
throw new Exception("Failed to reflect LogEntries.GetCount");
_getEntryMethod = logEntriesType.GetMethod("GetEntryInternal", staticFlags);
if (_getEntryMethod == null)
throw new Exception("Failed to reflect LogEntries.GetEntryInternal");
// Optional: bypass Console toolbar filters (severity / Collapse / search).
_consoleFlagsProperty = logEntriesType.GetProperty("consoleFlags", staticFlags);
_setConsoleFlagMethod = logEntriesType.GetMethod(
"SetConsoleFlag",
staticFlags,
null,
new[] { typeof(int), typeof(bool) },
null
);
_setFilteringTextMethod = logEntriesType.GetMethod(
"SetFilteringText",
staticFlags,
null,
new[] { typeof(string) },
null
);
_getFilteringTextMethod = logEntriesType.GetMethod(
"GetFilteringText",
staticFlags,
null,
Type.EmptyTypes,
null
);
Type logEntryType = typeof(EditorApplication).Assembly.GetType(
"UnityEditor.LogEntry"
);
if (logEntryType == null)
throw new Exception("Could not find internal type UnityEditor.LogEntry");
_modeField = logEntryType.GetField("mode", instanceFlags);
if (_modeField == null)
throw new Exception("Failed to reflect LogEntry.mode");
_messageField = logEntryType.GetField("message", instanceFlags);
if (_messageField == null)
throw new Exception("Failed to reflect LogEntry.message");
_fileField = logEntryType.GetField("file", instanceFlags);
if (_fileField == null)
throw new Exception("Failed to reflect LogEntry.file");
_lineField = logEntryType.GetField("line", instanceFlags);
if (_lineField == null)
throw new Exception("Failed to reflect LogEntry.line");
_instanceIdField = logEntryType.GetField("instanceID", instanceFlags)
?? logEntryType.GetField("entityId", instanceFlags);
// Optional: log entry instance id is not used when reading messages.
// (Calibration removed)
}
catch (Exception e)
{
CodelyLogger.LogError(
$"[ReadConsole] Static Initialization Failed: Could not setup reflection for LogEntries/LogEntry. Console reading/clearing will likely fail. Specific Error: {e.Message}"
);
// Set members to null to prevent NullReferenceExceptions later, HandleCommand should check this.
_startGettingEntriesMethod =
_endGettingEntriesMethod =
_clearMethod =
_getCountMethod =
_getEntryMethod =
null;
_consoleFlagsProperty = null;
_setConsoleFlagMethod = null;
_setFilteringTextMethod = null;
_getFilteringTextMethod = null;
_modeField = _messageField = _fileField = _lineField = _instanceIdField = null;
}
}
// --- Main Handler ---
public static object HandleCommand(JObject @params)
{
// Check if ALL required reflection members were successfully initialized.
if (
_startGettingEntriesMethod == null
|| _endGettingEntriesMethod == null
|| _clearMethod == null
|| _getCountMethod == null
|| _getEntryMethod == null
|| _modeField == null
|| _messageField == null
|| _fileField == null
|| _lineField == null
)
{
// Log the error here as well for easier debugging in Unity Console
CodelyLogger.LogError(
"[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue."
);
return Response.Error(
"ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs."
);
}
return ActionRouter.Route(@params, ActionHandlers, defaultAction: "get");
}
private static readonly Dictionary> ActionHandlers =
new Dictionary>
{
{ "clear", ClearAction },
{ "get", GetAction },
};
private static object ClearAction(JObject @params)
{
string scope = (@params["scope"]?.ToString() ?? "all").ToLower();
return ClearConsole(scope);
}
private static object GetAction(JObject @params)
{
var types =
(@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList()
?? new List { "error", "warning", "log" };
int? count = @params["count"]?.ToObject();
string filterText = @params["filterText"]?.ToString();
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString();
string format = (@params["format"]?.ToString() ?? "detailed").ToLower();
bool includeStacktrace =
@params["includeStacktrace"]?.ToObject() ?? true;
if (types.Contains("all"))
{
types = new List { "error", "warning", "log" };
}
if (!string.IsNullOrEmpty(sinceTimestampStr))
{
CodelyLogger.LogWarning(
"[ReadConsole] Filtering by 'since_timestamp' is not currently implemented."
);
}
return GetConsoleEntries(types, count, filterText, format, includeStacktrace);
}
///
/// Reads the complete post-compile console for start_compilation_pipeline and
/// normalizes the public command response into a stable nested payload. Keeping this
/// adapter here ensures the standalone console tool and the compilation pipeline use the
/// exact same filtering, formatting, truncation and full-log-file behavior.
///
internal static JObject ReadForCompilationPipeline()
{
object raw = HandleCommand(new JObject
{
["action"] = "get",
["types"] = new JArray("all"),
["format"] = "detailed",
["includeStacktrace"] = true,
});
JObject response;
try
{
response = raw as JObject ?? JObject.FromObject(raw);
}
catch (Exception e)
{
return new JObject
{
["included"] = true,
["cleared"] = true,
["read_success"] = false,
["entries"] = new JArray(),
["error"] = $"Failed to serialize console response: {e.Message}",
};
}
bool readSuccess = response?["success"]?.ToObject() == true;
var result = new JObject
{
["included"] = true,
["cleared"] = true,
["read_success"] = readSuccess,
["entries"] = response?["data"] is JArray entries ? entries : new JArray(),
["truncated"] = response?["truncated"]?.ToObject() ?? false,
};
CopyIfPresent(response, result, "totalCount");
CopyIfPresent(response, result, "returnedCount");
CopyIfPresent(response, result, "fullLogFile");
if (!readSuccess)
{
result["error"] =
response?["message"]?.ToString()
?? response?["error"]?.ToString()
?? "Failed to read the Unity console after compilation.";
}
return result;
}
private static void CopyIfPresent(JObject source, JObject destination, string name)
{
if (source?[name] != null)
destination[name] = source[name].DeepClone();
}
// --- Action Implementations ---
///
/// Clears the console with optional scope.
///
/// "all" clears everything (default), "errors_only" clears only error messages
private static object ClearConsole(string scope = "all")
{
try
{
if (scope == "errors_only")
{
// Note: Unity's LogEntries.Clear() clears everything.
// For errors_only, we would need to iterate and selectively remove,
// which is not directly supported by Unity's API.
// For now, we log a warning and clear all (future enhancement could filter).
CodelyLogger.LogWarning("[ReadConsole] 'errors_only' scope is not fully supported by Unity's internal API. Clearing all messages.");
}
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
StateComposer.UpdateConsoleState(0, new object[0]);
return new
{
success = true,
message = scope == "errors_only"
? "Console cleared (errors_only scope limited by Unity API - all messages cleared)."
: "Console cleared successfully.",
data = new
{
scope = scope
}
};
}
catch (Exception e)
{
CodelyLogger.LogError($"[ReadConsole] Failed to clear console: {e}");
return Response.Error($"Failed to clear console: {e.Message}");
}
}
private static object[] ExtractErrorsFromList(System.Collections.IList list)
{
var errors = new List