From c026cb49bf7855ff71170520620393d725d2be63 Mon Sep 17 00:00:00 2001 From: KamilDev Date: Sun, 23 Aug 2026 11:08:21 +1000 Subject: [PATCH] fix(read_console): stop the Console window's filters from hiding entries LogEntries filtering state is global and shared with the Console window. StartGettingEntries() honors both the toolbar's Log/Warning/Error severity toggles (ConsoleFlags bits 1<<7..1<<9) and the search box (SetFilteringText), so a toggle switched off or a leftover search query silently starves read_console of entries. The tool still reports success, so an agent reads "Retrieved 0 log entries" as a clean console -- worst case right after a compile, where it proceeds on broken state. Snapshot both, neutralize them for the duration of the read, and restore them in the finally block once the iteration session is closed, so the user's view is left exactly as they had it. The tool applies its own types and filterText arguments, which is what callers actually asked for. The search query is only cleared when GetFilteringText is also available, so a user's search is never discarded without a way to put it back. All four reflected members exist in 2021.3, 2022.3, 6000.0 and 6000.3, but they are reflected optionally: if a future Unity renames them, console reads degrade to the previous behavior instead of failing outright. Fixes #1239 --- MCPForUnity/Editor/Tools/ReadConsole.cs | 139 ++++++++++++++++++ .../Tests/EditMode/Tools/ReadConsoleTests.cs | 111 ++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/MCPForUnity/Editor/Tools/ReadConsole.cs b/MCPForUnity/Editor/Tools/ReadConsole.cs index b7ee1772a..6fa354178 100644 --- a/MCPForUnity/Editor/Tools/ReadConsole.cs +++ b/MCPForUnity/Editor/Tools/ReadConsole.cs @@ -30,6 +30,19 @@ public static class ReadConsole private static FieldInfo _messageField; private static FieldInfo _fileField; private static FieldInfo _lineField; + + // Optional reflection members: used to neutralize the Console window's own filters + // while reading. Absent members degrade to the previous (filter-inheriting) behavior. + private static PropertyInfo _consoleFlagsProperty; + private static MethodInfo _setFilteringTextMethod; + private static MethodInfo _getFilteringTextMethod; + + // Severity bits from the internal UnityEditor.ConsoleWindow.ConsoleFlags enum. + private const int ConsoleFlagLogLevelLog = 1 << 7; + private const int ConsoleFlagLogLevelWarning = 1 << 8; + private const int ConsoleFlagLogLevelError = 1 << 9; + private const int ConsoleFlagLogLevelMask = + ConsoleFlagLogLevelLog | ConsoleFlagLogLevelWarning | ConsoleFlagLogLevelError; // Static constructor for reflection setup static ReadConsole() @@ -99,6 +112,13 @@ static ReadConsole() if (_lineField == null) throw new Exception("Failed to reflect LogEntry.line"); + // Console window UI state. Present on every Unity version this package + // supports, but reflected optionally so a future rename degrades to the old + // behavior rather than disabling console reads outright. + _consoleFlagsProperty = logEntriesType.GetProperty("consoleFlags", staticFlags); + _setFilteringTextMethod = logEntriesType.GetMethod("SetFilteringText", staticFlags); + _getFilteringTextMethod = logEntriesType.GetMethod("GetFilteringText", staticFlags); + // (Calibration removed) } @@ -115,6 +135,8 @@ static ReadConsole() _getEntryMethod = null; _modeField = _messageField = _fileField = _lineField = null; + _consoleFlagsProperty = null; + _setFilteringTextMethod = _getFilteringTextMethod = null; } } @@ -202,6 +224,99 @@ public static object HandleCommand(JObject @params) // --- Action Implementations --- + /// + /// Forces the Console window's Log/Warning/Error severity bits on so that + /// StartGettingEntries reports every entry regardless of the toolbar toggles. + /// + /// The flags as they were, for restoration. + /// True when the flags were changed and must be restored. + private static bool TryForceLogLevelFlags(out int savedConsoleFlags) + { + savedConsoleFlags = 0; + if (_consoleFlagsProperty == null) + { + return false; + } + + try + { + savedConsoleFlags = (int)_consoleFlagsProperty.GetValue(null); + int forcedFlags = savedConsoleFlags | ConsoleFlagLogLevelMask; + if (forcedFlags == savedConsoleFlags) + { + return false; // Nothing is hidden; leave the property untouched. + } + + _consoleFlagsProperty.SetValue(null, forcedFlags); + return true; + } + catch (Exception e) + { + McpLog.Warn( + $"[ReadConsole] Could not override console severity flags; entries hidden by the Console window may be missing: {e.Message}" + ); + return false; + } + } + + private static void RestoreConsoleFlags(int savedConsoleFlags) + { + try + { + _consoleFlagsProperty.SetValue(null, savedConsoleFlags); + } + catch (Exception e) + { + McpLog.Error($"[ReadConsole] Failed to restore console severity flags: {e}"); + } + } + + /// + /// Clears the Console window's search query for the duration of a read. Only clears it + /// when it can also be read back, so a user's search is never silently discarded. + /// + /// The query as it was, for restoration. + /// True when the query was cleared and must be restored. + private static bool TryClearFilteringText(out string savedFilteringText) + { + savedFilteringText = null; + if (_setFilteringTextMethod == null || _getFilteringTextMethod == null) + { + return false; + } + + try + { + savedFilteringText = _getFilteringTextMethod.Invoke(null, null) as string; + if (string.IsNullOrEmpty(savedFilteringText)) + { + return false; // No search query active; nothing to neutralize. + } + + _setFilteringTextMethod.Invoke(null, new object[] { string.Empty }); + return true; + } + catch (Exception e) + { + McpLog.Warn( + $"[ReadConsole] Could not clear the console search filter; entries hidden by it may be missing: {e.Message}" + ); + return false; + } + } + + private static void RestoreFilteringText(string savedFilteringText) + { + try + { + _setFilteringTextMethod.Invoke(null, new object[] { savedFilteringText }); + } + catch (Exception e) + { + McpLog.Error($"[ReadConsole] Failed to restore the console search filter: {e}"); + } + } + private static object ClearConsole() { try @@ -246,8 +361,20 @@ bool includeStacktrace int resolvedCursor = Mathf.Max(0, cursor ?? 0); int pageEndExclusive = resolvedCursor + resolvedPageSize; + // LogEntries filtering state is global and shared with the Console window, so a + // severity toggle switched off or a leftover search query in the toolbar silently + // starves this tool of entries. Neutralize both for the duration of the read; the + // tool applies its own 'types' and 'filterText' arguments instead. + int savedConsoleFlags = 0; + bool consoleFlagsOverridden = false; + string savedFilteringText = null; + bool filteringTextOverridden = false; + try { + consoleFlagsOverridden = TryForceLogLevelFlags(out savedConsoleFlags); + filteringTextOverridden = TryClearFilteringText(out savedFilteringText); + // LogEntries requires calling Start/Stop around GetEntries/GetEntryInternal. // StartGettingEntries() returns the entry count — use it instead of GetCount() // which may return stale values within an active iteration session. @@ -392,6 +519,18 @@ bool includeStacktrace McpLog.Error($"[ReadConsole] Failed to call EndGettingEntries: {e}"); // Don't return error here as we might have valid data, but log it. } + + // Restore the Console window's filters once the iteration session is closed, + // so the user's view is left exactly as they had it. + if (filteringTextOverridden) + { + RestoreFilteringText(savedFilteringText); + } + + if (consoleFlagsOverridden) + { + RestoreConsoleFlags(savedConsoleFlags); + } } if (usePaging) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs index 190948348..7d5933519 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ReadConsoleTests.cs @@ -1,6 +1,8 @@ using System; +using System.Reflection; using Newtonsoft.Json.Linq; using NUnit.Framework; +using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Tools; using static MCPForUnityTests.Editor.TestUtilities; @@ -152,5 +154,114 @@ public void GetLogTypeFromMode_UnknownBits_FallBackToLog() Assert.AreEqual(LogType.Log, ReadConsole.GetLogTypeFromMode(1 << 14)); } + // The Console window's severity toggles and search box live on the shared internal + // UnityEditor.LogEntries state, so they leak into every StartGettingEntries caller. + // read_console must neutralize them for the duration of a read and restore them after. + + private const int ConsoleFlagLogLevelLog = 1 << 7; + private const int ConsoleFlagLogLevelWarning = 1 << 8; + + private static Type LogEntriesType => + typeof(EditorApplication).Assembly.GetType("UnityEditor.LogEntries"); + + private static PropertyInfo ConsoleFlagsProperty => LogEntriesType.GetProperty( + "consoleFlags", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + private static MethodInfo SetFilteringTextMethod => LogEntriesType.GetMethod( + "SetFilteringText", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + private static MethodInfo GetFilteringTextMethod => LogEntriesType.GetMethod( + "GetFilteringText", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + private static int ConsoleFlags + { + get => (int)ConsoleFlagsProperty.GetValue(null); + set => ConsoleFlagsProperty.SetValue(null, value); + } + + private static string FilteringText + { + get => (string)GetFilteringTextMethod.Invoke(null, null); + set => SetFilteringTextMethod.Invoke(null, new object[] { value }); + } + + private static JArray GetAllEntries() + { + var result = ToJObject(ReadConsole.HandleCommand(new JObject + { + ["action"] = "get", + ["types"] = new JArray { "error", "warning", "log" }, + ["format"] = "detailed", + ["count"] = 1000 + })); + Assert.IsTrue(result.Value("success"), result.ToString()); + return result["data"] as JArray; + } + + private static bool ContainsMessage(JArray entries, string needle) + { + if (entries == null) return false; + foreach (var entry in entries) + { + if (entry["message"]?.ToString().Contains(needle) == true) return true; + } + return false; + } + + [Test] + public void HandleCommand_Get_IgnoresConsoleSearchFilter() + { + string uniqueMessage = $"Search filter probe {Guid.NewGuid()}"; + string unrelatedQuery = $"no-entry-matches-{Guid.NewGuid()}"; + string originalFilter = FilteringText; + + try + { + Debug.Log(uniqueMessage); + FilteringText = unrelatedQuery; + + var entries = GetAllEntries(); + + Assert.IsTrue( + ContainsMessage(entries, uniqueMessage), + "read_console must return entries hidden by the Console window's search query."); + Assert.AreEqual( + unrelatedQuery, + FilteringText, + "read_console must leave the user's console search query untouched."); + } + finally + { + FilteringText = originalFilter ?? string.Empty; + } + } + + [Test] + public void HandleCommand_Get_IgnoresConsoleSeverityToggles() + { + string uniqueMessage = $"Severity toggle probe {Guid.NewGuid()}"; + int originalFlags = ConsoleFlags; + int hiddenFlags = originalFlags & ~(ConsoleFlagLogLevelLog | ConsoleFlagLogLevelWarning); + + try + { + Debug.Log(uniqueMessage); + ConsoleFlags = hiddenFlags; + + var entries = GetAllEntries(); + + Assert.IsTrue( + ContainsMessage(entries, uniqueMessage), + "read_console must return entries hidden by the Console window's severity toggles."); + Assert.AreEqual( + hiddenFlags, + ConsoleFlags, + "read_console must restore the Console window's severity toggles."); + } + finally + { + ConsoleFlags = originalFlags; + } + } } }