Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 102 additions & 6 deletions MCPForUnity/Editor/Tools/ExecuteCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ public static class ExecuteCode

private static readonly List<HistoryEntry> _history = new List<HistoryEntry>();
private static string[] _cachedAssemblyPaths;
private static string[] _cachedCodeDomAssemblyPaths;

[UnityEditor.InitializeOnLoadMethod]
private static void OnDomainReload()
{
_cachedAssemblyPaths = null;
_cachedCodeDomAssemblyPaths = null;
RoslynCompiler.ResetCache();
}

Expand Down Expand Up @@ -340,16 +342,110 @@ 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 useCache = ReferenceEquals(allPaths, _cachedAssemblyPaths);
if (useCache && _cachedCodeDomAssemblyPaths != null)
return _cachedCodeDomAssemblyPaths;

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;

var result = DeduplicateAssemblyPathsForCodeDom(filtered);
if (useCache)
_cachedCodeDomAssemblyPaths = result;
return result;
}

private static string[] DeduplicateAssemblyPathsForCodeDom(string[] paths)
{
var candidates = new List<CodeDomAssemblyCandidate>();
var unresolvedPaths = new HashSet<string>(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<string>(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<string, int> GetLoadedAssemblyReferenceCounts()
{
var referenceCounts = new Dictionary<string, int>(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<string, int> 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;
}

return allPaths.Where(p =>
!_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray();
public string Path { get; }
public AssemblyName AssemblyName { get; }
}

// ──────────────────── Shared helpers ────────────────────
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CSharp;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using MCPForUnity.Editor.Tools;
Expand Down Expand Up @@ -367,8 +373,170 @@ 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);
}
}

[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()
{
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<CompilerError>()
.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
Expand Down
Loading