using System;
using System.Collections.Generic;
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.
/// Supports incremental reading via since_token mechanism.
///
public static class ReadConsole
{
// SessionState keys for persisting token state across domain reloads
private const string SessionKeyTokenCounter = "ReadConsole_TokenCounter";
private const string SessionKeySinceToken = "ReadConsole_SinceToken";
private const string SessionKeyClearTimeTicks = "ReadConsole_ClearTimeTicks";
private const string SessionKeyEntryCountAtClear = "ReadConsole_EntryCountAtClear";
private const string SessionKeyTokenMap = "ReadConsole_TokenMap";
// Token generation state - backed by SessionState for persistence across domain reloads
private static readonly object _tokenLock = new object();
// Properties that persist via SessionState
private static long TokenCounter
{
get => SessionState.GetInt(SessionKeyTokenCounter, 0);
set => SessionState.SetInt(SessionKeyTokenCounter, (int)value);
}
private static string CurrentSinceToken
{
get => SessionState.GetString(SessionKeySinceToken, null);
set => SessionState.SetString(SessionKeySinceToken, value ?? string.Empty);
}
private static long ClearTimeTicks
{
get
{
var str = SessionState.GetString(SessionKeyClearTimeTicks, "0");
return long.TryParse(str, out var val) ? val : 0;
}
set => SessionState.SetString(SessionKeyClearTimeTicks, value.ToString());
}
private static int EntryCountAtClear
{
get => SessionState.GetInt(SessionKeyEntryCountAtClear, 0);
set => SessionState.SetInt(SessionKeyEntryCountAtClear, value);
}
// Token entry count map - serialized as JSON in SessionState
private static Dictionary GetTokenEntryCountMap()
{
var json = SessionState.GetString(SessionKeyTokenMap, "{}");
try
{
var jobj = Codely.Newtonsoft.Json.Linq.JObject.Parse(json);
var dict = new Dictionary();
foreach (var prop in jobj.Properties())
{
if (int.TryParse(prop.Value.ToString(), out var count))
{
dict[prop.Name] = count;
}
}
return dict;
}
catch
{
return new Dictionary();
}
}
private static void SaveTokenEntryCountMap(Dictionary map)
{
var jobj = new Codely.Newtonsoft.Json.Linq.JObject();
foreach (var kvp in map)
{
jobj[kvp.Key] = kvp.Value;
}
SessionState.SetString(SessionKeyTokenMap, jobj.ToString());
}
// 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 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?
// 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");
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);
if (_instanceIdField == null)
throw new Exception("Failed to reflect LogEntry.instanceID");
// (Calibration removed)
}
catch (Exception e)
{
Debug.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;
_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
|| _instanceIdField == null
)
{
// Log the error here as well for easier debugging in Unity Console
Debug.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."
);
}
string action = @params["action"]?.ToString().ToLower() ?? "get";
try
{
if (action == "clear")
{
// Extract scope parameter: "all" (default) or "errors_only"
string scope = (@params["scope"]?.ToString() ?? "all").ToLower();
return ClearConsole(scope);
}
else if (action == "get")
{
// Extract parameters for 'get'
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 sinceToken = @params["since_token"]?.ToString(); // NEW: since_token parameter
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // Legacy: timestamp filtering
string format = (@params["format"]?.ToString() ?? "detailed").ToLower();
bool includeStacktrace =
@params["includeStacktrace"]?.ToObject() ?? true;
if (types.Contains("all"))
{
types = new List { "error", "warning", "log" }; // Expand 'all'
}
// Prioritize since_token over sinceTimestamp
if (!string.IsNullOrEmpty(sinceToken))
{
return GetConsoleEntriesSinceToken(sinceToken, types, count, filterText, format, includeStacktrace);
}
else if (!string.IsNullOrEmpty(sinceTimestampStr))
{
Debug.LogWarning(
"[ReadConsole] Filtering by 'since_timestamp' is not currently implemented. Use 'since_token' instead."
);
}
return GetConsoleEntries(types, count, filterText, format, includeStacktrace);
}
else
{
return Response.Error(
$"Unknown action: '{action}'. Valid actions are 'get' or 'clear'."
);
}
}
catch (Exception e)
{
Debug.LogError($"[ReadConsole] Action '{action}' failed: {e}");
return Response.Error($"Internal error processing action '{action}': {e.Message}");
}
}
// --- 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).
Debug.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
// Generate a new since_token
string sinceToken;
lock (_tokenLock)
{
var clearTimeTicks = DateTime.UtcNow.Ticks;
ClearTimeTicks = clearTimeTicks;
var counter = TokenCounter + 1;
TokenCounter = counter;
sinceToken = $"{clearTimeTicks}-{counter}";
CurrentSinceToken = sinceToken;
// Record that at this token, there were 0 entries (console was just cleared)
EntryCountAtClear = 0;
// Update the token entry count map
var tokenMap = GetTokenEntryCountMap();
tokenMap[sinceToken] = 0;
// Clean up old tokens (keep only last 10)
if (tokenMap.Count > 10)
{
var oldTokens = tokenMap.Keys
.Where(k => k != sinceToken)
.OrderBy(k => k)
.Take(tokenMap.Count - 10)
.ToList();
foreach (var oldToken in oldTokens)
{
tokenMap.Remove(oldToken);
}
}
SaveTokenEntryCountMap(tokenMap);
}
// Update state revision and console state tracking
StateComposer.IncrementRevision();
StateComposer.UpdateConsoleState(sinceToken, 0, new object[0]);
// Return success with the since_token in data and state_delta
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
{
sinceToken = sinceToken,
scope = scope
},
state_delta = StateComposer.CreateConsoleDelta(sinceToken, 0, new object[0])
};
}
catch (Exception e)
{
Debug.LogError($"[ReadConsole] Failed to clear console: {e}");
return Response.Error($"Failed to clear console: {e.Message}");
}
}
private static object GetConsoleEntriesSinceToken(
string sinceToken,
List types,
int? count,
string filterText,
string format,
bool includeStacktrace
)
{
int startIndex = 0;
bool tokenValid = false;
string currentToken;
// Validate the since_token and determine start index
lock (_tokenLock)
{
currentToken = CurrentSinceToken;
if (string.IsNullOrEmpty(currentToken))
{
return Response.Error(
"No valid since_token available. Please call 'clear' first to obtain a since_token.",
new { sinceToken = (string)null }
);
}
if (sinceToken == currentToken)
{
// Token matches current - read entries after the clear point
startIndex = EntryCountAtClear;
tokenValid = true;
}
else
{
// Check if it's an older but still tracked token
var tokenMap = GetTokenEntryCountMap();
if (tokenMap.TryGetValue(sinceToken, out int recordedCount))
{
// Token is older but still tracked - use its recorded count
startIndex = recordedCount;
tokenValid = true;
Debug.LogWarning(
$"[ReadConsole] Using older token. Provided: {sinceToken}, Current: {currentToken}. " +
"Reading from recorded position."
);
}
else
{
// Token is unknown/expired - return all entries with warning
Debug.LogWarning(
$"[ReadConsole] since_token unknown or expired. Provided: {sinceToken}, Current: {currentToken}. " +
"Returning all entries. Please clear console and use the new token."
);
startIndex = 0;
tokenValid = false;
}
}
}
// Get entries starting from the determined index
var result = GetConsoleEntriesFromIndex(startIndex, types, count, filterText, format, includeStacktrace);
// Enhance the response with since_token information
if (result is System.Collections.IDictionary dict && dict.Contains("data"))
{
var dataObj = dict["data"];
int entryCount = 0;
if (dataObj is System.Collections.IList list)
{
entryCount = list.Count;
}
// Update console state with current unread count
var errors = ExtractErrorsFromResult(result);
StateComposer.UpdateConsoleState(currentToken, entryCount, errors);
// Add metadata about the token
var enhancedData = new
{
entries = dataObj,
sinceToken = currentToken,
tokenValidated = tokenValid,
providedToken = sinceToken,
startIndex = startIndex,
summary = new
{
total = entryCount,
filtered = !string.IsNullOrEmpty(filterText),
newSinceToken = tokenValid
}
};
return Response.Success(
tokenValid
? $"Retrieved {entryCount} new log entries since token."
: $"Retrieved {entryCount} log entries (token was invalid/expired).",
enhancedData
);
}
return result;
}
///
/// Extracts error entries from a result for state tracking.
///
private static object[] ExtractErrorsFromResult(object result)
{
var errors = new List