From 6f4c81224aa58505a3184b32b7f6bfd1220c2c4f Mon Sep 17 00:00:00 2001 From: XiaoLongHan Date: Thu, 30 Jul 2026 03:37:54 +0800 Subject: [PATCH 1/2] fix: deduplicate CodeDom assembly references --- MCPForUnity/Editor/Tools/ExecuteCode.cs | 99 ++++++++++++++- .../Tests/EditMode/Tools/ExecuteCodeTests.cs | 120 ++++++++++++++++++ 2 files changed, 213 insertions(+), 6 deletions(-) diff --git a/MCPForUnity/Editor/Tools/ExecuteCode.cs b/MCPForUnity/Editor/Tools/ExecuteCode.cs index 34d3f6679..fdef27d67 100644 --- a/MCPForUnity/Editor/Tools/ExecuteCode.cs +++ b/MCPForUnity/Editor/Tools/ExecuteCode.cs @@ -340,16 +340,103 @@ private static Assembly CodeDomCompile(string source, string[] assemblyPaths, ou "System.Collections", }; - private static string[] FilterAssemblyPathsForCodeDom(string[] allPaths) + internal static string[] FilterAssemblyPathsForCodeDom(string[] allPaths) { - bool hasNetstandard = allPaths.Any(p => + var hasNetstandard = allPaths.Any(p => string.Equals(Path.GetFileNameWithoutExtension(p), "netstandard", StringComparison.OrdinalIgnoreCase)); - if (!hasNetstandard) - return allPaths; + var filtered = hasNetstandard + ? allPaths.Where(p => + !_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray() + : allPaths; - return allPaths.Where(p => - !_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray(); + return DeduplicateAssemblyPathsForCodeDom(filtered); + } + + private static string[] DeduplicateAssemblyPathsForCodeDom(string[] paths) + { + var candidates = new List(); + var unresolvedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var path in paths) + { + try + { + candidates.Add(new CodeDomAssemblyCandidate(path, AssemblyName.GetAssemblyName(path))); + } + catch + { + unresolvedPaths.Add(path); + } + } + + var groups = candidates + .GroupBy(candidate => candidate.AssemblyName.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (groups.All(group => group.Count() == 1)) + return paths; + + var referenceCounts = GetLoadedAssemblyReferenceCounts(); + var selectedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var group in groups) + { + var selected = group + .OrderByDescending(candidate => GetReferenceCount(referenceCounts, candidate.AssemblyName.FullName)) + .ThenByDescending(candidate => candidate.AssemblyName.Version) + .ThenBy(candidate => candidate.Path, StringComparer.OrdinalIgnoreCase) + .First(); + selectedPaths.Add(selected.Path); + } + + return paths.Where(path => unresolvedPaths.Contains(path) || selectedPaths.Contains(path)).ToArray(); + } + + private static Dictionary GetLoadedAssemblyReferenceCounts() + { + var referenceCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies()) + { + if (assembly.IsDynamic) continue; + + AssemblyName[] referencedAssemblies; + try + { + referencedAssemblies = assembly.GetReferencedAssemblies(); + } + catch (NotSupportedException) + { + continue; + } + + foreach (var referencedAssembly in referencedAssemblies) + { + var fullName = referencedAssembly.FullName; + referenceCounts.TryGetValue(fullName, out var count); + referenceCounts[fullName] = count + 1; + } + } + + return referenceCounts; + } + + private static int GetReferenceCount(Dictionary referenceCounts, string fullName) + { + return referenceCounts.TryGetValue(fullName, out var count) ? count : 0; + } + + private sealed class CodeDomAssemblyCandidate + { + public CodeDomAssemblyCandidate(string path, AssemblyName assemblyName) + { + Path = path; + AssemblyName = assemblyName; + } + + public string Path { get; } + public AssemblyName AssemblyName { get; } } // ──────────────────── Shared helpers ──────────────────── diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs index 28331ac7a..30bc1a514 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs @@ -1,3 +1,8 @@ +using System; +using System.CodeDom.Compiler; +using System.IO; +using System.Linq; +using Microsoft.CSharp; using Newtonsoft.Json.Linq; using NUnit.Framework; using MCPForUnity.Editor.Tools; @@ -367,8 +372,123 @@ public void Execute_CodedomBackend_ResolvesUnityTypes() Assert.IsNotNull(result["data"]["result"]); } + [Test] + public void FilterAssemblyPathsForCodeDom_WithNetstandard_PreservesSystemSecurity() + { + var tempRoot = CreateTempDirectory(); + try + { + var netstandardPath = CompileVersionedAssembly(tempRoot, "netstandard", "2.0.0.0"); + var securityFixturePath = CompileVersionedAssembly(tempRoot, "SystemSecurityFixture", "4.0.0.0"); + var systemSecurityPath = Path.Combine( + Path.GetDirectoryName(securityFixturePath), + "System.Security.dll"); + File.Copy(securityFixturePath, systemSecurityPath); + + var filtered = ExecuteCode.FilterAssemblyPathsForCodeDom(new[] + { + netstandardPath, + systemSecurityPath, + }); + + CollectionAssert.Contains(filtered, systemSecurityPath); + } + finally + { + Directory.Delete(tempRoot, true); + } + } + + [Test] + public void FilterAssemblyPathsForCodeDom_DuplicateNames_PrefersReferencedVersion() + { + var tempRoot = CreateTempDirectory(); + try + { + var assemblyName = "McpCodeDomDuplicate" + Guid.NewGuid().ToString("N"); + var referencedPath = CompileVersionedAssembly(tempRoot, assemblyName, "1.0.0.0"); + var newerPath = CompileVersionedAssembly(tempRoot, assemblyName, "2.0.0.0"); + LoadAssemblyReferencing(referencedPath); + + var filtered = ExecuteCode.FilterAssemblyPathsForCodeDom(new[] + { + newerPath, + referencedPath, + }); + + Assert.AreEqual(1, filtered.Length); + Assert.AreEqual(referencedPath, filtered[0]); + } + finally + { + Directory.Delete(tempRoot, true); + } + } + // ──────────────────── Helpers ──────────────────── + private static string CreateTempDirectory() + { + var path = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(path); + return path; + } + + private static string CompileVersionedAssembly(string tempRoot, string assemblyName, string version) + { + var outputDirectory = Path.Combine(tempRoot, version); + Directory.CreateDirectory(outputDirectory); + var outputPath = Path.Combine(outputDirectory, assemblyName + ".dll"); + var source = + "using System.Reflection;\n" + + "[assembly: AssemblyVersion(\"" + version + "\")]\n" + + "public sealed class VersionMarker { }"; + + using (var provider = new CSharpCodeProvider()) + { + var parameters = new CompilerParameters + { + GenerateExecutable = false, + GenerateInMemory = false, + OutputAssembly = outputPath, + }; + var results = provider.CompileAssemblyFromSource(parameters, source); + AssertCompilerSuccess(results); + } + + return outputPath; + } + + private static void LoadAssemblyReferencing(string referencedAssemblyPath) + { + using (var provider = new CSharpCodeProvider()) + { + var parameters = new CompilerParameters + { + GenerateExecutable = false, + GenerateInMemory = true, + }; + parameters.ReferencedAssemblies.Add(referencedAssemblyPath); + + var results = provider.CompileAssemblyFromSource( + parameters, + "public static class ReferenceHolder { " + + "public static System.Type Get() { return typeof(VersionMarker); } }"); + AssertCompilerSuccess(results); + Assert.IsNotNull(results.CompiledAssembly); + } + } + + private static void AssertCompilerSuccess(CompilerResults results) + { + var errors = results.Errors + .Cast() + .Where(error => !error.IsWarning) + .Select(error => error.ToString()) + .ToArray(); + Assert.IsFalse(results.Errors.HasErrors, string.Join("\n", errors)); + } + private static JObject Execute(string code) { return ToJObject(ExecuteCode.HandleCommand(new JObject From d2247e942efa4cdb89c97d91d506cd9b04d0d197 Mon Sep 17 00:00:00 2001 From: XiaoLongHan Date: Mon, 3 Aug 2026 14:42:32 +0800 Subject: [PATCH 2/2] perf: cache CodeDom assembly paths --- MCPForUnity/Editor/Tools/ExecuteCode.cs | 11 ++++- .../Tests/EditMode/Tools/ExecuteCodeTests.cs | 48 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/MCPForUnity/Editor/Tools/ExecuteCode.cs b/MCPForUnity/Editor/Tools/ExecuteCode.cs index fdef27d67..0ec953e4f 100644 --- a/MCPForUnity/Editor/Tools/ExecuteCode.cs +++ b/MCPForUnity/Editor/Tools/ExecuteCode.cs @@ -30,11 +30,13 @@ public static class ExecuteCode private static readonly List _history = new List(); private static string[] _cachedAssemblyPaths; + private static string[] _cachedCodeDomAssemblyPaths; [UnityEditor.InitializeOnLoadMethod] private static void OnDomainReload() { _cachedAssemblyPaths = null; + _cachedCodeDomAssemblyPaths = null; RoslynCompiler.ResetCache(); } @@ -342,6 +344,10 @@ private static Assembly CodeDomCompile(string source, string[] assemblyPaths, ou internal static string[] FilterAssemblyPathsForCodeDom(string[] allPaths) { + var useCache = ReferenceEquals(allPaths, _cachedAssemblyPaths); + if (useCache && _cachedCodeDomAssemblyPaths != null) + return _cachedCodeDomAssemblyPaths; + var hasNetstandard = allPaths.Any(p => string.Equals(Path.GetFileNameWithoutExtension(p), "netstandard", StringComparison.OrdinalIgnoreCase)); @@ -350,7 +356,10 @@ internal static string[] FilterAssemblyPathsForCodeDom(string[] allPaths) !_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray() : allPaths; - return DeduplicateAssemblyPathsForCodeDom(filtered); + var result = DeduplicateAssemblyPathsForCodeDom(filtered); + if (useCache) + _cachedCodeDomAssemblyPaths = result; + return result; } private static string[] DeduplicateAssemblyPathsForCodeDom(string[] paths) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs index 30bc1a514..091fee72c 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ExecuteCodeTests.cs @@ -2,6 +2,7 @@ using System.CodeDom.Compiler; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.CSharp; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -425,6 +426,53 @@ public void FilterAssemblyPathsForCodeDom_DuplicateNames_PrefersReferencedVersio } } + [Test] + public void FilterAssemblyPathsForCodeDom_CachedAssemblyPaths_ReusesResultUntilDomainReload() + { + var tempRoot = CreateTempDirectory(); + var cachedAssemblyPathsField = typeof(ExecuteCode).GetField( + "_cachedAssemblyPaths", + BindingFlags.NonPublic | BindingFlags.Static); + var cachedCodeDomAssemblyPathsField = typeof(ExecuteCode).GetField( + "_cachedCodeDomAssemblyPaths", + BindingFlags.NonPublic | BindingFlags.Static); + var onDomainReload = typeof(ExecuteCode).GetMethod( + "OnDomainReload", + BindingFlags.NonPublic | BindingFlags.Static); + Assert.IsNotNull(cachedAssemblyPathsField); + Assert.IsNotNull(cachedCodeDomAssemblyPathsField); + Assert.IsNotNull(onDomainReload); + + try + { + onDomainReload.Invoke(null, null); + var assemblyName = "McpCodeDomCache" + Guid.NewGuid().ToString("N"); + var olderPath = CompileVersionedAssembly(tempRoot, assemblyName, "1.0.0.0"); + var newerPath = CompileVersionedAssembly(tempRoot, assemblyName, "2.0.0.0"); + var cachedAssemblyPaths = new[] { olderPath, newerPath }; + cachedAssemblyPathsField.SetValue(null, cachedAssemblyPaths); + + var first = ExecuteCode.FilterAssemblyPathsForCodeDom(cachedAssemblyPaths); + Assert.AreEqual(1, first.Length); + + File.WriteAllText(olderPath, "invalidated"); + File.WriteAllText(newerPath, "invalidated"); + var second = ExecuteCode.FilterAssemblyPathsForCodeDom(cachedAssemblyPaths); + Assert.AreSame(first, second); + + onDomainReload.Invoke(null, null); + cachedAssemblyPathsField.SetValue(null, cachedAssemblyPaths); + var afterReload = ExecuteCode.FilterAssemblyPathsForCodeDom(cachedAssemblyPaths); + Assert.AreNotSame(first, afterReload); + Assert.AreEqual(2, afterReload.Length); + } + finally + { + onDomainReload.Invoke(null, null); + Directory.Delete(tempRoot, true); + } + } + // ──────────────────── Helpers ──────────────────── private static string CreateTempDirectory()