using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MCPForUnity.Editor.Helpers; // For Response class
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace MCPForUnity.Editor.Tools
{
///
/// Handles reading and clearing Unity Editor console log entries.
/// Uses reflection to access internal LogEntry methods/properties.
///
[McpForUnityTool("read_console", AutoRegister = false)]
public static class ReadConsole
{
// (Calibration removed)
// 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)
{
McpLog.Error(
$"[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
McpLog.Error(
"[ReadConsole] HandleCommand called but reflection members are not initialized. Static constructor might have failed silently or there's an issue."
);
return new ErrorResponse(
"ReadConsole handler failed to initialize due to reflection errors. Cannot access console logs."
);
}
string action = @params["action"]?.ToString().ToLower() ?? "get";
try
{
if (action == "clear")
{
return ClearConsole();
}
else if (action == "get")
{
// Extract parameters for 'get'
var types =
(@params["types"] as JArray)?.Select(t => t.ToString().ToLower()).ToList()
?? new List { "error", "warning" };
int? count = @params["count"]?.ToObject();
int? pageSize =
@params["pageSize"]?.ToObject()
?? @params["page_size"]?.ToObject();
int? cursor = @params["cursor"]?.ToObject();
string filterText = @params["filterText"]?.ToString();
string sinceTimestampStr = @params["sinceTimestamp"]?.ToString(); // TODO: Implement timestamp filtering
string format = (@params["format"]?.ToString() ?? "plain").ToLower();
bool includeStacktrace =
@params["includeStacktrace"]?.ToObject() ?? false;
if (types.Contains("all"))
{
types = new List { "error", "warning", "log" }; // Expand 'all'
}
if (!string.IsNullOrEmpty(sinceTimestampStr))
{
McpLog.Warn(
"[ReadConsole] Filtering by 'since_timestamp' is not currently implemented."
);
// Need a way to get timestamp per log entry.
}
return GetConsoleEntries(
types,
count,
pageSize,
cursor,
filterText,
format,
includeStacktrace
);
}
else
{
return new ErrorResponse(
$"Unknown action: '{action}'. Valid actions are 'get' or 'clear'."
);
}
}
catch (Exception e)
{
McpLog.Error($"[ReadConsole] Action '{action}' failed: {e}");
return new ErrorResponse($"Internal error processing action '{action}': {e.Message}");
}
}
// --- Action Implementations ---
private static object ClearConsole()
{
try
{
_clearMethod.Invoke(null, null); // Static method, no instance, no parameters
return new SuccessResponse("Console cleared successfully.");
}
catch (Exception e)
{
McpLog.Error($"[ReadConsole] Failed to clear console: {e}");
return new ErrorResponse($"Failed to clear console: {e.Message}");
}
}
///
/// Retrieves console log entries with optional filtering and paging.
///
/// Log types to include (e.g., "error", "warning", "log").
/// Maximum entries to return in non-paging mode. Ignored when paging is active.
/// Number of entries per page. Defaults to 50 when omitted.
/// Starting index for paging (0-based). Defaults to 0.
/// Optional text filter (case-insensitive substring match).
/// Output format: "plain", "detailed", or "json".
/// Whether to include stack traces in the output.
/// A success response with entries, or an error response.
private static object GetConsoleEntries(
List types,
int? count,
int? pageSize,
int? cursor,
string filterText,
string format,
bool includeStacktrace
)
{
List