chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class AssemblyBuilderFallbackCompilerBackend
|
||||
{
|
||||
public static async Task<DynamicCompilationBackendResult> CompileAsync(
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
List<string> references,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
TaskCompletionSource<CompilerMessage[]> taskCompletionSource = new();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
incrementBuildCount();
|
||||
|
||||
string[] referenceArray = references != null
|
||||
? DynamicReferenceSetBuilder.MergeReferencesByAssemblyName(Array.Empty<string>(), references)
|
||||
: Array.Empty<string>();
|
||||
|
||||
AssemblyBuilder builder = new AssemblyBuilder(dllPath, sourcePath)
|
||||
{
|
||||
referencesOptions = ReferencesOptions.UseEngineModules,
|
||||
additionalReferences = referenceArray
|
||||
};
|
||||
|
||||
builder.buildFinished += (string assemblyPath, CompilerMessage[] compilerMessages) =>
|
||||
{
|
||||
taskCompletionSource.TrySetResult(compilerMessages);
|
||||
};
|
||||
_ = RegisterBuildFinishedContinuation(taskCompletionSource.Task, markBuildFinished);
|
||||
|
||||
bool started = builder.Build();
|
||||
if (!started)
|
||||
{
|
||||
return new DynamicCompilationBackendResult(
|
||||
new CompilerMessage[]
|
||||
{
|
||||
new CompilerMessage
|
||||
{
|
||||
type = CompilerMessageType.Error,
|
||||
message = "AssemblyBuilder.Build() failed to start compilation"
|
||||
}
|
||||
},
|
||||
DynamicCompilationBackendKind.AssemblyBuilderFallback);
|
||||
}
|
||||
|
||||
markBuildStarted();
|
||||
CompilerMessage[] messages = await AwaitBuildCompletionAsync(taskCompletionSource.Task, ct).ConfigureAwait(false);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return new DynamicCompilationBackendResult(
|
||||
messages,
|
||||
DynamicCompilationBackendKind.AssemblyBuilderFallback);
|
||||
}
|
||||
|
||||
internal static async Task<CompilerMessage[]> AwaitBuildCompletionAsync(
|
||||
Task<CompilerMessage[]> buildTask,
|
||||
CancellationToken ct)
|
||||
{
|
||||
TaskCompletionSource<CompilerMessage[]> cancellationTaskCompletionSource =
|
||||
new TaskCompletionSource<CompilerMessage[]>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
using CancellationTokenRegistration cancellationRegistration =
|
||||
ct.Register(
|
||||
static state => ((TaskCompletionSource<CompilerMessage[]>)state).TrySetCanceled(),
|
||||
cancellationTaskCompletionSource);
|
||||
|
||||
Task completedTask = await Task.WhenAny(buildTask, cancellationTaskCompletionSource.Task).ConfigureAwait(false);
|
||||
if (completedTask == cancellationTaskCompletionSource.Task)
|
||||
{
|
||||
await buildTask.ConfigureAwait(false);
|
||||
return await cancellationTaskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await buildTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal static Task RegisterBuildFinishedContinuation(
|
||||
Task<CompilerMessage[]> buildTask,
|
||||
Action markBuildFinished)
|
||||
{
|
||||
return buildTask.ContinueWith(
|
||||
static (_, state) => ((Action)state)(),
|
||||
markBuildFinished,
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14b708ec67cfd4eeb877b3a9fbf85de5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,218 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a type-name → namespace lookup from loaded assemblies.
|
||||
/// Used by AutoUsingResolver to find missing using directives without Roslyn.
|
||||
/// </summary>
|
||||
internal sealed class AssemblyTypeIndex
|
||||
{
|
||||
private static AssemblyTypeIndex _instance;
|
||||
private readonly Dictionary<string, HashSet<string>> _typeToNamespaces = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, HashSet<string>> _typeToAssemblyLocations = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, HashSet<string>> _qualifiedTypeToAssemblyLocations = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, HashSet<string>> _namespaceToAssemblyLocations = new(StringComparer.Ordinal);
|
||||
|
||||
public static AssemblyTypeIndex Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new AssemblyTypeIndex();
|
||||
_instance.Build();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
private static void InvalidateOnDomainReload()
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
|
||||
private void Build()
|
||||
{
|
||||
_typeToNamespaces.Clear();
|
||||
_typeToAssemblyLocations.Clear();
|
||||
_qualifiedTypeToAssemblyLocations.Clear();
|
||||
_namespaceToAssemblyLocations.Clear();
|
||||
|
||||
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (assembly.IsDynamic) continue;
|
||||
|
||||
string assemblyLocation = GetAssemblyLocation(assembly);
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = Array.FindAll(ex.Types, static t => t != null);
|
||||
}
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
if (type == null || string.IsNullOrEmpty(type.Namespace)) continue;
|
||||
if (!type.IsPublic) continue;
|
||||
|
||||
string typeName = type.Name;
|
||||
|
||||
// Strip generic arity suffix (e.g. "List`1" → "List")
|
||||
int backtick = typeName.IndexOf('`');
|
||||
if (backtick > 0)
|
||||
{
|
||||
typeName = typeName.Substring(0, backtick);
|
||||
}
|
||||
|
||||
if (!_typeToNamespaces.TryGetValue(typeName, out HashSet<string> namespaces))
|
||||
{
|
||||
namespaces = new HashSet<string>(StringComparer.Ordinal);
|
||||
_typeToNamespaces[typeName] = namespaces;
|
||||
}
|
||||
namespaces.Add(type.Namespace);
|
||||
|
||||
if (!string.IsNullOrEmpty(assemblyLocation))
|
||||
{
|
||||
RegisterTypeAssemblyLocation(typeName, assemblyLocation);
|
||||
RegisterQualifiedTypeAssemblyLocation(type.Namespace, typeName, assemblyLocation);
|
||||
RegisterNamespaceAssemblyLocation(type.Namespace, assemblyLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> FindNamespacesForType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName)) return new List<string>();
|
||||
|
||||
if (_typeToNamespaces.TryGetValue(typeName, out HashSet<string> namespaces))
|
||||
{
|
||||
return new List<string>(namespaces);
|
||||
}
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public List<string> FindAssemblyLocationsForType(string typeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
if (_typeToAssemblyLocations.TryGetValue(typeName, out HashSet<string> assemblyLocations))
|
||||
{
|
||||
return new List<string>(assemblyLocations);
|
||||
}
|
||||
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public List<string> FindAssemblyLocationsForIdentifier(string identifier)
|
||||
{
|
||||
if (string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
int terminalSeparatorIndex = identifier.LastIndexOf('.');
|
||||
string terminalTypeName = terminalSeparatorIndex >= 0
|
||||
? identifier.Substring(terminalSeparatorIndex + 1)
|
||||
: identifier;
|
||||
string currentIdentifier = identifier;
|
||||
while (true)
|
||||
{
|
||||
if (_qualifiedTypeToAssemblyLocations.TryGetValue(currentIdentifier, out HashSet<string> qualifiedTypeAssemblyLocations))
|
||||
{
|
||||
return new List<string>(qualifiedTypeAssemblyLocations);
|
||||
}
|
||||
|
||||
if (_namespaceToAssemblyLocations.TryGetValue(currentIdentifier, out HashSet<string> namespaceAssemblyLocations))
|
||||
{
|
||||
return new List<string>(namespaceAssemblyLocations);
|
||||
}
|
||||
|
||||
int separatorIndex = currentIdentifier.LastIndexOf('.');
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
currentIdentifier = currentIdentifier.Substring(0, separatorIndex);
|
||||
}
|
||||
|
||||
if (_typeToAssemblyLocations.TryGetValue(terminalTypeName, out HashSet<string> typeAssemblyLocations))
|
||||
{
|
||||
return new List<string>(typeAssemblyLocations);
|
||||
}
|
||||
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
private static string GetAssemblyLocation(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.Location;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterTypeAssemblyLocation(string typeName, string assemblyLocation)
|
||||
{
|
||||
if (!_typeToAssemblyLocations.TryGetValue(typeName, out HashSet<string> assemblyLocations))
|
||||
{
|
||||
assemblyLocations = new HashSet<string>(StringComparer.Ordinal);
|
||||
_typeToAssemblyLocations[typeName] = assemblyLocations;
|
||||
}
|
||||
|
||||
assemblyLocations.Add(assemblyLocation);
|
||||
}
|
||||
|
||||
private void RegisterQualifiedTypeAssemblyLocation(
|
||||
string namespaceName,
|
||||
string typeName,
|
||||
string assemblyLocation)
|
||||
{
|
||||
string qualifiedTypeName = $"{namespaceName}.{typeName}";
|
||||
if (!_qualifiedTypeToAssemblyLocations.TryGetValue(qualifiedTypeName, out HashSet<string> assemblyLocations))
|
||||
{
|
||||
assemblyLocations = new HashSet<string>(StringComparer.Ordinal);
|
||||
_qualifiedTypeToAssemblyLocations[qualifiedTypeName] = assemblyLocations;
|
||||
}
|
||||
|
||||
assemblyLocations.Add(assemblyLocation);
|
||||
}
|
||||
|
||||
private void RegisterNamespaceAssemblyLocation(string namespaceName, string assemblyLocation)
|
||||
{
|
||||
string[] namespaceParts = namespaceName.Split('.');
|
||||
string currentNamespace = string.Empty;
|
||||
|
||||
foreach (string namespacePart in namespaceParts)
|
||||
{
|
||||
currentNamespace = currentNamespace.Length == 0
|
||||
? namespacePart
|
||||
: $"{currentNamespace}.{namespacePart}";
|
||||
|
||||
if (!_namespaceToAssemblyLocations.TryGetValue(currentNamespace, out HashSet<string> assemblyLocations))
|
||||
{
|
||||
assemblyLocations = new HashSet<string>(StringComparer.Ordinal);
|
||||
_namespaceToAssemblyLocations[currentNamespace] = assemblyLocations;
|
||||
}
|
||||
|
||||
assemblyLocations.Add(assemblyLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49f2b146b90d84a68bcdc448c0678101
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves missing using directives by retrying compilation with candidate namespaces.
|
||||
/// Parses CS0246/CS0103 from CompilerMessage and looks up types via AssemblyTypeIndex.
|
||||
/// </summary>
|
||||
internal sealed class AutoUsingResolver
|
||||
{
|
||||
private const int MaxRetries = 3;
|
||||
|
||||
public async Task<AutoUsingResult> ResolveAsync(
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
string originalSource,
|
||||
List<string> currentReferences,
|
||||
System.Func<string, string, List<string>, CancellationToken, Task<CompilerMessage[]>> buildFunc,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Debug.Assert(originalSource != null, "originalSource must not be null");
|
||||
|
||||
string currentSource = originalSource;
|
||||
Dictionary<string, List<string>> ambiguousCandidates = new();
|
||||
HashSet<string> addedNamespaces = new(System.StringComparer.Ordinal);
|
||||
List<string> mutableReferences = currentReferences != null
|
||||
? new List<string>(currentReferences)
|
||||
: new List<string>();
|
||||
HashSet<string> addedAssemblyReferences = new(System.StringComparer.OrdinalIgnoreCase);
|
||||
double referenceResolutionMilliseconds = 0;
|
||||
|
||||
for (int attempt = 0; attempt < MaxRetries; attempt++)
|
||||
{
|
||||
File.WriteAllText(sourcePath, currentSource);
|
||||
// AssemblyBuilder retries must stay on Unity's main thread because the compiler API is not thread-safe.
|
||||
CompilerMessage[] messages = await buildFunc(sourcePath, dllPath, mutableReferences, ct);
|
||||
|
||||
List<string> unresolvedTypes = ExtractUnresolvedTypes(messages);
|
||||
if (unresolvedTypes.Count == 0)
|
||||
{
|
||||
return new AutoUsingResult(
|
||||
currentSource,
|
||||
messages,
|
||||
ambiguousCandidates,
|
||||
addedNamespaces,
|
||||
addedAssemblyReferences.ToList(),
|
||||
referenceResolutionMilliseconds);
|
||||
}
|
||||
|
||||
HashSet<string> namespacesToAdd = new(System.StringComparer.Ordinal);
|
||||
List<string> assemblyReferencesToAdd = new List<string>();
|
||||
AssemblyTypeIndex index = AssemblyTypeIndex.Instance;
|
||||
Stopwatch resolutionStopwatch = Stopwatch.StartNew();
|
||||
|
||||
foreach (string typeName in unresolvedTypes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(typeName)) continue;
|
||||
|
||||
List<string> candidates = index.FindNamespacesForType(typeName);
|
||||
|
||||
if (candidates.Count == 1 && addedNamespaces.Add(candidates[0]))
|
||||
{
|
||||
namespacesToAdd.Add(candidates[0]);
|
||||
}
|
||||
else if (candidates.Count > 1)
|
||||
{
|
||||
ambiguousCandidates[typeName] = candidates;
|
||||
}
|
||||
|
||||
List<string> assemblyCandidates = index.FindAssemblyLocationsForIdentifier(typeName);
|
||||
if (assemblyCandidates.Count == 1)
|
||||
{
|
||||
AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, mutableReferences, assemblyCandidates[0]);
|
||||
}
|
||||
}
|
||||
|
||||
resolutionStopwatch.Stop();
|
||||
referenceResolutionMilliseconds += resolutionStopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
if (namespacesToAdd.Count == 0 && assemblyReferencesToAdd.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (namespacesToAdd.Count > 0)
|
||||
{
|
||||
currentSource = InsertUsingDirectives(currentSource, namespacesToAdd);
|
||||
}
|
||||
|
||||
foreach (string assemblyReference in assemblyReferencesToAdd)
|
||||
{
|
||||
addedAssemblyReferences.Add(assemblyReference);
|
||||
mutableReferences.Add(assemblyReference);
|
||||
}
|
||||
}
|
||||
|
||||
// Final compilation with all added usings
|
||||
File.WriteAllText(sourcePath, currentSource);
|
||||
CompilerMessage[] finalMessages = await buildFunc(sourcePath, dllPath, mutableReferences, ct);
|
||||
return new AutoUsingResult(
|
||||
currentSource,
|
||||
finalMessages,
|
||||
ambiguousCandidates,
|
||||
addedNamespaces,
|
||||
addedAssemblyReferences.ToList(),
|
||||
referenceResolutionMilliseconds);
|
||||
}
|
||||
|
||||
private static List<string> ExtractUnresolvedTypes(CompilerMessage[] messages)
|
||||
{
|
||||
List<string> types = new();
|
||||
foreach (CompilerMessage msg in messages)
|
||||
{
|
||||
if (msg.type != CompilerMessageType.Error) continue;
|
||||
|
||||
// CS0246: The type or namespace name 'X' could not be found
|
||||
// CS0103: The name 'X' does not exist in the current context
|
||||
if (!msg.message.Contains("CS0246")
|
||||
&& !msg.message.Contains("CS0103")
|
||||
&& !msg.message.Contains("CS1069"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string typeName = CompilationDiagnosticMessageParser.ExtractTypeNameFromMessage(msg.message);
|
||||
if (!string.IsNullOrEmpty(typeName))
|
||||
{
|
||||
types.Add(typeName);
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
internal static string InsertUsingDirectives(string source, IEnumerable<string> namespaces)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (string ns in namespaces)
|
||||
{
|
||||
sb.AppendLine($"using {ns};");
|
||||
}
|
||||
sb.Append(source);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
internal static void AddAssemblyReferenceIfMissing(
|
||||
List<string> assemblyReferencesToAdd,
|
||||
List<string> currentReferences,
|
||||
string assemblyReference)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyReference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContainsAssemblyReference(currentReferences, assemblyReference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContainsAssemblyReference(assemblyReferencesToAdd, assemblyReference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
assemblyReferencesToAdd.Add(assemblyReference);
|
||||
}
|
||||
|
||||
internal static bool ContainsAssemblyReference(List<string> references, string assemblyReference)
|
||||
{
|
||||
string assemblyIdentity = GetAssemblyIdentityKey(assemblyReference);
|
||||
foreach (string existingReference in references)
|
||||
{
|
||||
if (string.Equals(
|
||||
GetAssemblyIdentityKey(existingReference),
|
||||
assemblyIdentity,
|
||||
System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GetAssemblyIdentityKey(string assemblyReference)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyReference))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(assemblyReference);
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
return assemblyReference;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class AutoUsingResult
|
||||
{
|
||||
public string UpdatedSource { get; }
|
||||
public CompilerMessage[] Messages { get; }
|
||||
public Dictionary<string, List<string>> AmbiguousTypeCandidates { get; }
|
||||
|
||||
public IReadOnlyCollection<string> AddedNamespaces { get; }
|
||||
|
||||
public IReadOnlyCollection<string> AddedAssemblyReferences { get; }
|
||||
|
||||
public double ReferenceResolutionMilliseconds { get; }
|
||||
|
||||
public AutoUsingResult(
|
||||
string updatedSource,
|
||||
CompilerMessage[] messages,
|
||||
Dictionary<string, List<string>> ambiguousTypeCandidates,
|
||||
IReadOnlyCollection<string> addedNamespaces,
|
||||
IReadOnlyCollection<string> addedAssemblyReferences,
|
||||
double referenceResolutionMilliseconds)
|
||||
{
|
||||
UpdatedSource = updatedSource;
|
||||
Messages = messages;
|
||||
AmbiguousTypeCandidates = ambiguousTypeCandidates;
|
||||
AddedNamespaces = addedNamespaces;
|
||||
AddedAssemblyReferences = addedAssemblyReferences;
|
||||
ReferenceResolutionMilliseconds = referenceResolutionMilliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee4e2b7b02dd448a6a7f1987cf21d258
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class CompiledAssemblyBuildResult
|
||||
{
|
||||
public string UpdatedSource { get; }
|
||||
|
||||
public CompilerDiagnostics Diagnostics { get; }
|
||||
|
||||
public Dictionary<string, List<string>> AmbiguousTypeCandidates { get; }
|
||||
|
||||
public List<string> AutoInjectedNamespaces { get; }
|
||||
|
||||
public byte[] AssemblyBytes { get; }
|
||||
|
||||
public double ReferenceResolutionMilliseconds { get; }
|
||||
|
||||
public double BuildMilliseconds { get; }
|
||||
|
||||
public int BuildCount { get; }
|
||||
|
||||
public bool ShouldCacheResult { get; }
|
||||
|
||||
public DynamicCompilationBackendKind CompilationBackendKind { get; }
|
||||
|
||||
public CompiledAssemblyBuildResult(
|
||||
string updatedSource,
|
||||
CompilerDiagnostics diagnostics,
|
||||
Dictionary<string, List<string>> ambiguousTypeCandidates,
|
||||
List<string> autoInjectedNamespaces,
|
||||
byte[] assemblyBytes,
|
||||
double referenceResolutionMilliseconds,
|
||||
double buildMilliseconds,
|
||||
int buildCount,
|
||||
bool shouldCacheResult,
|
||||
DynamicCompilationBackendKind compilationBackendKind)
|
||||
{
|
||||
UpdatedSource = updatedSource;
|
||||
Diagnostics = diagnostics;
|
||||
AmbiguousTypeCandidates = ambiguousTypeCandidates;
|
||||
AutoInjectedNamespaces = autoInjectedNamespaces;
|
||||
AssemblyBytes = assemblyBytes;
|
||||
ReferenceResolutionMilliseconds = referenceResolutionMilliseconds;
|
||||
BuildMilliseconds = buildMilliseconds;
|
||||
BuildCount = buildCount;
|
||||
ShouldCacheResult = shouldCacheResult;
|
||||
CompilationBackendKind = compilationBackendKind;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7d128b9fe39f427da5d828eb3b45d58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,374 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor.Compilation;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class CompiledAssemblyBuilder : ICompiledAssemblyBuilder
|
||||
{
|
||||
private static int _compileCounter;
|
||||
private static readonly string[] LiteralHoistingFallbackErrorCodes = { "CS0133", "CS0150", "CS0182", "CS1736" };
|
||||
|
||||
private readonly ExternalCompilerPathResolutionService _externalCompilerPathResolver;
|
||||
private readonly DynamicReferenceSetBuilderService _referenceSetBuilder;
|
||||
private readonly DynamicCompilationBackend _compilationBackend;
|
||||
|
||||
private sealed class BuildAttemptResult
|
||||
{
|
||||
public string UpdatedSource { get; }
|
||||
|
||||
public CompilerDiagnostics Diagnostics { get; }
|
||||
|
||||
public Dictionary<string, List<string>> AmbiguousTypeCandidates { get; }
|
||||
|
||||
public List<string> AutoInjectedNamespaces { get; }
|
||||
|
||||
public byte[] AssemblyBytes { get; }
|
||||
|
||||
public BuildAttemptResult(
|
||||
string updatedSource,
|
||||
CompilerDiagnostics diagnostics,
|
||||
Dictionary<string, List<string>> ambiguousTypeCandidates,
|
||||
List<string> autoInjectedNamespaces,
|
||||
byte[] assemblyBytes)
|
||||
{
|
||||
UpdatedSource = updatedSource;
|
||||
Diagnostics = diagnostics;
|
||||
AmbiguousTypeCandidates = ambiguousTypeCandidates;
|
||||
AutoInjectedNamespaces = autoInjectedNamespaces;
|
||||
AssemblyBytes = assemblyBytes;
|
||||
}
|
||||
}
|
||||
|
||||
public CompiledAssemblyBuilder(
|
||||
ExternalCompilerPathResolutionService externalCompilerPathResolver,
|
||||
DynamicReferenceSetBuilderService referenceSetBuilder,
|
||||
DynamicCompilationBackend compilationBackend)
|
||||
{
|
||||
_externalCompilerPathResolver = externalCompilerPathResolver;
|
||||
_referenceSetBuilder = referenceSetBuilder;
|
||||
_compilationBackend = compilationBackend;
|
||||
}
|
||||
|
||||
public bool SupportsAutoPrewarm()
|
||||
{
|
||||
return SupportsAutoPrewarm(_externalCompilerPathResolver.Resolve(), Application.platform);
|
||||
}
|
||||
|
||||
internal static bool SupportsAutoPrewarm(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
RuntimePlatform platform)
|
||||
{
|
||||
return externalCompilerPaths != null;
|
||||
}
|
||||
|
||||
public async Task<CompiledAssemblyBuildResult> BuildAsync(
|
||||
DynamicCompilationPlan plan,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
Debug.Assert(plan != null, "plan must not be null");
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
ExternalCompilerPaths externalCompilerPaths = _externalCompilerPathResolver.Resolve();
|
||||
string tempDirectoryPath = Path.Combine("Temp", "uLoopMCPCompilation");
|
||||
int compileCounter = Interlocked.Increment(ref _compileCounter);
|
||||
string uniqueName = CreateUniqueCompilationName(plan.ClassName, compileCounter);
|
||||
string sourcePath = Path.Combine(tempDirectoryPath, $"{uniqueName}.cs");
|
||||
string dllPath = Path.Combine(tempDirectoryPath, $"{uniqueName}.dll");
|
||||
bool canDeleteTempFiles = true;
|
||||
double referenceResolutionMilliseconds = 0;
|
||||
double buildMilliseconds = 0;
|
||||
int buildCount = 0;
|
||||
DynamicCompilationBackendKind compilationBackendKind = DynamicCompilationBackendKind.Unknown;
|
||||
|
||||
Directory.CreateDirectory(tempDirectoryPath);
|
||||
|
||||
try
|
||||
{
|
||||
async Task<CompilerMessage[]> BuildFunc(
|
||||
string resolvedSourcePath,
|
||||
string resolvedDllPath,
|
||||
List<string> resolvedReferences,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Stopwatch buildStopwatch = Stopwatch.StartNew();
|
||||
DynamicCompilationBackendResult backendResult = await _compilationBackend.CompileAsync(
|
||||
resolvedSourcePath,
|
||||
resolvedDllPath,
|
||||
resolvedReferences,
|
||||
externalCompilerPaths,
|
||||
cancellationToken,
|
||||
() => canDeleteTempFiles = false,
|
||||
() => canDeleteTempFiles = true,
|
||||
() => buildCount++);
|
||||
compilationBackendKind = backendResult.BackendKind;
|
||||
buildStopwatch.Stop();
|
||||
buildMilliseconds += buildStopwatch.Elapsed.TotalMilliseconds;
|
||||
return backendResult.CompilerMessages;
|
||||
}
|
||||
|
||||
BuildAttemptResult attemptResult = await BuildPreparedCodeAsync(plan.PreparedCode, ct);
|
||||
bool shouldCacheResult = true;
|
||||
|
||||
if (ShouldRetryWithoutLiteralHoisting(plan.PreparedCode, attemptResult.Diagnostics))
|
||||
{
|
||||
PreparedDynamicCode fallbackPreparedCode = DynamicCodeSourcePreparer.PrepareWithoutLiteralHoisting(
|
||||
plan.OriginalRequest.Code,
|
||||
plan.NamespaceName,
|
||||
plan.ClassName);
|
||||
attemptResult = await BuildPreparedCodeAsync(fallbackPreparedCode, ct);
|
||||
shouldCacheResult = false;
|
||||
}
|
||||
|
||||
return new CompiledAssemblyBuildResult(
|
||||
attemptResult.UpdatedSource,
|
||||
attemptResult.Diagnostics,
|
||||
attemptResult.AmbiguousTypeCandidates,
|
||||
attemptResult.AutoInjectedNamespaces,
|
||||
attemptResult.AssemblyBytes,
|
||||
referenceResolutionMilliseconds,
|
||||
buildMilliseconds,
|
||||
buildCount,
|
||||
shouldCacheResult,
|
||||
compilationBackendKind);
|
||||
|
||||
async Task<BuildAttemptResult> BuildPreparedCodeAsync(
|
||||
PreparedDynamicCode preparedCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string wrappedCode = preparedCode.PreparedSource;
|
||||
string originalWrappedCode = wrappedCode;
|
||||
bool preUsingAdded = false;
|
||||
PreUsingResult preUsingResult = null;
|
||||
|
||||
Stopwatch initialReferenceResolutionStopwatch = Stopwatch.StartNew();
|
||||
List<string> initialReferences = BuildInitialReferences(
|
||||
plan,
|
||||
externalCompilerPaths,
|
||||
preparedCode,
|
||||
ref wrappedCode,
|
||||
ref preUsingAdded,
|
||||
ref preUsingResult);
|
||||
initialReferenceResolutionStopwatch.Stop();
|
||||
referenceResolutionMilliseconds += initialReferenceResolutionStopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
AutoUsingResolver resolver = new AutoUsingResolver();
|
||||
AutoUsingResult autoResult = await resolver.ResolveAsync(
|
||||
sourcePath,
|
||||
dllPath,
|
||||
wrappedCode,
|
||||
initialReferences,
|
||||
BuildFunc,
|
||||
cancellationToken);
|
||||
referenceResolutionMilliseconds += autoResult.ReferenceResolutionMilliseconds;
|
||||
|
||||
wrappedCode = autoResult.UpdatedSource;
|
||||
CompilerDiagnostics diagnostics = CompilerDiagnostics.FromMessages(autoResult.Messages);
|
||||
|
||||
bool preUsingRolledBack = false;
|
||||
if (diagnostics.Errors.Count > 0 && preUsingAdded && diagnostics.HasAmbiguityErrors)
|
||||
{
|
||||
Stopwatch rollbackReferenceResolutionStopwatch = Stopwatch.StartNew();
|
||||
List<string> rollbackReferences = _referenceSetBuilder.BuildReferenceSet(
|
||||
plan.OriginalRequest.AdditionalReferences,
|
||||
null,
|
||||
externalCompilerPaths);
|
||||
rollbackReferenceResolutionStopwatch.Stop();
|
||||
referenceResolutionMilliseconds += rollbackReferenceResolutionStopwatch.Elapsed.TotalMilliseconds;
|
||||
|
||||
AutoUsingResult rollbackResult = await resolver.ResolveAsync(
|
||||
sourcePath,
|
||||
dllPath,
|
||||
originalWrappedCode,
|
||||
rollbackReferences,
|
||||
BuildFunc,
|
||||
cancellationToken);
|
||||
referenceResolutionMilliseconds += rollbackResult.ReferenceResolutionMilliseconds;
|
||||
|
||||
CompilerDiagnostics rollbackDiagnostics = CompilerDiagnostics.FromMessages(rollbackResult.Messages);
|
||||
if (rollbackDiagnostics.Errors.Count < diagnostics.Errors.Count)
|
||||
{
|
||||
wrappedCode = rollbackResult.UpdatedSource;
|
||||
diagnostics = rollbackDiagnostics;
|
||||
autoResult = rollbackResult;
|
||||
preUsingRolledBack = true;
|
||||
}
|
||||
}
|
||||
|
||||
List<string> autoInjectedNamespaces = MergeAutoInjectedNamespaces(
|
||||
preUsingRolledBack,
|
||||
preUsingResult,
|
||||
autoResult);
|
||||
|
||||
byte[] assemblyBytes = null;
|
||||
if (diagnostics.Errors.Count == 0)
|
||||
{
|
||||
assemblyBytes = File.ReadAllBytes(dllPath);
|
||||
}
|
||||
|
||||
return new BuildAttemptResult(
|
||||
wrappedCode,
|
||||
diagnostics,
|
||||
autoResult.AmbiguousTypeCandidates,
|
||||
autoInjectedNamespaces,
|
||||
assemblyBytes);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (canDeleteTempFiles)
|
||||
{
|
||||
File.Delete(sourcePath);
|
||||
File.Delete(dllPath);
|
||||
File.Delete(Path.ChangeExtension(dllPath, ".pdb"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static string CreateUniqueCompilationName(string className, int compileCounter)
|
||||
{
|
||||
string safeClassName = SanitizeCompilationFileNameSegment(className);
|
||||
return $"{safeClassName}_{compileCounter}";
|
||||
}
|
||||
|
||||
internal static string SanitizeCompilationFileNameSegment(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return DynamicCodeConstants.DEFAULT_CLASS_NAME;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(value.Length);
|
||||
foreach (char ch in value)
|
||||
{
|
||||
builder.Append(IsUnsafeFileNameCharacter(ch) ? '_' : ch);
|
||||
}
|
||||
|
||||
string sanitized = builder.ToString().Trim('.');
|
||||
if (string.IsNullOrWhiteSpace(sanitized))
|
||||
{
|
||||
return DynamicCodeConstants.DEFAULT_CLASS_NAME;
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static bool IsUnsafeFileNameCharacter(char value)
|
||||
{
|
||||
if (value == Path.DirectorySeparatorChar || value == Path.AltDirectorySeparatorChar)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case '<':
|
||||
case '>':
|
||||
case ':':
|
||||
case '"':
|
||||
case '/':
|
||||
case '\\':
|
||||
case '|':
|
||||
case '?':
|
||||
case '*':
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (char invalidChar in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
if (value == invalidChar)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<string> BuildInitialReferences(
|
||||
DynamicCompilationPlan plan,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
PreparedDynamicCode preparedCode,
|
||||
ref string wrappedCode,
|
||||
ref bool preUsingAdded,
|
||||
ref PreUsingResult preUsingResult)
|
||||
{
|
||||
if (!preparedCode.IsScriptMode)
|
||||
{
|
||||
return _referenceSetBuilder.BuildReferenceSet(
|
||||
plan.OriginalRequest.AdditionalReferences,
|
||||
null,
|
||||
externalCompilerPaths);
|
||||
}
|
||||
|
||||
preUsingResult = PreUsingResolver.Resolve(wrappedCode, AssemblyTypeIndex.Instance);
|
||||
preUsingAdded = !ReferenceEquals(preUsingResult.UpdatedSource, wrappedCode);
|
||||
wrappedCode = preUsingResult.UpdatedSource;
|
||||
return _referenceSetBuilder.BuildReferenceSet(
|
||||
plan.OriginalRequest.AdditionalReferences,
|
||||
preUsingResult.AddedAssemblyReferences,
|
||||
externalCompilerPaths);
|
||||
}
|
||||
|
||||
private static bool ShouldRetryWithoutLiteralHoisting(
|
||||
PreparedDynamicCode preparedCode,
|
||||
CompilerDiagnostics diagnostics)
|
||||
{
|
||||
if (preparedCode == null || diagnostics == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preparedCode.IsScriptMode || preparedCode.HoistedLiteralBindings.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (CompilationError error in diagnostics.Errors)
|
||||
{
|
||||
foreach (string errorCode in LiteralHoistingFallbackErrorCodes)
|
||||
{
|
||||
if (error.ErrorCode == errorCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<string> MergeAutoInjectedNamespaces(
|
||||
bool preUsingRolledBack,
|
||||
PreUsingResult preUsingResult,
|
||||
AutoUsingResult autoResult)
|
||||
{
|
||||
List<string> mergedNamespaces = new List<string>();
|
||||
|
||||
if (!preUsingRolledBack && preUsingResult != null && preUsingResult.AddedNamespaces.Count > 0)
|
||||
{
|
||||
foreach (string namespaceName in preUsingResult.AddedNamespaces)
|
||||
{
|
||||
mergedNamespaces.Add(namespaceName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string namespaceName in autoResult.AddedNamespaces)
|
||||
{
|
||||
if (!mergedNamespaces.Contains(namespaceName))
|
||||
{
|
||||
mergedNamespaces.Add(namespaceName);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedNamespaces;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe943af28806b49428a19a663d2b976d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using Assembly = System.Reflection.Assembly;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class CompiledAssemblyLoadResult
|
||||
{
|
||||
public bool Success { get; }
|
||||
|
||||
public Assembly CompiledAssembly { get; }
|
||||
|
||||
public List<SecurityViolation> SecurityViolations { get; }
|
||||
|
||||
public double AssemblyLoadMilliseconds { get; }
|
||||
|
||||
public CompiledAssemblyLoadResult(
|
||||
bool success,
|
||||
Assembly compiledAssembly,
|
||||
List<SecurityViolation> securityViolations,
|
||||
double assemblyLoadMilliseconds)
|
||||
{
|
||||
Success = success;
|
||||
CompiledAssembly = compiledAssembly;
|
||||
SecurityViolations = securityViolations;
|
||||
AssemblyLoadMilliseconds = assemblyLoadMilliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbf74301aec6044fe9761f7fa20286e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class CompiledAssemblyLoadService : ICompiledAssemblyLoader
|
||||
{
|
||||
public CompiledAssemblyLoadResult Load(
|
||||
DynamicCodeSecurityLevel securityLevel,
|
||||
byte[] assemblyBytes)
|
||||
{
|
||||
return CompiledAssemblyLoader.Load(securityLevel, assemblyBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f14bb6d62e8364e858e6e573fa56f526
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Assembly = System.Reflection.Assembly;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class CompiledAssemblyLoader
|
||||
{
|
||||
public static CompiledAssemblyLoadResult Load(
|
||||
DynamicCodeSecurityLevel securityLevel,
|
||||
byte[] assemblyBytes)
|
||||
{
|
||||
Debug.Assert(assemblyBytes != null, "assemblyBytes must not be null");
|
||||
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
|
||||
if (securityLevel == DynamicCodeSecurityLevel.Restricted)
|
||||
{
|
||||
SecurityValidationResult preloadValidationResult = ValidateBeforeAssemblyLoad(assemblyBytes);
|
||||
if (!preloadValidationResult.IsValid)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
return new CompiledAssemblyLoadResult(
|
||||
false,
|
||||
null,
|
||||
preloadValidationResult.Violations,
|
||||
stopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
Assembly compiledAssembly = Assembly.Load(assemblyBytes);
|
||||
if (securityLevel == DynamicCodeSecurityLevel.Restricted)
|
||||
{
|
||||
IlSecurityValidator postLoadValidator = new IlSecurityValidator();
|
||||
SecurityValidationResult postLoadValidationResult = postLoadValidator.Validate(compiledAssembly);
|
||||
if (!postLoadValidationResult.IsValid)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
return new CompiledAssemblyLoadResult(
|
||||
false,
|
||||
null,
|
||||
postLoadValidationResult.Violations,
|
||||
stopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
return new CompiledAssemblyLoadResult(
|
||||
true,
|
||||
compiledAssembly,
|
||||
new List<SecurityViolation>(),
|
||||
stopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
private static SecurityValidationResult ValidateBeforeAssemblyLoad(byte[] assemblyBytes)
|
||||
{
|
||||
SecurityValidationResult aggregatedResult = new SecurityValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Violations = new List<SecurityViolation>(),
|
||||
CompilationErrors = new List<string>()
|
||||
};
|
||||
bool shouldRunMetadataFallback = true;
|
||||
|
||||
IPreloadAssemblySecurityValidator registeredValidator;
|
||||
if (PreloadAssemblySecurityValidatorRegistry.TryGetValidator(out registeredValidator))
|
||||
{
|
||||
MergeValidationResult(aggregatedResult, registeredValidator.Validate(assemblyBytes));
|
||||
shouldRunMetadataFallback = registeredValidator is not IOverrideDefaultPreloadAssemblySecurityValidation;
|
||||
}
|
||||
|
||||
if (shouldRunMetadataFallback)
|
||||
{
|
||||
SystemReflectionMetadataPreloadValidator fallbackValidator = new SystemReflectionMetadataPreloadValidator();
|
||||
MergeValidationResult(aggregatedResult, fallbackValidator.Validate(assemblyBytes));
|
||||
}
|
||||
|
||||
PreloadIlSecurityValidator ilValidator = new PreloadIlSecurityValidator();
|
||||
MergeValidationResult(aggregatedResult, ilValidator.Validate(assemblyBytes));
|
||||
|
||||
aggregatedResult.IsValid = aggregatedResult.Violations.Count == 0 &&
|
||||
aggregatedResult.CompilationErrors.Count == 0;
|
||||
return aggregatedResult;
|
||||
}
|
||||
|
||||
private static void MergeValidationResult(
|
||||
SecurityValidationResult target,
|
||||
SecurityValidationResult source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HashSet<string> seenViolations = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
foreach (SecurityViolation violation in target.Violations)
|
||||
{
|
||||
seenViolations.Add(CreateViolationKey(violation));
|
||||
}
|
||||
|
||||
if (source.Violations != null)
|
||||
{
|
||||
foreach (SecurityViolation violation in source.Violations)
|
||||
{
|
||||
if (seenViolations.Add(CreateViolationKey(violation)))
|
||||
{
|
||||
target.Violations.Add(violation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source.CompilationErrors == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string error in source.CompilationErrors)
|
||||
{
|
||||
if (!target.CompilationErrors.Contains(error))
|
||||
{
|
||||
target.CompilationErrors.Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string CreateViolationKey(SecurityViolation violation)
|
||||
{
|
||||
return $"{violation.Location}|{violation.ApiName}|{violation.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84266c80ce6874ff88ed120faa159df3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class CompilerDiagnostics
|
||||
{
|
||||
public List<CompilationError> Errors { get; }
|
||||
|
||||
public List<string> Warnings { get; }
|
||||
|
||||
public bool HasAmbiguityErrors { get; }
|
||||
|
||||
private CompilerDiagnostics(
|
||||
List<CompilationError> errors,
|
||||
List<string> warnings,
|
||||
bool hasAmbiguityErrors)
|
||||
{
|
||||
Errors = errors;
|
||||
Warnings = warnings;
|
||||
HasAmbiguityErrors = hasAmbiguityErrors;
|
||||
}
|
||||
|
||||
public static CompilerDiagnostics FromMessages(CompilerMessage[] messages)
|
||||
{
|
||||
List<CompilationError> errors = new List<CompilationError>();
|
||||
List<string> warnings = new List<string>();
|
||||
bool hasAmbiguityErrors = false;
|
||||
|
||||
foreach (CompilerMessage message in messages)
|
||||
{
|
||||
if (message.type == CompilerMessageType.Warning)
|
||||
{
|
||||
warnings.Add(message.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.type != CompilerMessageType.Error)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string errorCode = ExtractErrorCode(message.message);
|
||||
if (errorCode == "CS0104" || errorCode == "CS0234")
|
||||
{
|
||||
hasAmbiguityErrors = true;
|
||||
}
|
||||
|
||||
errors.Add(new CompilationError
|
||||
{
|
||||
Message = message.message,
|
||||
ErrorCode = errorCode,
|
||||
Line = message.line,
|
||||
Column = message.column
|
||||
});
|
||||
}
|
||||
|
||||
return new CompilerDiagnostics(errors, warnings, hasAmbiguityErrors);
|
||||
}
|
||||
|
||||
private static string ExtractErrorCode(string message)
|
||||
{
|
||||
int csIndex = message.IndexOf("CS", StringComparison.Ordinal);
|
||||
if (csIndex >= 0 && csIndex + 6 <= message.Length)
|
||||
{
|
||||
string candidate = message.Substring(csIndex, 6);
|
||||
if (candidate.Length == 6 && candidate[2] >= '0' && candidate[2] <= '9')
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcdd52bd855284532a70b6aac3b791c5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public sealed class DynamicCodeCompilationServiceFactory : IDynamicCompilationServiceFactory
|
||||
{
|
||||
public IDynamicCompilationService Create(DynamicCodeSecurityLevel securityLevel)
|
||||
{
|
||||
return new DynamicCodeCompiler(securityLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96f75272fbc5342bcb4e3706cf884fcf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
public static class DynamicCodeCompilationServiceRegistration
|
||||
{
|
||||
private static readonly object SyncRoot = new object();
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
private static void Register()
|
||||
{
|
||||
EnsureRegistered();
|
||||
}
|
||||
|
||||
internal static void EnsureRegistered()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (DynamicCompilationServiceRegistry.HasRegisteredFactory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicCompilationServiceRegistry.RegisterFactory(new DynamicCodeCompilationServiceFactory());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 937b57e6191b647de852905df11c5b25
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,319 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Debug = UnityEngine.Debug;
|
||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Dynamic compilation service for execute-dynamic-code.
|
||||
/// Keeps the orchestration flow in one place and delegates low-level compiler concerns to helpers.
|
||||
/// </summary>
|
||||
public sealed class DynamicCodeCompiler : IDynamicCompilationService, IDisposable
|
||||
{
|
||||
private readonly DynamicCodeSecurityLevel _securityLevel;
|
||||
private readonly IDynamicCompilationPlanner _planner;
|
||||
private readonly ICompiledAssemblyBuilder _assemblyBuilder;
|
||||
private readonly ICompiledAssemblyLoader _assemblyLoader;
|
||||
private readonly CompilationCacheManager _cacheManager = new();
|
||||
private bool _disposed;
|
||||
|
||||
internal int LastBuildCount { get; private set; }
|
||||
|
||||
public DynamicCodeCompiler(DynamicCodeSecurityLevel securityLevel)
|
||||
: this(
|
||||
securityLevel,
|
||||
DynamicCodeServices.CompilationPlanner,
|
||||
DynamicCodeServices.AssemblyBuilder,
|
||||
DynamicCodeServices.AssemblyLoadService)
|
||||
{
|
||||
}
|
||||
|
||||
internal DynamicCodeCompiler(
|
||||
DynamicCodeSecurityLevel securityLevel,
|
||||
IDynamicCompilationPlanner planner,
|
||||
ICompiledAssemblyBuilder assemblyBuilder,
|
||||
ICompiledAssemblyLoader assemblyLoader)
|
||||
{
|
||||
_securityLevel = securityLevel;
|
||||
_planner = planner ?? throw new ArgumentNullException(nameof(planner));
|
||||
_assemblyBuilder = assemblyBuilder ?? throw new ArgumentNullException(nameof(assemblyBuilder));
|
||||
_assemblyLoader = assemblyLoader ?? throw new ArgumentNullException(nameof(assemblyLoader));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cacheManager.ClearCache();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async Task<CompilationResult> CompileAsync(CompilationRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Debug.Assert(request != null, "request must not be null");
|
||||
Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty");
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
LastBuildCount = 0;
|
||||
Stopwatch compilerTotalStopwatch = Stopwatch.StartNew();
|
||||
Stopwatch planStopwatch = Stopwatch.StartNew();
|
||||
|
||||
DynamicCompilationPlan plan = _planner.CreatePlan(request);
|
||||
planStopwatch.Stop();
|
||||
|
||||
Stopwatch cacheStopwatch = Stopwatch.StartNew();
|
||||
CompilationResult cachedResult = TryGetCachedResult(plan);
|
||||
cacheStopwatch.Stop();
|
||||
if (cachedResult != null)
|
||||
{
|
||||
AppendCompilerStageTimings(
|
||||
cachedResult.Timings,
|
||||
planStopwatch.Elapsed.TotalMilliseconds,
|
||||
cacheStopwatch.Elapsed.TotalMilliseconds,
|
||||
0,
|
||||
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
Stopwatch sourceSecurityStopwatch = Stopwatch.StartNew();
|
||||
CompilationResult sourceSecurityFailure = CreateSourceSecurityFailure(request.Code);
|
||||
sourceSecurityStopwatch.Stop();
|
||||
if (sourceSecurityFailure != null)
|
||||
{
|
||||
AppendCompilerStageTimings(
|
||||
sourceSecurityFailure.Timings,
|
||||
planStopwatch.Elapsed.TotalMilliseconds,
|
||||
cacheStopwatch.Elapsed.TotalMilliseconds,
|
||||
0,
|
||||
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return sourceSecurityFailure;
|
||||
}
|
||||
|
||||
if (plan.PreparedCode.PreparedSource == null)
|
||||
{
|
||||
CompilationResult mixedModeFailure = CreateMixedModeFailureResult(request.Code, 0, 0);
|
||||
AppendCompilerStageTimings(
|
||||
mixedModeFailure.Timings,
|
||||
planStopwatch.Elapsed.TotalMilliseconds,
|
||||
cacheStopwatch.Elapsed.TotalMilliseconds,
|
||||
0,
|
||||
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return mixedModeFailure;
|
||||
}
|
||||
|
||||
Stopwatch builderTotalStopwatch = Stopwatch.StartNew();
|
||||
CompiledAssemblyBuildResult buildResult = await _assemblyBuilder.BuildAsync(plan, ct);
|
||||
builderTotalStopwatch.Stop();
|
||||
LastBuildCount = buildResult.BuildCount;
|
||||
|
||||
if (buildResult.Diagnostics.Errors.Count > 0)
|
||||
{
|
||||
CompilationResult failureResult = new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Errors = buildResult.Diagnostics.Errors,
|
||||
Warnings = buildResult.Diagnostics.Warnings,
|
||||
UpdatedCode = buildResult.UpdatedSource,
|
||||
FailureReason = CompilationFailureReason.CompilationError,
|
||||
AmbiguousTypeCandidates = buildResult.AmbiguousTypeCandidates,
|
||||
AutoInjectedNamespaces = buildResult.AutoInjectedNamespaces,
|
||||
AdvisoryLogs = BuildAdvisoryLogs(buildResult.CompilationBackendKind),
|
||||
CompilationBackendKind = buildResult.CompilationBackendKind,
|
||||
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
||||
buildResult.ReferenceResolutionMilliseconds,
|
||||
buildResult.BuildMilliseconds,
|
||||
0,
|
||||
buildResult.CompilationBackendKind)
|
||||
};
|
||||
AppendCompilerStageTimings(
|
||||
failureResult.Timings,
|
||||
planStopwatch.Elapsed.TotalMilliseconds,
|
||||
cacheStopwatch.Elapsed.TotalMilliseconds,
|
||||
builderTotalStopwatch.Elapsed.TotalMilliseconds,
|
||||
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return failureResult;
|
||||
}
|
||||
|
||||
CompiledAssemblyLoadResult assemblyLoadResult = _assemblyLoader.Load(
|
||||
_securityLevel,
|
||||
buildResult.AssemblyBytes);
|
||||
if (!assemblyLoadResult.Success)
|
||||
{
|
||||
CompilationResult assemblySecurityFailure = CreateAssemblySecurityFailure(
|
||||
assemblyLoadResult.SecurityViolations,
|
||||
buildResult.Diagnostics.Warnings,
|
||||
buildResult.UpdatedSource,
|
||||
buildResult.AmbiguousTypeCandidates,
|
||||
buildResult.AutoInjectedNamespaces,
|
||||
buildResult.ReferenceResolutionMilliseconds,
|
||||
buildResult.BuildMilliseconds,
|
||||
assemblyLoadResult.AssemblyLoadMilliseconds,
|
||||
buildResult.CompilationBackendKind);
|
||||
AppendCompilerStageTimings(
|
||||
assemblySecurityFailure.Timings,
|
||||
planStopwatch.Elapsed.TotalMilliseconds,
|
||||
cacheStopwatch.Elapsed.TotalMilliseconds,
|
||||
builderTotalStopwatch.Elapsed.TotalMilliseconds,
|
||||
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return assemblySecurityFailure;
|
||||
}
|
||||
|
||||
CompilationResult result = new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
CompiledAssembly = assemblyLoadResult.CompiledAssembly,
|
||||
Warnings = buildResult.Diagnostics.Warnings,
|
||||
UpdatedCode = buildResult.UpdatedSource,
|
||||
AmbiguousTypeCandidates = buildResult.AmbiguousTypeCandidates,
|
||||
AutoInjectedNamespaces = buildResult.AutoInjectedNamespaces,
|
||||
AdvisoryLogs = BuildAdvisoryLogs(buildResult.CompilationBackendKind),
|
||||
CompilationBackendKind = buildResult.CompilationBackendKind,
|
||||
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
||||
buildResult.ReferenceResolutionMilliseconds,
|
||||
buildResult.BuildMilliseconds,
|
||||
assemblyLoadResult.AssemblyLoadMilliseconds,
|
||||
buildResult.CompilationBackendKind)
|
||||
};
|
||||
AppendCompilerStageTimings(
|
||||
result.Timings,
|
||||
planStopwatch.Elapsed.TotalMilliseconds,
|
||||
cacheStopwatch.Elapsed.TotalMilliseconds,
|
||||
builderTotalStopwatch.Elapsed.TotalMilliseconds,
|
||||
compilerTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
|
||||
if (buildResult.ShouldCacheResult)
|
||||
{
|
||||
_cacheManager.CacheResultIfSuccessful(result, plan.NormalizedRequest);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private CompilationResult TryGetCachedResult(
|
||||
DynamicCompilationPlan plan)
|
||||
{
|
||||
return _cacheManager.CheckCache(plan.NormalizedRequest);
|
||||
}
|
||||
|
||||
private CompilationResult CreateSourceSecurityFailure(string originalCode)
|
||||
{
|
||||
if (_securityLevel != DynamicCodeSecurityLevel.Restricted)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
SecurityValidationResult sourceSecurityResult = SourceSecurityScanner.Scan(originalCode);
|
||||
if (sourceSecurityResult.IsValid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
HasSecurityViolations = true,
|
||||
SecurityViolations = sourceSecurityResult.Violations,
|
||||
UpdatedCode = originalCode,
|
||||
FailureReason = CompilationFailureReason.SecurityViolation,
|
||||
CompilationBackendKind = DynamicCompilationBackendKind.Unknown,
|
||||
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(0, 0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
private static CompilationResult CreateMixedModeFailureResult(
|
||||
string originalCode,
|
||||
double referenceResolutionMilliseconds,
|
||||
double buildMilliseconds)
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
Errors = new List<CompilationError>
|
||||
{
|
||||
new CompilationError
|
||||
{
|
||||
Message = "Mixed top-level statements and type declarations are not supported. Use a full class definition instead.",
|
||||
ErrorCode = "MIXED_MODE",
|
||||
Line = 0,
|
||||
Column = 0
|
||||
}
|
||||
},
|
||||
FailureReason = CompilationFailureReason.CompilationError,
|
||||
UpdatedCode = originalCode,
|
||||
CompilationBackendKind = DynamicCompilationBackendKind.Unknown,
|
||||
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
||||
referenceResolutionMilliseconds,
|
||||
buildMilliseconds,
|
||||
0)
|
||||
};
|
||||
}
|
||||
|
||||
private static CompilationResult CreateAssemblySecurityFailure(
|
||||
List<SecurityViolation> securityViolations,
|
||||
List<string> warnings,
|
||||
string updatedCode,
|
||||
Dictionary<string, List<string>> ambiguousTypeCandidates,
|
||||
List<string> autoInjectedNamespaces,
|
||||
double referenceResolutionMilliseconds,
|
||||
double buildMilliseconds,
|
||||
double assemblyLoadMilliseconds,
|
||||
DynamicCompilationBackendKind compilationBackendKind)
|
||||
{
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
HasSecurityViolations = true,
|
||||
SecurityViolations = securityViolations,
|
||||
Warnings = warnings,
|
||||
UpdatedCode = updatedCode,
|
||||
FailureReason = CompilationFailureReason.SecurityViolation,
|
||||
AmbiguousTypeCandidates = ambiguousTypeCandidates,
|
||||
AutoInjectedNamespaces = autoInjectedNamespaces,
|
||||
AdvisoryLogs = BuildAdvisoryLogs(compilationBackendKind),
|
||||
CompilationBackendKind = compilationBackendKind,
|
||||
Timings = DynamicCompilationTimingFormatter.CreateCompilationTimings(
|
||||
referenceResolutionMilliseconds,
|
||||
buildMilliseconds,
|
||||
assemblyLoadMilliseconds,
|
||||
compilationBackendKind)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> BuildAdvisoryLogs(DynamicCompilationBackendKind compilationBackendKind)
|
||||
{
|
||||
return compilationBackendKind == DynamicCompilationBackendKind.AssemblyBuilderFallback
|
||||
? new List<string>
|
||||
{
|
||||
"Warning: Fast Roslyn path is unavailable; execute-dynamic-code is using AssemblyBuilder fallback, so new snippets compile slower."
|
||||
}
|
||||
: new List<string>();
|
||||
}
|
||||
|
||||
private static void AppendCompilerStageTimings(
|
||||
List<string> timings,
|
||||
double planMilliseconds,
|
||||
double cacheMilliseconds,
|
||||
double builderTotalMilliseconds,
|
||||
double compilerTotalMilliseconds)
|
||||
{
|
||||
if (timings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timings.Add($"[Perf] CompilePlan: {planMilliseconds:F1}ms");
|
||||
timings.Add($"[Perf] CompileCacheCheck: {cacheMilliseconds:F1}ms");
|
||||
if (builderTotalMilliseconds > 0)
|
||||
{
|
||||
timings.Add($"[Perf] BuilderTotal: {builderTotalMilliseconds:F1}ms");
|
||||
}
|
||||
timings.Add($"[Perf] CompilerTotal: {compilerTotalMilliseconds:F1}ms");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 516acc8dbb4a44cc78a6fe5901c0ad45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,863 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicCodeLiteralHoister
|
||||
{
|
||||
private const string LiteralParameterPrefix = "__uloop_literal_";
|
||||
|
||||
public static HoistedLiteralRewriteResult Rewrite(string source)
|
||||
{
|
||||
StringBuilder rewrittenSource = new StringBuilder(source.Length);
|
||||
List<HoistedLiteralBinding> bindings = new List<HoistedLiteralBinding>();
|
||||
int index = 0;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
if (TryCopyInterpolatedStringLiteral(source, rewrittenSource, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryCopyVerbatimStringLiteral(source, rewrittenSource, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryCopyCharLiteral(source, rewrittenSource, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryCopyLineComment(source, rewrittenSource, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryCopyBlockComment(source, rewrittenSource, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryHoistRegularStringLiteral(source, rewrittenSource, bindings, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryHoistIntegerLiteral(source, rewrittenSource, bindings, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
rewrittenSource.Append(source[index]);
|
||||
index++;
|
||||
}
|
||||
|
||||
List<string> declarationLines = new List<string>();
|
||||
foreach (HoistedLiteralBinding binding in bindings)
|
||||
{
|
||||
declarationLines.Add(
|
||||
$"{binding.TypeName} {binding.ParameterName} = ({binding.TypeName})parameters[\"{binding.ParameterName}\"];");
|
||||
}
|
||||
|
||||
return new HoistedLiteralRewriteResult(
|
||||
rewrittenSource.ToString(),
|
||||
bindings,
|
||||
declarationLines);
|
||||
}
|
||||
|
||||
private static bool TryCopyInterpolatedStringLiteral(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
ref int index)
|
||||
{
|
||||
int start = index;
|
||||
if (!TryAdvanceInterpolatedStringLiteral(source, ref index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rewrittenSource.Append(source, start, index - start);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCopyLineComment(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
ref int index)
|
||||
{
|
||||
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
char current = source[index];
|
||||
rewrittenSource.Append(current);
|
||||
index++;
|
||||
if (current == '\n')
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCopyBlockComment(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
ref int index)
|
||||
{
|
||||
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rewrittenSource.Append(source[index]);
|
||||
rewrittenSource.Append(source[index + 1]);
|
||||
index += 2;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
char current = source[index];
|
||||
rewrittenSource.Append(current);
|
||||
index++;
|
||||
if (current == '*' && index < source.Length && source[index] == '/')
|
||||
{
|
||||
rewrittenSource.Append('/');
|
||||
index++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCopyVerbatimStringLiteral(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
ref int index)
|
||||
{
|
||||
if (source[index] != '@'
|
||||
|| index + 1 >= source.Length
|
||||
|| source[index + 1] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index += 2;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
if (source[index] != '"')
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index + 1 < source.Length && source[index + 1] == '"')
|
||||
{
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
rewrittenSource.Append(source, start, index - start);
|
||||
return true;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceInterpolatedStringLiteral(string source, ref int index)
|
||||
{
|
||||
int start = index;
|
||||
if (!TryMatchInterpolatedStringStart(source, ref index, out bool isVerbatim))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int interpolationDepth = 0;
|
||||
while (index < source.Length)
|
||||
{
|
||||
if (interpolationDepth > 0)
|
||||
{
|
||||
if (TryAdvanceInterpolatedExpressionToken(source, ref index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char expressionCharacter = source[index];
|
||||
if (expressionCharacter == '{')
|
||||
{
|
||||
interpolationDepth++;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (expressionCharacter == '}')
|
||||
{
|
||||
interpolationDepth--;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
char current = source[index];
|
||||
if (current == '{')
|
||||
{
|
||||
if (index + 1 < source.Length && source[index + 1] == '{')
|
||||
{
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
interpolationDepth = 1;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '}')
|
||||
{
|
||||
if (index + 1 < source.Length && source[index + 1] == '}')
|
||||
{
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isVerbatim && current == '\\')
|
||||
{
|
||||
AdvanceEscapedLiteralSequence(source, ref index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '"')
|
||||
{
|
||||
if (isVerbatim && index + 1 < source.Length && source[index + 1] == '"')
|
||||
{
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryMatchInterpolatedStringStart(
|
||||
string source,
|
||||
ref int index,
|
||||
out bool isVerbatim)
|
||||
{
|
||||
isVerbatim = false;
|
||||
|
||||
if (source[index] == '$')
|
||||
{
|
||||
if (index + 1 < source.Length && source[index + 1] == '"')
|
||||
{
|
||||
index += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (index + 2 < source.Length && source[index + 1] == '@' && source[index + 2] == '"')
|
||||
{
|
||||
isVerbatim = true;
|
||||
index += 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source[index] != '@')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index + 2 < source.Length && source[index + 1] == '$' && source[index + 2] == '"')
|
||||
{
|
||||
isVerbatim = true;
|
||||
index += 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceInterpolatedExpressionToken(string source, ref int index)
|
||||
{
|
||||
if (TryAdvanceInterpolatedStringLiteral(source, ref index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryAdvanceVerbatimStringLiteral(source, ref index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryAdvanceRegularStringLiteral(source, ref index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryAdvanceCharLiteral(source, ref index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryAdvanceLineComment(source, ref index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryAdvanceBlockComment(source, ref index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceLineComment(string source, ref int index)
|
||||
{
|
||||
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '/')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index += 2;
|
||||
while (index < source.Length && source[index] != '\n')
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < source.Length)
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceBlockComment(string source, ref int index)
|
||||
{
|
||||
if (index + 1 >= source.Length || source[index] != '/' || source[index + 1] != '*')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index += 2;
|
||||
while (index < source.Length)
|
||||
{
|
||||
if (source[index] == '*' && index + 1 < source.Length && source[index + 1] == '/')
|
||||
{
|
||||
index += 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceVerbatimStringLiteral(string source, ref int index)
|
||||
{
|
||||
if (source[index] != '@'
|
||||
|| index + 1 >= source.Length
|
||||
|| source[index + 1] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index += 2;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
if (source[index] != '"')
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index + 1 < source.Length && source[index + 1] == '"')
|
||||
{
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceRegularStringLiteral(string source, ref int index)
|
||||
{
|
||||
if (source[index] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index > 0 && (source[index - 1] == '@' || source[index - 1] == '$'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index++;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
char current = source[index];
|
||||
if (current == '\\')
|
||||
{
|
||||
AdvanceEscapedLiteralSequence(source, ref index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '"')
|
||||
{
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceCharLiteral(string source, ref int index)
|
||||
{
|
||||
if (source[index] != '\'')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index++;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
char current = source[index];
|
||||
if (current == '\\')
|
||||
{
|
||||
AdvanceEscapedLiteralSequence(source, ref index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '\'')
|
||||
{
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryCopyCharLiteral(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
ref int index)
|
||||
{
|
||||
if (source[index] != '\'')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index++;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
char current = source[index];
|
||||
if (current == '\\')
|
||||
{
|
||||
AdvanceEscapedLiteralSequence(source, ref index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '\'')
|
||||
{
|
||||
index++;
|
||||
rewrittenSource.Append(source, start, index - start);
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryHoistRegularStringLiteral(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
List<HoistedLiteralBinding> bindings,
|
||||
ref int index)
|
||||
{
|
||||
if (source[index] != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index > 0 && (source[index - 1] == '@' || source[index - 1] == '$'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index++;
|
||||
|
||||
while (index < source.Length)
|
||||
{
|
||||
char current = source[index];
|
||||
if (current == '\\')
|
||||
{
|
||||
AdvanceEscapedLiteralSequence(source, ref index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == '"')
|
||||
{
|
||||
index++;
|
||||
string literalToken = source.Substring(start, index - start);
|
||||
if (!TryUnescapeRegularStringLiteral(literalToken, out string value))
|
||||
{
|
||||
rewrittenSource.Append(literalToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
string parameterName = CreateParameterName(bindings.Count);
|
||||
bindings.Add(new HoistedLiteralBinding(parameterName, "string", value));
|
||||
rewrittenSource.Append(parameterName);
|
||||
return true;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryHoistIntegerLiteral(
|
||||
string source,
|
||||
StringBuilder rewrittenSource,
|
||||
List<HoistedLiteralBinding> bindings,
|
||||
ref int index)
|
||||
{
|
||||
char current = source[index];
|
||||
if (!char.IsDigit(current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
char previous = source[index - 1];
|
||||
if (char.IsLetterOrDigit(previous) || previous == '_' || previous == '.')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int start = index;
|
||||
index++;
|
||||
while (index < source.Length && char.IsDigit(source[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < source.Length && (source[index] == 'L' || source[index] == 'l'))
|
||||
{
|
||||
int suffixIndex = index;
|
||||
index++;
|
||||
if (!HasIntegerLiteralBoundary(source, index))
|
||||
{
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
string longToken = source.Substring(start, suffixIndex - start);
|
||||
if (!long.TryParse(longToken, NumberStyles.None, CultureInfo.InvariantCulture, out long longValue))
|
||||
{
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
string longParameterName = CreateParameterName(bindings.Count);
|
||||
bindings.Add(new HoistedLiteralBinding(longParameterName, "long", longValue));
|
||||
rewrittenSource.Append(longParameterName);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!HasIntegerLiteralBoundary(source, index))
|
||||
{
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
string token = source.Substring(start, index - start);
|
||||
if (!int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out int intValue))
|
||||
{
|
||||
index = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
string parameterName = CreateParameterName(bindings.Count);
|
||||
bindings.Add(new HoistedLiteralBinding(parameterName, "int", intValue));
|
||||
rewrittenSource.Append(parameterName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasIntegerLiteralBoundary(string source, int index)
|
||||
{
|
||||
if (index >= source.Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
char next = source[index];
|
||||
return !char.IsLetter(next) && !char.IsDigit(next) && next != '_' && next != '.';
|
||||
}
|
||||
|
||||
private static string CreateParameterName(int index)
|
||||
{
|
||||
return $"{LiteralParameterPrefix}{index}";
|
||||
}
|
||||
|
||||
private static void AdvanceEscapedLiteralSequence(string source, ref int index)
|
||||
{
|
||||
index++;
|
||||
if (index >= source.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char escape = source[index];
|
||||
index++;
|
||||
|
||||
switch (escape)
|
||||
{
|
||||
case 'u':
|
||||
index = Math.Min(source.Length, index + 4);
|
||||
break;
|
||||
case 'U':
|
||||
index = Math.Min(source.Length, index + 8);
|
||||
break;
|
||||
case 'x':
|
||||
index = AdvanceVariableLengthHexDigits(source, index, 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static int AdvanceVariableLengthHexDigits(string value, int index, int maxDigits)
|
||||
{
|
||||
int digitsConsumed = 0;
|
||||
while (index < value.Length && digitsConsumed < maxDigits && IsHexDigit(value[index]))
|
||||
{
|
||||
index++;
|
||||
digitsConsumed++;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private static bool TryUnescapeRegularStringLiteral(string token, out string value)
|
||||
{
|
||||
string inner = token.Substring(1, token.Length - 2);
|
||||
StringBuilder unescaped = new StringBuilder(inner.Length);
|
||||
|
||||
for (int index = 0; index < inner.Length; index++)
|
||||
{
|
||||
char current = inner[index];
|
||||
if (current != '\\')
|
||||
{
|
||||
unescaped.Append(current);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index + 1 >= inner.Length)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
char escape = inner[index];
|
||||
switch (escape)
|
||||
{
|
||||
case '\'':
|
||||
unescaped.Append('\'');
|
||||
break;
|
||||
case '"':
|
||||
unescaped.Append('"');
|
||||
break;
|
||||
case '\\':
|
||||
unescaped.Append('\\');
|
||||
break;
|
||||
case '0':
|
||||
unescaped.Append('\0');
|
||||
break;
|
||||
case 'a':
|
||||
unescaped.Append('\a');
|
||||
break;
|
||||
case 'b':
|
||||
unescaped.Append('\b');
|
||||
break;
|
||||
case 'f':
|
||||
unescaped.Append('\f');
|
||||
break;
|
||||
case 'n':
|
||||
unescaped.Append('\n');
|
||||
break;
|
||||
case 'r':
|
||||
unescaped.Append('\r');
|
||||
break;
|
||||
case 't':
|
||||
unescaped.Append('\t');
|
||||
break;
|
||||
case 'v':
|
||||
unescaped.Append('\v');
|
||||
break;
|
||||
case 'u':
|
||||
if (!TryParseHexDigits(inner, ref index, 4, out int unicodeValue))
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
unescaped.Append((char)unicodeValue);
|
||||
break;
|
||||
case 'U':
|
||||
if (!TryParseHexDigits(inner, ref index, 8, out int codePoint) ||
|
||||
!IsValidUtf32CodePoint(codePoint))
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
unescaped.Append(char.ConvertFromUtf32(codePoint));
|
||||
break;
|
||||
case 'x':
|
||||
if (!TryParseVariableLengthHexDigits(inner, ref index, out int variableLengthValue))
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
unescaped.Append((char)variableLengthValue);
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
value = unescaped.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseHexDigits(string value, ref int index, int digitCount, out int parsedValue)
|
||||
{
|
||||
int start = index + 1;
|
||||
if (start + digitCount > value.Length)
|
||||
{
|
||||
parsedValue = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
string hex = value.Substring(start, digitCount);
|
||||
if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index += digitCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseVariableLengthHexDigits(string value, ref int index, out int parsedValue)
|
||||
{
|
||||
int start = index + 1;
|
||||
int end = AdvanceVariableLengthHexDigits(value, start, 4);
|
||||
if (end == start)
|
||||
{
|
||||
parsedValue = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
string hex = value.Substring(start, end - start);
|
||||
if (!int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsedValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
index = end - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsValidUtf32CodePoint(int value)
|
||||
{
|
||||
return value >= 0
|
||||
&& value <= 0x10FFFF
|
||||
&& (value < 0xD800 || value > 0xDFFF);
|
||||
}
|
||||
|
||||
private static bool IsHexDigit(char value)
|
||||
{
|
||||
return (value >= '0' && value <= '9')
|
||||
|| (value >= 'a' && value <= 'f')
|
||||
|| (value >= 'A' && value <= 'F');
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HoistedLiteralRewriteResult
|
||||
{
|
||||
public string RewrittenSource { get; }
|
||||
public List<HoistedLiteralBinding> Bindings { get; }
|
||||
public List<string> DeclarationLines { get; }
|
||||
|
||||
public HoistedLiteralRewriteResult(
|
||||
string rewrittenSource,
|
||||
List<HoistedLiteralBinding> bindings,
|
||||
List<string> declarationLines)
|
||||
{
|
||||
RewrittenSource = rewrittenSource;
|
||||
Bindings = bindings;
|
||||
DeclarationLines = declarationLines;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5582b74341280488f9b5bcdf69463f26
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCodeSourcePreparationService
|
||||
{
|
||||
public PreparedDynamicCode Prepare(
|
||||
string source,
|
||||
string namespaceName,
|
||||
string className)
|
||||
{
|
||||
return DynamicCodeSourcePreparer.Prepare(source, namespaceName, className);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3bd88ecabcf9b4e7dad4f5638ad6d861
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicCodeSourcePreparer
|
||||
{
|
||||
private const string InterpolatedStringMarker = "$\"";
|
||||
private const string InterpolatedVerbatimStringMarker = "$@\"";
|
||||
private const string VerbatimInterpolatedStringMarker = "@$\"";
|
||||
|
||||
public static PreparedDynamicCode PrepareWithoutLiteralHoisting(
|
||||
string source,
|
||||
string namespaceName,
|
||||
string className)
|
||||
{
|
||||
return Prepare(source, namespaceName, className, false);
|
||||
}
|
||||
|
||||
public static PreparedDynamicCode Prepare(
|
||||
string source,
|
||||
string namespaceName,
|
||||
string className)
|
||||
{
|
||||
return Prepare(source, namespaceName, className, true);
|
||||
}
|
||||
|
||||
private static PreparedDynamicCode Prepare(
|
||||
string source,
|
||||
string namespaceName,
|
||||
string className,
|
||||
bool enableLiteralHoisting)
|
||||
{
|
||||
SourceShapeResult shape = SourceShaper.Analyze(source);
|
||||
|
||||
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && !shape.HasTopLevelStatements)
|
||||
{
|
||||
return new PreparedDynamicCode(source, false, new List<HoistedLiteralBinding>());
|
||||
}
|
||||
|
||||
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && shape.HasTopLevelStatements)
|
||||
{
|
||||
return new PreparedDynamicCode(null, false, new List<HoistedLiteralBinding>());
|
||||
}
|
||||
|
||||
string body = shape.TopLevelBodyBuilder.ToString().TrimEnd();
|
||||
bool hasReturn = TopLevelReturnDetector.HasTopLevelReturn(body);
|
||||
if (!hasReturn)
|
||||
{
|
||||
body = string.IsNullOrWhiteSpace(body)
|
||||
? "return null;"
|
||||
: body + "\nreturn null;";
|
||||
}
|
||||
|
||||
HoistedLiteralRewriteResult hoistedResult = !enableLiteralHoisting || ShouldSkipLiteralHoisting(body)
|
||||
? new HoistedLiteralRewriteResult(body, new List<HoistedLiteralBinding>(), new List<string>())
|
||||
: DynamicCodeLiteralHoister.Rewrite(body);
|
||||
string preparedSource = WrapperTemplate.Build(
|
||||
shape.UsingDirectives,
|
||||
namespaceName,
|
||||
className,
|
||||
hoistedResult.RewrittenSource,
|
||||
hoistedResult.DeclarationLines);
|
||||
|
||||
return new PreparedDynamicCode(preparedSource, true, hoistedResult.Bindings);
|
||||
}
|
||||
|
||||
private static bool ShouldSkipLiteralHoisting(string body)
|
||||
{
|
||||
return body.Contains(InterpolatedStringMarker)
|
||||
|| body.Contains(InterpolatedVerbatimStringMarker)
|
||||
|| body.Contains(VerbatimInterpolatedStringMarker)
|
||||
|| ContainsInterpolatedRawStringMarker(body);
|
||||
}
|
||||
|
||||
private static bool ContainsInterpolatedRawStringMarker(string body)
|
||||
{
|
||||
int quoteIndex = body.IndexOf("\"\"\"", StringComparison.Ordinal);
|
||||
while (quoteIndex >= 0)
|
||||
{
|
||||
int dollarCount = 0;
|
||||
int markerIndex = quoteIndex - 1;
|
||||
while (markerIndex >= 0 && body[markerIndex] == '$')
|
||||
{
|
||||
dollarCount++;
|
||||
markerIndex--;
|
||||
}
|
||||
|
||||
if (dollarCount > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
quoteIndex = body.IndexOf("\"\"\"", quoteIndex + 3, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85cb11422a761428aae898d21695b9a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCompilationBackendResult
|
||||
{
|
||||
public CompilerMessage[] CompilerMessages { get; }
|
||||
|
||||
public DynamicCompilationBackendKind BackendKind { get; }
|
||||
|
||||
public DynamicCompilationBackendResult(
|
||||
CompilerMessage[] compilerMessages,
|
||||
DynamicCompilationBackendKind backendKind)
|
||||
{
|
||||
CompilerMessages = compilerMessages;
|
||||
BackendKind = backendKind;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DynamicCompilationBackend
|
||||
{
|
||||
public Task<DynamicCompilationBackendResult> CompileAsync(
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
List<string> references,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
if (externalCompilerPaths != null)
|
||||
{
|
||||
return RoslynCompilerBackend.CompileAsync(
|
||||
sourcePath,
|
||||
dllPath,
|
||||
references,
|
||||
externalCompilerPaths,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount);
|
||||
}
|
||||
|
||||
return AssemblyBuilderFallbackCompilerBackend.CompileAsync(
|
||||
sourcePath,
|
||||
dllPath,
|
||||
references,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b1f77be16bf24a6aa55f573f693172b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicCompilationHealthMonitor
|
||||
{
|
||||
private static readonly object ReportedIssueLock = new();
|
||||
private static readonly HashSet<string> ReportedIssues = new(System.StringComparer.Ordinal);
|
||||
private static readonly AsyncLocal<string> ConsoleDiagnosticSource = new();
|
||||
|
||||
public static void ReportFastPathUnavailable(
|
||||
string editorPath,
|
||||
string contentsPath,
|
||||
IReadOnlyCollection<string> missingComponents)
|
||||
{
|
||||
string issueKey = $"fast_path_unavailable::{editorPath}";
|
||||
LogErrorOnce(
|
||||
issueKey,
|
||||
"dynamic_code_fast_path_unavailable",
|
||||
"execute-dynamic-code fast Roslyn path is unavailable; AssemblyBuilder fallback will be used",
|
||||
new
|
||||
{
|
||||
editor_path = editorPath,
|
||||
editor_contents_path = contentsPath,
|
||||
missing_components = missingComponents
|
||||
},
|
||||
"Fast dynamic compilation path is unavailable in this Unity installation.",
|
||||
"Check Unity version layout changes and ExternalCompilerPathResolver assumptions.");
|
||||
}
|
||||
|
||||
public static void ReportSharedWorkerFallback(string reason, object context = null)
|
||||
{
|
||||
string issueKey = $"shared_worker_fallback::{reason}";
|
||||
string message =
|
||||
$"execute-dynamic-code shared Roslyn worker is unavailable; falling back to one-shot compiler execution; reason={reason}";
|
||||
LogErrorOnce(
|
||||
issueKey,
|
||||
"dynamic_code_shared_worker_fallback",
|
||||
message,
|
||||
context ?? new { reason },
|
||||
"Shared worker fast path is not active.",
|
||||
"Investigate shared worker startup, protocol, or platform support.");
|
||||
}
|
||||
|
||||
public static void ReportSharedWorkerFailure(string reason, object context = null)
|
||||
{
|
||||
string issueKey = $"shared_worker_failure::{reason}";
|
||||
string message =
|
||||
$"execute-dynamic-code shared Roslyn worker failed to operate correctly; reason={reason}";
|
||||
LogErrorOnce(
|
||||
issueKey,
|
||||
"dynamic_code_shared_worker_failure",
|
||||
message,
|
||||
context ?? new { reason },
|
||||
"Shared Roslyn worker encountered an unexpected failure.",
|
||||
"Investigate worker startup, request handling, and Unity-version-specific runtime assumptions.");
|
||||
}
|
||||
|
||||
public static void ReportOneShotCompilerStartFailure(object context)
|
||||
{
|
||||
LogErrorOnce(
|
||||
"one_shot_compiler_start_failure",
|
||||
"dynamic_code_one_shot_compiler_start_failure",
|
||||
"execute-dynamic-code one-shot Roslyn compiler process failed to start",
|
||||
context,
|
||||
"One-shot Roslyn compiler could not be started.",
|
||||
"Investigate Unity-bundled dotnet/csc availability and process launch assumptions.");
|
||||
}
|
||||
|
||||
private static void LogErrorOnce(
|
||||
string issueKey,
|
||||
string operation,
|
||||
string message,
|
||||
object context,
|
||||
string humanNote,
|
||||
string aiTodo)
|
||||
{
|
||||
string effectiveIssueKey = CreateEffectiveIssueKey(issueKey);
|
||||
|
||||
lock (ReportedIssueLock)
|
||||
{
|
||||
if (!ReportedIssues.Add(effectiveIssueKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
VibeLogger.LogError(
|
||||
operation,
|
||||
message,
|
||||
context,
|
||||
humanNote: humanNote,
|
||||
aiTodo: aiTodo);
|
||||
Debug.LogError(FormatConsoleErrorMessage(operation, message, context));
|
||||
}
|
||||
|
||||
private static string CreateEffectiveIssueKey(string issueKey)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(issueKey), "issueKey must not be empty");
|
||||
|
||||
if (string.IsNullOrEmpty(ConsoleDiagnosticSource.Value))
|
||||
{
|
||||
return issueKey;
|
||||
}
|
||||
|
||||
return $"{issueKey}::source::{ConsoleDiagnosticSource.Value}";
|
||||
}
|
||||
|
||||
internal static System.IDisposable UseConsoleDiagnosticSource(string source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source))
|
||||
{
|
||||
return EmptyDisposable.Instance;
|
||||
}
|
||||
|
||||
string previousSource = ConsoleDiagnosticSource.Value;
|
||||
ConsoleDiagnosticSource.Value = source;
|
||||
return new ConsoleDiagnosticSourceScope(previousSource);
|
||||
}
|
||||
|
||||
private static string FormatConsoleErrorMessage(string operation, string message, object context)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(operation), "operation must not be empty");
|
||||
Debug.Assert(!string.IsNullOrEmpty(message), "message must not be empty");
|
||||
|
||||
string diagnosticSourceLine = string.IsNullOrEmpty(ConsoleDiagnosticSource.Value)
|
||||
? string.Empty
|
||||
: $"\ndiagnostic_source: {ConsoleDiagnosticSource.Value}";
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
return $"[{McpConstants.PROJECT_NAME}] {message}\noperation: {operation}{diagnosticSourceLine}";
|
||||
}
|
||||
|
||||
return $"[{McpConstants.PROJECT_NAME}] {message}\noperation: {operation}{diagnosticSourceLine}\ncontext: {context}";
|
||||
}
|
||||
|
||||
private sealed class ConsoleDiagnosticSourceScope : System.IDisposable
|
||||
{
|
||||
private readonly string _previousSource;
|
||||
|
||||
public ConsoleDiagnosticSourceScope(string previousSource)
|
||||
{
|
||||
_previousSource = previousSource;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ConsoleDiagnosticSource.Value = _previousSource;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EmptyDisposable : System.IDisposable
|
||||
{
|
||||
public static readonly EmptyDisposable Instance = new EmptyDisposable();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetForTests()
|
||||
{
|
||||
lock (ReportedIssueLock)
|
||||
{
|
||||
ReportedIssues.Clear();
|
||||
}
|
||||
|
||||
ConsoleDiagnosticSource.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc51d8d05ce484405a35501c4f13ccfe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCompilationPlan
|
||||
{
|
||||
public CompilationRequest OriginalRequest { get; }
|
||||
|
||||
public CompilationRequest NormalizedRequest { get; }
|
||||
|
||||
public PreparedDynamicCode PreparedCode { get; }
|
||||
|
||||
public string ClassName { get; }
|
||||
|
||||
public string NamespaceName { get; }
|
||||
|
||||
public DynamicCompilationPlan(
|
||||
CompilationRequest originalRequest,
|
||||
CompilationRequest normalizedRequest,
|
||||
PreparedDynamicCode preparedCode,
|
||||
string className,
|
||||
string namespaceName)
|
||||
{
|
||||
OriginalRequest = originalRequest;
|
||||
NormalizedRequest = normalizedRequest;
|
||||
PreparedCode = preparedCode;
|
||||
ClassName = className;
|
||||
NamespaceName = namespaceName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1997758546984a2cbb1f5237b929481
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCompilationPlanner : IDynamicCompilationPlanner
|
||||
{
|
||||
private readonly DynamicCodeSourcePreparationService _sourcePreparationService;
|
||||
|
||||
public DynamicCompilationPlanner(DynamicCodeSourcePreparationService sourcePreparationService)
|
||||
{
|
||||
_sourcePreparationService = sourcePreparationService;
|
||||
}
|
||||
|
||||
public DynamicCompilationPlan CreatePlan(CompilationRequest request)
|
||||
{
|
||||
Debug.Assert(request != null, "request must not be null");
|
||||
Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty");
|
||||
|
||||
string namespaceName = request.Namespace ?? DynamicCodeConstants.DEFAULT_NAMESPACE;
|
||||
string className = request.ClassName ?? DynamicCodeConstants.DEFAULT_CLASS_NAME;
|
||||
PreparedDynamicCode preparedCode = _sourcePreparationService.Prepare(
|
||||
request.Code,
|
||||
namespaceName,
|
||||
className);
|
||||
|
||||
CompilationRequest normalizedRequest = new CompilationRequest
|
||||
{
|
||||
Code = preparedCode.PreparedSource ?? request.Code,
|
||||
ClassName = className,
|
||||
Namespace = namespaceName,
|
||||
AdditionalReferences = request.AdditionalReferences != null
|
||||
? new List<string>(request.AdditionalReferences)
|
||||
: new List<string>(),
|
||||
AssemblyMode = request.AssemblyMode
|
||||
};
|
||||
|
||||
return new DynamicCompilationPlan(
|
||||
request,
|
||||
normalizedRequest,
|
||||
preparedCode,
|
||||
className,
|
||||
namespaceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f46051bf774dd48acb16efe9055ac7f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicCompilationTimingFormatter
|
||||
{
|
||||
private const string CacheHitTimingEntry = "[Perf] CacheHit: true";
|
||||
|
||||
public static List<string> CreateCompilationTimings(
|
||||
double referenceResolutionMilliseconds,
|
||||
double buildMilliseconds,
|
||||
double assemblyLoadMilliseconds,
|
||||
DynamicCompilationBackendKind backendKind = DynamicCompilationBackendKind.Unknown)
|
||||
{
|
||||
List<string> timings = new List<string>
|
||||
{
|
||||
$"[Perf] ReferenceResolution: {referenceResolutionMilliseconds:F1}ms",
|
||||
$"[Perf] Build: {buildMilliseconds:F1}ms",
|
||||
$"[Perf] AssemblyLoad: {assemblyLoadMilliseconds:F1}ms"
|
||||
};
|
||||
|
||||
string backendTimingEntry = CreateBackendTimingEntry(backendKind);
|
||||
if (!string.IsNullOrEmpty(backendTimingEntry))
|
||||
{
|
||||
timings.Add(backendTimingEntry);
|
||||
}
|
||||
|
||||
return timings;
|
||||
}
|
||||
|
||||
public static List<string> CreateCachedCompilationTimings(
|
||||
DynamicCompilationBackendKind backendKind = DynamicCompilationBackendKind.Unknown)
|
||||
{
|
||||
List<string> timings = CreateCompilationTimings(0, 0, 0, backendKind);
|
||||
timings.Add(CacheHitTimingEntry);
|
||||
return timings;
|
||||
}
|
||||
|
||||
private static string CreateBackendTimingEntry(DynamicCompilationBackendKind backendKind)
|
||||
{
|
||||
return backendKind switch
|
||||
{
|
||||
DynamicCompilationBackendKind.SharedRoslynWorker => "[Perf] Backend: SharedRoslynWorker",
|
||||
DynamicCompilationBackendKind.OneShotRoslyn => "[Perf] Backend: OneShotRoslyn",
|
||||
DynamicCompilationBackendKind.AssemblyBuilderFallback => "[Perf] Backend: AssemblyBuilderFallback",
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7ad46f4a7e8b4ed8a171b2f90432af7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using Assembly = System.Reflection.Assembly;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicReferenceSetBuilder
|
||||
{
|
||||
private static readonly object ReferenceCatalogLock = new();
|
||||
private static readonly string[] BaseReferenceAssemblyNames =
|
||||
{
|
||||
"mscorlib",
|
||||
"netstandard",
|
||||
"System",
|
||||
"System.Core",
|
||||
"System.Runtime",
|
||||
"System.Collections",
|
||||
"System.Net.Http",
|
||||
"System.Threading",
|
||||
"System.Threading.Tasks",
|
||||
"UnityEngine.CoreModule",
|
||||
"UnityEditor.CoreModule",
|
||||
};
|
||||
|
||||
private static Dictionary<string, string> _cachedAssemblyLocationsByName;
|
||||
private static int _cachedAssemblyCount = -1;
|
||||
|
||||
public static List<string> BuildReferenceSet(
|
||||
List<string> additionalReferences,
|
||||
IReadOnlyCollection<string> resolvedAssemblyReferences,
|
||||
ExternalCompilerPaths externalCompilerPaths)
|
||||
{
|
||||
Dictionary<string, string> assemblyLocationsByName = GetCachedAssemblyLocationsByName();
|
||||
List<string> baseReferences = new List<string>();
|
||||
|
||||
foreach (string assemblyName in BaseReferenceAssemblyNames)
|
||||
{
|
||||
string preferredReferencePath = GetPreferredBaseReferencePath(externalCompilerPaths, assemblyName);
|
||||
if (!string.IsNullOrEmpty(preferredReferencePath) && File.Exists(preferredReferencePath))
|
||||
{
|
||||
baseReferences.Add(preferredReferencePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assemblyLocationsByName.TryGetValue(assemblyName, out string loadedAssemblyPath))
|
||||
{
|
||||
baseReferences.Add(loadedAssemblyPath);
|
||||
}
|
||||
}
|
||||
|
||||
AddLoadedAssemblyReferences(baseReferences, assemblyLocationsByName);
|
||||
|
||||
List<string> mergedAdditionalReferences = new List<string>();
|
||||
AddExistingReferences(mergedAdditionalReferences, additionalReferences);
|
||||
AddExistingReferences(mergedAdditionalReferences, resolvedAssemblyReferences);
|
||||
|
||||
string[] mergedReferences = MergeReferencesByAssemblyName(
|
||||
baseReferences.ToArray(),
|
||||
mergedAdditionalReferences);
|
||||
return new List<string>(mergedReferences);
|
||||
}
|
||||
|
||||
internal static string[] MergeReferencesByAssemblyName(string[] baseReferences, List<string> additionalReferences)
|
||||
{
|
||||
UnityEngine.Debug.Assert(baseReferences != null, "baseReferences must not be null");
|
||||
UnityEngine.Debug.Assert(additionalReferences != null, "additionalReferences must not be null");
|
||||
|
||||
HashSet<string> seenNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
List<string> mergedReferences = new List<string>(baseReferences.Length + additionalReferences.Count);
|
||||
|
||||
foreach (string baseReference in baseReferences)
|
||||
{
|
||||
string assemblyName = Path.GetFileNameWithoutExtension(baseReference);
|
||||
if (seenNames.Add(assemblyName))
|
||||
{
|
||||
mergedReferences.Add(baseReference);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string additionalReference in additionalReferences)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(additionalReference) || !File.Exists(additionalReference))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string assemblyName = Path.GetFileNameWithoutExtension(additionalReference);
|
||||
if (seenNames.Add(assemblyName))
|
||||
{
|
||||
mergedReferences.Add(additionalReference);
|
||||
}
|
||||
}
|
||||
|
||||
return mergedReferences.ToArray();
|
||||
}
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
private static void InvalidateReferenceCacheOnDomainReload()
|
||||
{
|
||||
lock (ReferenceCatalogLock)
|
||||
{
|
||||
_cachedAssemblyLocationsByName = null;
|
||||
_cachedAssemblyCount = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddLoadedAssemblyReferences(
|
||||
List<string> destination,
|
||||
IReadOnlyDictionary<string, string> assemblyLocationsByName)
|
||||
{
|
||||
List<string> assemblyNames = new List<string>(assemblyLocationsByName.Keys);
|
||||
assemblyNames.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (string assemblyName in assemblyNames)
|
||||
{
|
||||
if (!assemblyLocationsByName.TryGetValue(assemblyName, out string assemblyPath)
|
||||
|| string.IsNullOrEmpty(assemblyPath)
|
||||
|| !File.Exists(assemblyPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
destination.Add(assemblyPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> GetCachedAssemblyLocationsByName()
|
||||
{
|
||||
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
lock (ReferenceCatalogLock)
|
||||
{
|
||||
if (_cachedAssemblyLocationsByName != null && _cachedAssemblyCount == assemblies.Length)
|
||||
{
|
||||
return _cachedAssemblyLocationsByName;
|
||||
}
|
||||
|
||||
Dictionary<string, string> assemblyLocationsByName =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (Assembly assembly in assemblies)
|
||||
{
|
||||
if (assembly.IsDynamic)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string location;
|
||||
try
|
||||
{
|
||||
location = assembly.Location;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string assemblyName = assembly.GetName().Name;
|
||||
if (string.IsNullOrEmpty(location)
|
||||
|| !File.Exists(location)
|
||||
|| string.IsNullOrEmpty(assemblyName)
|
||||
|| assemblyLocationsByName.ContainsKey(assemblyName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
assemblyLocationsByName.Add(assemblyName, location);
|
||||
}
|
||||
|
||||
_cachedAssemblyLocationsByName = assemblyLocationsByName;
|
||||
_cachedAssemblyCount = assemblies.Length;
|
||||
return _cachedAssemblyLocationsByName;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPreferredBaseReferencePath(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
string assemblyName)
|
||||
{
|
||||
if (externalCompilerPaths == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolvePreferredBaseReferencePath(
|
||||
externalCompilerPaths.EditorContentsPath,
|
||||
externalCompilerPaths.ScriptingRootPath,
|
||||
assemblyName);
|
||||
}
|
||||
|
||||
internal static string ResolvePreferredBaseReferencePath(
|
||||
string editorContentsPath,
|
||||
string assemblyName)
|
||||
{
|
||||
return ResolvePreferredBaseReferencePath(editorContentsPath, null, assemblyName);
|
||||
}
|
||||
|
||||
internal static string ResolvePreferredBaseReferencePath(
|
||||
string editorContentsPath,
|
||||
string scriptingRootPath,
|
||||
string assemblyName)
|
||||
{
|
||||
foreach (string referenceRootPath in EnumerateReferenceRootPaths(editorContentsPath, scriptingRootPath))
|
||||
{
|
||||
string candidatePath = BuildPreferredBaseReferencePath(referenceRootPath, assemblyName);
|
||||
if (!string.IsNullOrEmpty(candidatePath) && File.Exists(candidatePath))
|
||||
{
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateReferenceRootPaths(
|
||||
string editorContentsPath,
|
||||
string scriptingRootPath)
|
||||
{
|
||||
HashSet<string> emittedPaths = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
if (!string.IsNullOrEmpty(scriptingRootPath) && emittedPaths.Add(scriptingRootPath))
|
||||
{
|
||||
yield return scriptingRootPath;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(editorContentsPath))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
string resourcesScriptingPath = Path.Combine(editorContentsPath, "Resources", "Scripting");
|
||||
if (emittedPaths.Add(resourcesScriptingPath))
|
||||
{
|
||||
yield return resourcesScriptingPath;
|
||||
}
|
||||
|
||||
if (emittedPaths.Add(editorContentsPath))
|
||||
{
|
||||
yield return editorContentsPath;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildPreferredBaseReferencePath(string referenceRootPath, string assemblyName)
|
||||
{
|
||||
switch (assemblyName)
|
||||
{
|
||||
case "mscorlib":
|
||||
case "netstandard":
|
||||
case "System":
|
||||
case "System.Core":
|
||||
case "System.Net.Http":
|
||||
return Path.Combine(referenceRootPath, "UnityReferenceAssemblies", "unity-4.8-api", $"{assemblyName}.dll");
|
||||
case "System.Runtime":
|
||||
case "System.Collections":
|
||||
case "System.Threading":
|
||||
case "System.Threading.Tasks":
|
||||
return Path.Combine(referenceRootPath, "UnityReferenceAssemblies", "unity-4.8-api", "Facades", $"{assemblyName}.dll");
|
||||
case "UnityEngine":
|
||||
case "UnityEditor":
|
||||
return Path.Combine(referenceRootPath, "Managed", $"{assemblyName}.dll");
|
||||
case "UnityEngine.CoreModule":
|
||||
case "UnityEditor.CoreModule":
|
||||
return Path.Combine(referenceRootPath, "Managed", "UnityEngine", $"{assemblyName}.dll");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddExistingReferences(
|
||||
List<string> destination,
|
||||
IReadOnlyCollection<string> source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string reference in source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reference) || !File.Exists(reference))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
destination.Add(reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abf32b53ef0d64589b3d503e67f97a28
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicReferenceSetBuilderService
|
||||
{
|
||||
public List<string> BuildReferenceSet(
|
||||
List<string> additionalReferences,
|
||||
IReadOnlyCollection<string> resolvedAssemblyReferences,
|
||||
ExternalCompilerPaths externalCompilerPaths)
|
||||
{
|
||||
return DynamicReferenceSetBuilder.BuildReferenceSet(
|
||||
additionalReferences,
|
||||
resolvedAssemblyReferences,
|
||||
externalCompilerPaths);
|
||||
}
|
||||
|
||||
public string[] MergeReferencesByAssemblyName(
|
||||
string[] baseReferences,
|
||||
List<string> additionalReferences)
|
||||
{
|
||||
return DynamicReferenceSetBuilder.MergeReferencesByAssemblyName(baseReferences, additionalReferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fbe931b9a8844a7e9060c52680a32d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class ExternalCompilerMessageParser
|
||||
{
|
||||
private static readonly Regex DiagnosticRegex = new(
|
||||
@"^(?<file>.+)\((?<line>\d+),(?<column>\d+)\): (?<severity>error|warning) (?<code>[A-Z]+\d+): (?<message>.+)$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public static CompilerMessage[] Parse(
|
||||
string stdout,
|
||||
string stderr,
|
||||
int exitCode)
|
||||
{
|
||||
List<CompilerMessage> messages = new List<CompilerMessage>();
|
||||
AddParsedMessages(messages, stdout);
|
||||
AddParsedMessages(messages, stderr);
|
||||
|
||||
if (messages.Count > 0)
|
||||
{
|
||||
if (exitCode != 0 && !HasError(messages))
|
||||
{
|
||||
messages.Add(CreateInfrastructureFailureMessage(stdout, stderr, exitCode));
|
||||
}
|
||||
|
||||
return messages.ToArray();
|
||||
}
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
return Array.Empty<CompilerMessage>();
|
||||
}
|
||||
|
||||
string combinedOutput = CombineOutput(stdout, stderr);
|
||||
return new CompilerMessage[]
|
||||
{
|
||||
new CompilerMessage
|
||||
{
|
||||
type = CompilerMessageType.Error,
|
||||
message = string.IsNullOrWhiteSpace(combinedOutput)
|
||||
? "External C# compiler failed without diagnostics"
|
||||
: combinedOutput
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasError(IReadOnlyCollection<CompilerMessage> messages)
|
||||
{
|
||||
foreach (CompilerMessage message in messages)
|
||||
{
|
||||
if (message.type == CompilerMessageType.Error)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static CompilerMessage CreateInfrastructureFailureMessage(
|
||||
string stdout,
|
||||
string stderr,
|
||||
int exitCode)
|
||||
{
|
||||
string combinedOutput = CombineOutput(stdout, stderr);
|
||||
string message = $"External C# compiler exited with code {exitCode} without reporting an error diagnostic";
|
||||
if (!string.IsNullOrWhiteSpace(combinedOutput))
|
||||
{
|
||||
message = $"{message}: {combinedOutput}";
|
||||
}
|
||||
|
||||
return new CompilerMessage
|
||||
{
|
||||
type = CompilerMessageType.Error,
|
||||
message = message
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddParsedMessages(
|
||||
List<CompilerMessage> messages,
|
||||
string output)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (string line in lines)
|
||||
{
|
||||
Match match = DiagnosticRegex.Match(line.Trim());
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.Add(new CompilerMessage
|
||||
{
|
||||
type = match.Groups["severity"].Value == "warning"
|
||||
? CompilerMessageType.Warning
|
||||
: CompilerMessageType.Error,
|
||||
message = $"{match.Groups["code"].Value}: {match.Groups["message"].Value}",
|
||||
file = match.Groups["file"].Value,
|
||||
line = int.Parse(match.Groups["line"].Value),
|
||||
column = int.Parse(match.Groups["column"].Value)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static string CombineOutput(string stdout, string stderr)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stdout))
|
||||
{
|
||||
return stderr?.Trim();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stderr))
|
||||
{
|
||||
return stdout.Trim();
|
||||
}
|
||||
|
||||
return $"{stdout.Trim()}\n{stderr.Trim()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 443b319d90df34fa78eb110cb3c87cc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class ExternalCompilerPathResolutionService
|
||||
{
|
||||
public ExternalCompilerPaths Resolve()
|
||||
{
|
||||
return ExternalCompilerPathResolver.Resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bdc0f83d884c4d58b131562f3ff6f5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class ExternalCompilerPathResolver
|
||||
{
|
||||
private const string NetCoreRuntimeDirectoryName = "NetCoreRuntime";
|
||||
private const string DotNetSdkRoslynDirectoryName = "DotNetSdkRoslyn";
|
||||
private const string DotNetSdkDirectoryName = "DotNetSdk";
|
||||
private const string DotNetSdkSdkDirectoryName = "sdk";
|
||||
private const string RoslynDirectoryName = "Roslyn";
|
||||
private const string CompilerBincoreDirectoryName = "bincore";
|
||||
private const string CompilerDllFileName = "csc.dll";
|
||||
private const string CompilerRuntimeConfigFileName = "csc.runtimeconfig.json";
|
||||
private const string CompilerDepsFileName = "csc.deps.json";
|
||||
private const string CodeAnalysisDllFileName = "Microsoft.CodeAnalysis.dll";
|
||||
private const string CodeAnalysisCSharpDllFileName = "Microsoft.CodeAnalysis.CSharp.dll";
|
||||
private const string NetCoreRuntimeSharedDirectoryName = "shared";
|
||||
private const string NetCoreRuntimeSharedFrameworkName = "Microsoft.NETCore.App";
|
||||
|
||||
public static ExternalCompilerPaths Resolve()
|
||||
{
|
||||
string editorPath = EditorApplication.applicationPath;
|
||||
if (string.IsNullOrEmpty(editorPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string contentsPath = ResolveEditorContentsPath(editorPath);
|
||||
if (string.IsNullOrEmpty(contentsPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string scriptingRootPath = ResolveScriptingRootPath(contentsPath);
|
||||
string dotnetHostFileName = UnityEngine.Application.platform == UnityEngine.RuntimePlatform.WindowsEditor
|
||||
? "dotnet.exe"
|
||||
: "dotnet";
|
||||
string effectiveScriptingRootPath = scriptingRootPath ?? contentsPath;
|
||||
string compilerDirectoryPath = ResolveCompilerDirectoryPath(effectiveScriptingRootPath);
|
||||
|
||||
List<string> missingComponents = new List<string>();
|
||||
if (string.IsNullOrEmpty(scriptingRootPath))
|
||||
{
|
||||
missingComponents.Add(Path.Combine(contentsPath, NetCoreRuntimeDirectoryName));
|
||||
missingComponents.Add(Path.Combine(contentsPath, DotNetSdkRoslynDirectoryName));
|
||||
missingComponents.Add(Path.Combine(contentsPath, DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName, "*", RoslynDirectoryName, CompilerBincoreDirectoryName));
|
||||
missingComponents.Add(Path.Combine(contentsPath, "Resources", "Scripting", NetCoreRuntimeDirectoryName));
|
||||
missingComponents.Add(Path.Combine(contentsPath, "Resources", "Scripting", DotNetSdkRoslynDirectoryName));
|
||||
missingComponents.Add(Path.Combine(contentsPath, "Resources", "Scripting", DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName, "*", RoslynDirectoryName, CompilerBincoreDirectoryName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(compilerDirectoryPath))
|
||||
{
|
||||
compilerDirectoryPath = Path.Combine(effectiveScriptingRootPath, DotNetSdkRoslynDirectoryName);
|
||||
missingComponents.Add(compilerDirectoryPath);
|
||||
missingComponents.Add(Path.Combine(effectiveScriptingRootPath, DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName, "*", RoslynDirectoryName, CompilerBincoreDirectoryName));
|
||||
}
|
||||
|
||||
string dotnetHostPath = Path.Combine(effectiveScriptingRootPath, NetCoreRuntimeDirectoryName, dotnetHostFileName);
|
||||
string compilerDllPath = Path.Combine(compilerDirectoryPath, CompilerDllFileName);
|
||||
string compilerRuntimeConfigPath = Path.Combine(compilerDirectoryPath, CompilerRuntimeConfigFileName);
|
||||
string compilerDepsFilePath = Path.Combine(compilerDirectoryPath, CompilerDepsFileName);
|
||||
string codeAnalysisDllPath = Path.Combine(compilerDirectoryPath, CodeAnalysisDllFileName);
|
||||
string codeAnalysisCSharpDllPath = Path.Combine(compilerDirectoryPath, CodeAnalysisCSharpDllFileName);
|
||||
string netCoreRuntimeSharedRootPath = Path.Combine(effectiveScriptingRootPath, NetCoreRuntimeDirectoryName, NetCoreRuntimeSharedDirectoryName, NetCoreRuntimeSharedFrameworkName);
|
||||
string netCoreRuntimeSharedDirectoryPath = ResolveNetCoreRuntimeSharedDirectoryPath(netCoreRuntimeSharedRootPath);
|
||||
|
||||
if (!File.Exists(dotnetHostPath))
|
||||
{
|
||||
missingComponents.Add(dotnetHostPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(compilerDllPath))
|
||||
{
|
||||
missingComponents.Add(compilerDllPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(compilerRuntimeConfigPath))
|
||||
{
|
||||
missingComponents.Add(compilerRuntimeConfigPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(compilerDepsFilePath))
|
||||
{
|
||||
missingComponents.Add(compilerDepsFilePath);
|
||||
}
|
||||
|
||||
if (!File.Exists(codeAnalysisDllPath))
|
||||
{
|
||||
missingComponents.Add(codeAnalysisDllPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(codeAnalysisCSharpDllPath))
|
||||
{
|
||||
missingComponents.Add(codeAnalysisCSharpDllPath);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(netCoreRuntimeSharedDirectoryPath))
|
||||
{
|
||||
missingComponents.Add(netCoreRuntimeSharedRootPath);
|
||||
}
|
||||
|
||||
if (missingComponents.Count > 0)
|
||||
{
|
||||
DynamicCompilationHealthMonitor.ReportFastPathUnavailable(
|
||||
editorPath,
|
||||
contentsPath,
|
||||
missingComponents);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ExternalCompilerPaths(
|
||||
contentsPath,
|
||||
scriptingRootPath,
|
||||
dotnetHostPath,
|
||||
compilerDllPath,
|
||||
compilerRuntimeConfigPath,
|
||||
compilerDepsFilePath,
|
||||
codeAnalysisDllPath,
|
||||
codeAnalysisCSharpDllPath,
|
||||
netCoreRuntimeSharedDirectoryPath);
|
||||
}
|
||||
|
||||
internal static string ResolveScriptingRootPath(string contentsPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentsPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string resourcesScriptingRootPath = Path.Combine(contentsPath, "Resources", "Scripting");
|
||||
if (ContainsExternalCompilerLayout(resourcesScriptingRootPath))
|
||||
{
|
||||
return resourcesScriptingRootPath;
|
||||
}
|
||||
|
||||
if (ContainsExternalCompilerLayout(contentsPath))
|
||||
{
|
||||
return contentsPath;
|
||||
}
|
||||
|
||||
return ResolveScriptingRootPathByScan(contentsPath);
|
||||
}
|
||||
|
||||
internal static string ResolveCompilerDirectoryPath(string scriptingRootPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(scriptingRootPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string legacyCompilerDirectoryPath = Path.Combine(scriptingRootPath, DotNetSdkRoslynDirectoryName);
|
||||
if (Directory.Exists(legacyCompilerDirectoryPath))
|
||||
{
|
||||
return legacyCompilerDirectoryPath;
|
||||
}
|
||||
|
||||
return ResolveDotNetSdkCompilerDirectoryPath(scriptingRootPath);
|
||||
}
|
||||
|
||||
internal static string ResolveNetCoreRuntimeSharedDirectoryPath(string netCoreRuntimeSharedRootPath)
|
||||
{
|
||||
if (!Directory.Exists(netCoreRuntimeSharedRootPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] runtimeDirectories = Directory.GetDirectories(netCoreRuntimeSharedRootPath);
|
||||
if (runtimeDirectories.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string highestVersionDirectoryPath = runtimeDirectories
|
||||
.Select(runtimeDirectoryPath => new
|
||||
{
|
||||
Path = runtimeDirectoryPath,
|
||||
VersionText = Path.GetFileName(runtimeDirectoryPath)
|
||||
})
|
||||
.Where(candidate => Version.TryParse(candidate.VersionText, out _))
|
||||
.OrderByDescending(candidate => new Version(candidate.VersionText))
|
||||
.ThenByDescending(candidate => candidate.VersionText, StringComparer.Ordinal)
|
||||
.Select(candidate => candidate.Path)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrEmpty(highestVersionDirectoryPath))
|
||||
{
|
||||
return highestVersionDirectoryPath;
|
||||
}
|
||||
|
||||
return runtimeDirectories
|
||||
.OrderByDescending(Path.GetFileName, StringComparer.Ordinal)
|
||||
.First();
|
||||
}
|
||||
|
||||
private static bool ContainsExternalCompilerLayout(string rootPath)
|
||||
{
|
||||
return Directory.Exists(Path.Combine(rootPath, NetCoreRuntimeDirectoryName))
|
||||
&& !string.IsNullOrEmpty(ResolveCompilerDirectoryPath(rootPath));
|
||||
}
|
||||
|
||||
private static string ResolveDotNetSdkCompilerDirectoryPath(string scriptingRootPath)
|
||||
{
|
||||
string sdkRootPath = Path.Combine(scriptingRootPath, DotNetSdkDirectoryName, DotNetSdkSdkDirectoryName);
|
||||
if (!Directory.Exists(sdkRootPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> sdkDirectoryPaths = Directory.GetDirectories(sdkRootPath).ToList();
|
||||
sdkDirectoryPaths.Sort(CompareSdkDirectoryPathsDescending);
|
||||
|
||||
foreach (string sdkDirectoryPath in sdkDirectoryPaths)
|
||||
{
|
||||
string compilerDirectoryPath = Path.Combine(sdkDirectoryPath, RoslynDirectoryName, CompilerBincoreDirectoryName);
|
||||
if (Directory.Exists(compilerDirectoryPath))
|
||||
{
|
||||
return compilerDirectoryPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int CompareSdkDirectoryPathsDescending(string leftPath, string rightPath)
|
||||
{
|
||||
string leftVersionText = Path.GetFileName(leftPath);
|
||||
string rightVersionText = Path.GetFileName(rightPath);
|
||||
bool leftIsVersion = Version.TryParse(leftVersionText, out Version leftVersion);
|
||||
bool rightIsVersion = Version.TryParse(rightVersionText, out Version rightVersion);
|
||||
|
||||
if (leftIsVersion && rightIsVersion)
|
||||
{
|
||||
int versionComparison = rightVersion.CompareTo(leftVersion);
|
||||
if (versionComparison != 0)
|
||||
{
|
||||
return versionComparison;
|
||||
}
|
||||
}
|
||||
else if (leftIsVersion)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (rightIsVersion)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return string.Compare(rightVersionText, leftVersionText, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string ResolveScriptingRootPathByScan(string contentsPath)
|
||||
{
|
||||
if (!Directory.Exists(contentsPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Queue<(string Path, int Depth)> pendingDirectories = new Queue<(string Path, int Depth)>();
|
||||
pendingDirectories.Enqueue((contentsPath, 0));
|
||||
|
||||
while (pendingDirectories.Count > 0)
|
||||
{
|
||||
(string currentPath, int depth) = pendingDirectories.Dequeue();
|
||||
if (ContainsExternalCompilerLayout(currentPath))
|
||||
{
|
||||
return currentPath;
|
||||
}
|
||||
|
||||
if (depth >= 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (string childDirectoryPath in Directory.GetDirectories(currentPath).OrderBy(path => path, StringComparer.Ordinal))
|
||||
{
|
||||
pendingDirectories.Enqueue((childDirectoryPath, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string ResolveEditorContentsPath(string editorPath)
|
||||
{
|
||||
if (editorPath.EndsWith(".app", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Path.Combine(editorPath, "Contents");
|
||||
}
|
||||
|
||||
string editorDirectoryPath = Path.GetDirectoryName(editorPath);
|
||||
if (string.IsNullOrEmpty(editorDirectoryPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string dataDirectoryPath = Path.Combine(editorDirectoryPath, "Data");
|
||||
if (Directory.Exists(dataDirectoryPath))
|
||||
{
|
||||
return dataDirectoryPath;
|
||||
}
|
||||
|
||||
string installRootPath = Path.GetDirectoryName(editorDirectoryPath);
|
||||
return string.IsNullOrEmpty(installRootPath)
|
||||
? null
|
||||
: installRootPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0a902466c1547459a9c96425de6aa5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class ExternalCompilerPaths
|
||||
{
|
||||
public string EditorContentsPath { get; }
|
||||
|
||||
public string ScriptingRootPath { get; }
|
||||
|
||||
public string DotnetHostPath { get; }
|
||||
|
||||
public string CompilerDllPath { get; }
|
||||
|
||||
public string CompilerRuntimeConfigPath { get; }
|
||||
|
||||
public string CompilerDepsFilePath { get; }
|
||||
|
||||
public string CodeAnalysisDllPath { get; }
|
||||
|
||||
public string CodeAnalysisCSharpDllPath { get; }
|
||||
|
||||
public string NetCoreRuntimeSharedDirectoryPath { get; }
|
||||
|
||||
public ExternalCompilerPaths(
|
||||
string editorContentsPath,
|
||||
string scriptingRootPath,
|
||||
string dotnetHostPath,
|
||||
string compilerDllPath,
|
||||
string compilerRuntimeConfigPath,
|
||||
string compilerDepsFilePath,
|
||||
string codeAnalysisDllPath,
|
||||
string codeAnalysisCSharpDllPath,
|
||||
string netCoreRuntimeSharedDirectoryPath)
|
||||
{
|
||||
EditorContentsPath = editorContentsPath;
|
||||
ScriptingRootPath = scriptingRootPath;
|
||||
DotnetHostPath = dotnetHostPath;
|
||||
CompilerDllPath = compilerDllPath;
|
||||
CompilerRuntimeConfigPath = compilerRuntimeConfigPath;
|
||||
CompilerDepsFilePath = compilerDepsFilePath;
|
||||
CodeAnalysisDllPath = codeAnalysisDllPath;
|
||||
CodeAnalysisCSharpDllPath = codeAnalysisCSharpDllPath;
|
||||
NetCoreRuntimeSharedDirectoryPath = netCoreRuntimeSharedDirectoryPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bb3226a7b31442cf99c600c10958c3d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class HoistedLiteralBinding
|
||||
{
|
||||
public string ParameterName { get; }
|
||||
public string TypeName { get; }
|
||||
public object Value { get; }
|
||||
|
||||
public HoistedLiteralBinding(string parameterName, string typeName, object value)
|
||||
{
|
||||
ParameterName = parameterName;
|
||||
TypeName = typeName;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d14d4c8ee615a4ab0a781cb6cdbcf63e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface ICompiledAssemblyBuilder
|
||||
{
|
||||
bool SupportsAutoPrewarm();
|
||||
|
||||
Task<CompiledAssemblyBuildResult> BuildAsync(
|
||||
DynamicCompilationPlan plan,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a21ff6d17a104be1a2d9951ca23fe49
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface ICompiledAssemblyLoader
|
||||
{
|
||||
CompiledAssemblyLoadResult Load(
|
||||
DynamicCodeSecurityLevel securityLevel,
|
||||
byte[] assemblyBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbe1e5720235d4df18ae3f6ec2a03deb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface IDynamicCompilationPlanner
|
||||
{
|
||||
DynamicCompilationPlan CreatePlan(CompilationRequest request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b0c124a0e80b4fcaa29ddf6226dc6e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,382 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Post-compilation security validator using IL byte inspection.
|
||||
/// Scans call/callvirt/newobj/ldtoken opcodes to detect dangerous API usage
|
||||
/// that source-level scanning cannot catch (e.g. using aliases, fully-qualified names).
|
||||
/// </summary>
|
||||
internal sealed class IlSecurityValidator
|
||||
{
|
||||
private static readonly OpCode[] SingleByteOpCodes = BuildOpCodeTable(singleByte: true);
|
||||
private static readonly OpCode[] MultiByteOpCodes = BuildOpCodeTable(singleByte: false);
|
||||
|
||||
public SecurityValidationResult Validate(Assembly assembly)
|
||||
{
|
||||
SecurityValidationResult result = new SecurityValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Violations = new List<SecurityViolation>()
|
||||
};
|
||||
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = Array.FindAll(ex.Types, static t => t != null);
|
||||
}
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
ValidateTypeHierarchy(type, result);
|
||||
ValidateMethodReferences(type, result);
|
||||
}
|
||||
|
||||
result.IsValid = result.Violations.Count == 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void ValidateTypeHierarchy(Type type, SecurityValidationResult result)
|
||||
{
|
||||
ValidateReferencedType(
|
||||
type.BaseType,
|
||||
result,
|
||||
typeName => $"Inheriting from dangerous type: {typeName}",
|
||||
typeName => $"Type '{type.FullName}' inherits from blocked type '{typeName}'");
|
||||
|
||||
foreach (Type iface in type.GetInterfaces())
|
||||
{
|
||||
ValidateReferencedType(
|
||||
iface,
|
||||
result,
|
||||
typeName => $"Implementing dangerous interface: {typeName}",
|
||||
typeName => $"Type '{type.FullName}' implements blocked interface '{typeName}'");
|
||||
}
|
||||
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Instance | BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly;
|
||||
|
||||
foreach (FieldInfo field in type.GetFields(flags))
|
||||
{
|
||||
ValidateReferencedType(
|
||||
field.FieldType,
|
||||
result,
|
||||
typeName => $"Using dangerous type as field: {typeName}",
|
||||
typeName => $"Field '{field.Name}' of type '{typeName}' in {type.FullName}");
|
||||
}
|
||||
|
||||
foreach (PropertyInfo property in type.GetProperties(flags))
|
||||
{
|
||||
ValidateReferencedType(
|
||||
property.PropertyType,
|
||||
result,
|
||||
typeName => $"Using dangerous type as property: {typeName}",
|
||||
typeName => $"Property '{property.Name}' of type '{typeName}' in {type.FullName}");
|
||||
|
||||
foreach (System.Reflection.ParameterInfo indexParameter in property.GetIndexParameters())
|
||||
{
|
||||
ValidateReferencedType(
|
||||
indexParameter.ParameterType,
|
||||
result,
|
||||
typeName => $"Using dangerous type as property index parameter: {typeName}",
|
||||
typeName => $"Property index parameter '{indexParameter.Name}' of type '{typeName}' in {type.FullName}.{property.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (EventInfo eventInfo in type.GetEvents(flags))
|
||||
{
|
||||
ValidateReferencedType(
|
||||
eventInfo.EventHandlerType,
|
||||
result,
|
||||
typeName => $"Using dangerous type as event handler: {typeName}",
|
||||
typeName => $"Event '{eventInfo.Name}' of type '{typeName}' in {type.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateMethodReferences(Type type, SecurityValidationResult result)
|
||||
{
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Instance | BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly;
|
||||
|
||||
foreach (MethodInfo method in type.GetMethods(flags))
|
||||
{
|
||||
ValidateMethodBody(method, result);
|
||||
}
|
||||
|
||||
foreach (ConstructorInfo ctor in type.GetConstructors(flags))
|
||||
{
|
||||
ValidateMethodBody(ctor, result);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateMethodBody(MethodBase method, SecurityValidationResult result)
|
||||
{
|
||||
MethodBody body;
|
||||
try
|
||||
{
|
||||
body = method.GetMethodBody();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Abstract/extern methods have no body
|
||||
return;
|
||||
}
|
||||
if (body == null) return;
|
||||
|
||||
foreach (LocalVariableInfo local in body.LocalVariables)
|
||||
{
|
||||
string typeName = local.LocalType?.FullName;
|
||||
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
|
||||
{
|
||||
result.Violations.Add(new SecurityViolation
|
||||
{
|
||||
Type = SecurityViolationType.DangerousApiCall,
|
||||
ApiName = typeName,
|
||||
Message = $"Using dangerous type as local variable: {typeName}",
|
||||
Description = $"Local variable of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach (System.Reflection.ParameterInfo param in method.GetParameters())
|
||||
{
|
||||
string typeName = param.ParameterType?.FullName;
|
||||
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
|
||||
{
|
||||
result.Violations.Add(new SecurityViolation
|
||||
{
|
||||
Type = SecurityViolationType.DangerousApiCall,
|
||||
ApiName = typeName,
|
||||
Message = $"Using dangerous type as parameter: {typeName}",
|
||||
Description = $"Parameter '{param.Name}' of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
byte[] il = body.GetILAsByteArray();
|
||||
if (il == null) return;
|
||||
|
||||
ValidateIlCalls(method, il, result);
|
||||
}
|
||||
|
||||
private static void ValidateReferencedType(
|
||||
Type referencedType,
|
||||
SecurityValidationResult result,
|
||||
Func<string, string> createMessage,
|
||||
Func<string, string> createDescription)
|
||||
{
|
||||
foreach (string typeName in ExpandReferencedTypes(referencedType))
|
||||
{
|
||||
if (!DangerousApiCatalog.IsDangerousType(typeName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Violations.Add(new SecurityViolation
|
||||
{
|
||||
Type = SecurityViolationType.DangerousApiCall,
|
||||
ApiName = typeName,
|
||||
Message = createMessage(typeName),
|
||||
Description = createDescription(typeName)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ExpandReferencedTypes(Type referencedType)
|
||||
{
|
||||
if (referencedType == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (referencedType.HasElementType)
|
||||
{
|
||||
foreach (string typeName in ExpandReferencedTypes(referencedType.GetElementType()))
|
||||
{
|
||||
yield return typeName;
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
string fullName = referencedType.FullName;
|
||||
if (!string.IsNullOrEmpty(fullName))
|
||||
{
|
||||
yield return fullName;
|
||||
}
|
||||
|
||||
if (!referencedType.IsGenericType)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (Type genericArgument in referencedType.GetGenericArguments())
|
||||
{
|
||||
foreach (string typeName in ExpandReferencedTypes(genericArgument))
|
||||
{
|
||||
yield return typeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateIlCalls(MethodBase method, byte[] il, SecurityValidationResult result)
|
||||
{
|
||||
int offset = 0;
|
||||
Module module = method.Module;
|
||||
Type[] typeArgs = method.DeclaringType?.IsGenericType == true
|
||||
? method.DeclaringType.GetGenericArguments()
|
||||
: null;
|
||||
Type[] methodArgs = method.IsGenericMethod ? method.GetGenericArguments() : null;
|
||||
|
||||
while (offset < il.Length)
|
||||
{
|
||||
OpCode opCode = ReadOpCode(il, ref offset);
|
||||
|
||||
if (opCode.OperandType == OperandType.InlineMethod)
|
||||
{
|
||||
int token = ReadInt32(il, offset);
|
||||
|
||||
if (opCode == OpCodes.Call || opCode == OpCodes.Callvirt || opCode == OpCodes.Newobj ||
|
||||
opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn)
|
||||
{
|
||||
MethodBase calledMethod;
|
||||
try
|
||||
{
|
||||
calledMethod = module.ResolveMethod(token, typeArgs, methodArgs);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
offset += 4;
|
||||
continue;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
offset += 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
string declaringType = calledMethod.DeclaringType?.FullName;
|
||||
string memberName = calledMethod.Name;
|
||||
|
||||
if (declaringType != null && DangerousApiCatalog.IsDangerousApi(declaringType, memberName))
|
||||
{
|
||||
result.Violations.Add(new SecurityViolation
|
||||
{
|
||||
Type = SecurityViolationType.DangerousApiCall,
|
||||
ApiName = $"{declaringType}.{memberName}",
|
||||
Message = $"Dangerous API call: {declaringType}.{memberName}",
|
||||
Description = $"IL {opCode.Name} to '{declaringType}.{memberName}' in {method.DeclaringType?.FullName}.{method.Name}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ldtoken with InlineTok operand: detect typeof(DangerousType)
|
||||
if (opCode == OpCodes.Ldtoken && opCode.OperandType == OperandType.InlineTok)
|
||||
{
|
||||
int token = ReadInt32(il, offset);
|
||||
|
||||
Type resolvedType;
|
||||
try
|
||||
{
|
||||
resolvedType = module.ResolveType(token, typeArgs, methodArgs);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
// ldtoken can also reference fields/methods, not just types
|
||||
offset += GetOperandSize(opCode.OperandType, il, offset);
|
||||
continue;
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// ldtoken can also reference fields/methods, not just types
|
||||
offset += GetOperandSize(opCode.OperandType, il, offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
string fullName = resolvedType.FullName;
|
||||
if (fullName != null && DangerousApiCatalog.IsDangerousType(fullName))
|
||||
{
|
||||
result.Violations.Add(new SecurityViolation
|
||||
{
|
||||
Type = SecurityViolationType.DangerousApiCall,
|
||||
ApiName = fullName,
|
||||
Message = $"typeof() reference to dangerous type: {fullName}",
|
||||
Description = $"typeof({fullName}) in {method.DeclaringType?.FullName}.{method.Name}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
offset += GetOperandSize(opCode.OperandType, il, offset);
|
||||
}
|
||||
}
|
||||
|
||||
private static OpCode ReadOpCode(byte[] il, ref int offset)
|
||||
{
|
||||
byte first = il[offset++];
|
||||
if (first != 0xFE) return SingleByteOpCodes[first];
|
||||
byte second = il[offset++];
|
||||
return MultiByteOpCodes[second];
|
||||
}
|
||||
|
||||
private static int ReadInt32(byte[] il, int offset)
|
||||
{
|
||||
return il[offset] | (il[offset + 1] << 8) | (il[offset + 2] << 16) | (il[offset + 3] << 24);
|
||||
}
|
||||
|
||||
private static int GetOperandSize(OperandType operandType, byte[] il, int offset)
|
||||
{
|
||||
switch (operandType)
|
||||
{
|
||||
case OperandType.InlineNone: return 0;
|
||||
case OperandType.ShortInlineBrTarget:
|
||||
case OperandType.ShortInlineI:
|
||||
case OperandType.ShortInlineVar: return 1;
|
||||
case OperandType.InlineVar: return 2;
|
||||
case OperandType.InlineI:
|
||||
case OperandType.InlineBrTarget:
|
||||
case OperandType.InlineField:
|
||||
case OperandType.InlineMethod:
|
||||
case OperandType.InlineSig:
|
||||
case OperandType.InlineString:
|
||||
case OperandType.InlineTok:
|
||||
case OperandType.InlineType:
|
||||
case OperandType.ShortInlineR: return 4;
|
||||
case OperandType.InlineI8:
|
||||
case OperandType.InlineR: return 8;
|
||||
case OperandType.InlineSwitch:
|
||||
int count = ReadInt32(il, offset);
|
||||
return 4 + (count * 4);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static OpCode[] BuildOpCodeTable(bool singleByte)
|
||||
{
|
||||
OpCode[] table = new OpCode[0x100];
|
||||
foreach (FieldInfo field in typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (field.GetValue(null) is not OpCode opCode) continue;
|
||||
ushort value = unchecked((ushort)opCode.Value);
|
||||
if (singleByte && value <= 0xFF)
|
||||
{
|
||||
table[value] = opCode;
|
||||
}
|
||||
else if (!singleByte && (value & 0xFF00) == 0xFE00)
|
||||
{
|
||||
table[value & 0xFF] = opCode;
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ee54a51745834456a2a2b24860fc4f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,376 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans wrapped source for potential type identifiers and pre-injects using directives
|
||||
/// before the first compilation, avoiding AutoUsingResolver retry loops (~590ms/retry saved).
|
||||
/// Only applies to script mode (wrapped source from WrapperTemplate); raw mode is excluded.
|
||||
/// </summary>
|
||||
internal static class PreUsingResolver
|
||||
{
|
||||
private static readonly HashSet<string> ExcludedIdentifiers = new(System.StringComparer.Ordinal)
|
||||
{
|
||||
"abstract", "as", "async", "await", "base", "bool", "break", "byte",
|
||||
"case", "catch", "char", "checked", "class", "const", "continue",
|
||||
"decimal", "default", "delegate", "do", "double", "else", "enum",
|
||||
"event", "explicit", "extern", "false", "finally", "fixed", "float",
|
||||
"for", "foreach", "goto", "if", "implicit", "in", "int", "interface",
|
||||
"internal", "is", "lock", "long", "namespace", "new", "null", "object",
|
||||
"operator", "out", "override", "params", "private", "protected", "public",
|
||||
"readonly", "record", "ref", "return", "sbyte", "sealed", "short",
|
||||
"sizeof", "stackalloc", "static", "string", "struct", "switch",
|
||||
"this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked",
|
||||
"unsafe", "ushort", "using", "var", "virtual", "void", "volatile", "while",
|
||||
"yield",
|
||||
"String", "Int32", "Int64", "Boolean", "Single", "Double", "Byte",
|
||||
"Object", "Void", "Decimal", "Char", "UInt32", "UInt64", "Int16", "UInt16",
|
||||
"SByte", "IntPtr", "UIntPtr",
|
||||
"Task", "Dictionary", "CancellationToken", "ExecuteAsync", "Execute"
|
||||
};
|
||||
|
||||
public static PreUsingResult Resolve(string wrappedSource, AssemblyTypeIndex index)
|
||||
{
|
||||
Debug.Assert(wrappedSource != null, "wrappedSource must not be null");
|
||||
Debug.Assert(index != null, "index must not be null");
|
||||
|
||||
HashSet<string> existingNamespaces = ExtractExistingNamespaces(wrappedSource);
|
||||
string userCodeSection = ExtractUserCodeSection(wrappedSource);
|
||||
HashSet<string> candidateTypes = ExtractTypeIdentifiers(userCodeSection);
|
||||
HashSet<string> qualifiedTypeIdentifiers = ExtractQualifiedTypeIdentifiers(userCodeSection);
|
||||
|
||||
HashSet<string> namespacesToAdd = new(System.StringComparer.Ordinal);
|
||||
List<string> assemblyReferencesToAdd = new List<string>();
|
||||
foreach (string typeName in candidateTypes)
|
||||
{
|
||||
List<string> namespaces = index.FindNamespacesForType(typeName);
|
||||
if (namespaces.Count == 1 && !existingNamespaces.Contains(namespaces[0]))
|
||||
{
|
||||
namespacesToAdd.Add(namespaces[0]);
|
||||
}
|
||||
|
||||
List<string> assemblyReferences = index.FindAssemblyLocationsForType(typeName);
|
||||
if (assemblyReferences.Count == 1)
|
||||
{
|
||||
AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, assemblyReferences[0]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string typeName in qualifiedTypeIdentifiers)
|
||||
{
|
||||
List<string> assemblyReferences = index.FindAssemblyLocationsForIdentifier(typeName);
|
||||
if (assemblyReferences.Count == 1)
|
||||
{
|
||||
AddAssemblyReferenceIfMissing(assemblyReferencesToAdd, assemblyReferences[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (namespacesToAdd.Count == 0)
|
||||
{
|
||||
return new PreUsingResult(
|
||||
wrappedSource,
|
||||
System.Array.Empty<string>(),
|
||||
assemblyReferencesToAdd);
|
||||
}
|
||||
|
||||
string updatedSource = AutoUsingResolver.InsertUsingDirectives(wrappedSource, namespacesToAdd);
|
||||
return new PreUsingResult(updatedSource, namespacesToAdd, assemblyReferencesToAdd);
|
||||
}
|
||||
|
||||
private static HashSet<string> ExtractExistingNamespaces(string source)
|
||||
{
|
||||
HashSet<string> namespaces = new(System.StringComparer.Ordinal);
|
||||
int pos = 0;
|
||||
int length = source.Length;
|
||||
|
||||
while (pos < length)
|
||||
{
|
||||
pos = SourceShaper.SkipWhitespace(source, pos);
|
||||
if (pos >= length) break;
|
||||
|
||||
if (source[pos] == '#')
|
||||
{
|
||||
pos = SkipToEndOfLine(source, pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip comments that appear before using directives
|
||||
int skipped = SourceShaper.AdvanceOneTokenPublic(source, pos);
|
||||
if (skipped > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_')
|
||||
{
|
||||
pos = skipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle "global using Ns;" — advance past "global" to reach "using"
|
||||
int usingPos = pos;
|
||||
if (SourceShaper.StartsWithKeyword(source, pos, "global"))
|
||||
{
|
||||
usingPos = SourceShaper.SkipWhitespace(source, pos + 6);
|
||||
}
|
||||
|
||||
if (!SourceShaper.StartsWithKeyword(source, usingPos, "using"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int afterUsing = SourceShaper.SkipWhitespace(source, usingPos + 5);
|
||||
if (SourceShaper.StartsWithKeyword(source, afterUsing, "static") ||
|
||||
SourceShaper.StartsWithKeyword(source, afterUsing, "var") ||
|
||||
(afterUsing < length && source[afterUsing] == '('))
|
||||
{
|
||||
pos = SkipToSemicolon(source, pos);
|
||||
continue;
|
||||
}
|
||||
|
||||
int semiPos = source.IndexOf(';', afterUsing);
|
||||
if (semiPos > afterUsing)
|
||||
{
|
||||
string ns = source.Substring(afterUsing, semiPos - afterUsing).Trim();
|
||||
int eqIdx = ns.IndexOf('=');
|
||||
if (eqIdx >= 0)
|
||||
{
|
||||
ns = ns.Substring(eqIdx + 1).Trim();
|
||||
}
|
||||
if (ns.Length > 0)
|
||||
{
|
||||
namespaces.Add(ns);
|
||||
}
|
||||
}
|
||||
pos = semiPos >= 0 ? semiPos + 1 : length;
|
||||
}
|
||||
|
||||
return namespaces;
|
||||
}
|
||||
|
||||
internal static HashSet<string> ExtractTypeIdentifiers(string source)
|
||||
{
|
||||
HashSet<string> identifiers = new(System.StringComparer.Ordinal);
|
||||
int pos = 0;
|
||||
int length = source.Length;
|
||||
bool prevWasDot = false;
|
||||
|
||||
while (pos < length)
|
||||
{
|
||||
char c = source[pos];
|
||||
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos);
|
||||
if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_')
|
||||
{
|
||||
prevWasDot = false;
|
||||
pos = advanced;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '.')
|
||||
{
|
||||
prevWasDot = true;
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsLetter(c) || c == '_')
|
||||
{
|
||||
int start = pos;
|
||||
while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_'))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (!prevWasDot && char.IsUpper(c))
|
||||
{
|
||||
string identifier = source.Substring(start, pos - start);
|
||||
|
||||
// Member initializers (Name = ...), named arguments (Name: ...),
|
||||
// and labels (Name:) are not type candidates; skip them.
|
||||
// Exclude == so comparisons are not mistakenly filtered.
|
||||
int next = SourceShaper.SkipWhitespace(source, pos);
|
||||
bool looksLikeMemberOrLabel =
|
||||
next < length &&
|
||||
(source[next] == ':' ||
|
||||
(source[next] == '=' && (next + 1 >= length || source[next + 1] != '=')));
|
||||
|
||||
if (!looksLikeMemberOrLabel && !ExcludedIdentifiers.Contains(identifier))
|
||||
{
|
||||
identifiers.Add(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
prevWasDot = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
prevWasDot = false;
|
||||
pos = advanced;
|
||||
}
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
internal static HashSet<string> ExtractQualifiedTypeIdentifiers(string source)
|
||||
{
|
||||
HashSet<string> identifiers = new(System.StringComparer.Ordinal);
|
||||
int pos = 0;
|
||||
int length = source.Length;
|
||||
bool sawDot = false;
|
||||
List<string> qualifiedChainParts = new List<string>();
|
||||
|
||||
while (pos < length)
|
||||
{
|
||||
char c = source[pos];
|
||||
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int advanced = SourceShaper.AdvanceOneTokenPublic(source, pos);
|
||||
if (advanced > pos + 1 && !char.IsLetterOrDigit(source[pos]) && source[pos] != '_')
|
||||
{
|
||||
sawDot = false;
|
||||
qualifiedChainParts.Clear();
|
||||
pos = advanced;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '.')
|
||||
{
|
||||
sawDot = true;
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsLetter(c) || c == '_')
|
||||
{
|
||||
int start = pos;
|
||||
while (pos < length && (char.IsLetterOrDigit(source[pos]) || source[pos] == '_'))
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
string identifier = source.Substring(start, pos - start);
|
||||
if (!sawDot)
|
||||
{
|
||||
qualifiedChainParts.Clear();
|
||||
qualifiedChainParts.Add(identifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
qualifiedChainParts.Add(identifier);
|
||||
if (ShouldResolveQualifiedTypeAssembly(qualifiedChainParts))
|
||||
{
|
||||
identifiers.Add(string.Join(".", qualifiedChainParts));
|
||||
}
|
||||
}
|
||||
|
||||
sawDot = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
sawDot = false;
|
||||
qualifiedChainParts.Clear();
|
||||
pos = advanced;
|
||||
}
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
private static bool ShouldResolveQualifiedTypeAssembly(IReadOnlyList<string> qualifiedChainParts)
|
||||
{
|
||||
return qualifiedChainParts != null && qualifiedChainParts.Count >= 2;
|
||||
}
|
||||
|
||||
// WrapperTemplate marks user code with #line directives;
|
||||
// scanning only this region avoids false positives from boilerplate identifiers
|
||||
private static string ExtractUserCodeSection(string wrappedSource)
|
||||
{
|
||||
string startMarker = WrapperTemplate.UserCodeStartMarker;
|
||||
string endMarker = WrapperTemplate.UserCodeEndMarker;
|
||||
|
||||
int startIdx = wrappedSource.IndexOf(startMarker, System.StringComparison.Ordinal);
|
||||
// Wrapped source from WrapperTemplate should always contain the marker
|
||||
Debug.Assert(startIdx >= 0, "UserCodeStartMarker not found in wrappedSource");
|
||||
if (startIdx < 0)
|
||||
{
|
||||
return wrappedSource;
|
||||
}
|
||||
|
||||
int codeStart = wrappedSource.IndexOf('\n', startIdx);
|
||||
Debug.Assert(codeStart >= 0, "newline after UserCodeStartMarker not found");
|
||||
if (codeStart < 0)
|
||||
{
|
||||
return wrappedSource;
|
||||
}
|
||||
codeStart++;
|
||||
|
||||
int endIdx = wrappedSource.IndexOf(endMarker, codeStart, System.StringComparison.Ordinal);
|
||||
Debug.Assert(endIdx >= 0, "UserCodeEndMarker not found in wrappedSource");
|
||||
if (endIdx < 0)
|
||||
{
|
||||
return wrappedSource.Substring(codeStart);
|
||||
}
|
||||
|
||||
return wrappedSource.Substring(codeStart, endIdx - codeStart);
|
||||
}
|
||||
|
||||
private static int SkipToEndOfLine(string s, int pos)
|
||||
{
|
||||
while (pos < s.Length && s[pos] != '\n') pos++;
|
||||
if (pos < s.Length) pos++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static int SkipToSemicolon(string s, int pos)
|
||||
{
|
||||
int semi = s.IndexOf(';', pos);
|
||||
return semi >= 0 ? semi + 1 : s.Length;
|
||||
}
|
||||
|
||||
private static void AddAssemblyReferenceIfMissing(
|
||||
List<string> assemblyReferencesToAdd,
|
||||
string assemblyReference)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assemblyReference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string existingReference in assemblyReferencesToAdd)
|
||||
{
|
||||
if (string.Equals(existingReference, assemblyReference, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assemblyReferencesToAdd.Add(assemblyReference);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PreUsingResult
|
||||
{
|
||||
public string UpdatedSource { get; }
|
||||
|
||||
public IReadOnlyCollection<string> AddedNamespaces { get; }
|
||||
|
||||
public IReadOnlyCollection<string> AddedAssemblyReferences { get; }
|
||||
|
||||
public PreUsingResult(
|
||||
string updatedSource,
|
||||
IReadOnlyCollection<string> addedNamespaces,
|
||||
IReadOnlyCollection<string> addedAssemblyReferences)
|
||||
{
|
||||
UpdatedSource = updatedSource;
|
||||
AddedNamespaces = addedNamespaces;
|
||||
AddedAssemblyReferences = addedAssemblyReferences;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd0c59b5504094ef0a08a56f0617524c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Compatibility wrapper that keeps the old type name while delegating to the structured metadata validator.
|
||||
/// The preload security check must inspect real metadata rows so user literals do not trigger false positives.
|
||||
/// </summary>
|
||||
internal sealed class PreloadMetadataSecurityValidator : IPreloadAssemblySecurityValidator
|
||||
{
|
||||
private readonly SystemReflectionMetadataPreloadValidator _validator = new SystemReflectionMetadataPreloadValidator();
|
||||
|
||||
public SecurityValidationResult Validate(byte[] assemblyBytes)
|
||||
{
|
||||
return _validator.Validate(assemblyBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd9a3932eef9a4a519b81f9b83044b52
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class PreparedDynamicCode
|
||||
{
|
||||
public string PreparedSource { get; }
|
||||
public bool IsScriptMode { get; }
|
||||
public List<HoistedLiteralBinding> HoistedLiteralBindings { get; }
|
||||
|
||||
public PreparedDynamicCode(
|
||||
string preparedSource,
|
||||
bool isScriptMode,
|
||||
List<HoistedLiteralBinding> hoistedLiteralBindings)
|
||||
{
|
||||
PreparedSource = preparedSource;
|
||||
IsScriptMode = isScriptMode;
|
||||
HoistedLiteralBindings = hoistedLiteralBindings ?? new List<HoistedLiteralBinding>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c942bf096c16b4ce581ba1ca5c5226ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,443 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class RoslynCompilerBackend
|
||||
{
|
||||
private sealed class OneShotCompileResult
|
||||
{
|
||||
public DynamicCompilationBackendResult BackendResult { get; }
|
||||
|
||||
public bool ShouldFallback { get; }
|
||||
|
||||
private OneShotCompileResult(DynamicCompilationBackendResult backendResult, bool shouldFallback)
|
||||
{
|
||||
BackendResult = backendResult;
|
||||
ShouldFallback = shouldFallback;
|
||||
}
|
||||
|
||||
public static OneShotCompileResult Successful(CompilerMessage[] compilerMessages)
|
||||
{
|
||||
return new OneShotCompileResult(
|
||||
new DynamicCompilationBackendResult(
|
||||
compilerMessages,
|
||||
DynamicCompilationBackendKind.OneShotRoslyn),
|
||||
false);
|
||||
}
|
||||
|
||||
public static OneShotCompileResult Fallback()
|
||||
{
|
||||
return new OneShotCompileResult(null, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<DynamicCompilationBackendResult> CompileAsync(
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
List<string> references,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
string workerRequestFilePath = Path.ChangeExtension(sourcePath, ".worker");
|
||||
string[] defineSymbols = GetActiveDefineSymbols();
|
||||
bool allowUnsafeCode = PlayerSettings.allowUnsafeCode;
|
||||
|
||||
try
|
||||
{
|
||||
WriteWorkerRequestFile(
|
||||
workerRequestFilePath,
|
||||
sourcePath,
|
||||
dllPath,
|
||||
references,
|
||||
defineSymbols,
|
||||
allowUnsafeCode);
|
||||
|
||||
CompilerMessage[] workerMessages = SharedRoslynCompilerWorkerHost.TryCompile(
|
||||
workerRequestFilePath,
|
||||
externalCompilerPaths,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount);
|
||||
if (workerMessages != null)
|
||||
{
|
||||
return new DynamicCompilationBackendResult(
|
||||
workerMessages,
|
||||
DynamicCompilationBackendKind.SharedRoslynWorker);
|
||||
}
|
||||
|
||||
DynamicCompilationHealthMonitor.ReportSharedWorkerFallback(
|
||||
"worker_unavailable",
|
||||
new
|
||||
{
|
||||
platform = Application.platform.ToString(),
|
||||
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
||||
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
||||
});
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
OneShotCompileResult oneShotResult = await CompileWithOneShotAsync(
|
||||
sourcePath,
|
||||
dllPath,
|
||||
references,
|
||||
defineSymbols,
|
||||
allowUnsafeCode,
|
||||
externalCompilerPaths,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount).ConfigureAwait(false);
|
||||
if (oneShotResult.ShouldFallback)
|
||||
{
|
||||
return await AssemblyBuilderFallbackCompilerBackend.CompileAsync(
|
||||
sourcePath,
|
||||
dllPath,
|
||||
references,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return oneShotResult.BackendResult;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(workerRequestFilePath))
|
||||
{
|
||||
File.Delete(workerRequestFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<OneShotCompileResult> CompileWithOneShotAsync(
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
List<string> references,
|
||||
IReadOnlyCollection<string> defineSymbols,
|
||||
bool allowUnsafeCode,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
string responseFilePath = Path.ChangeExtension(sourcePath, ".rsp");
|
||||
WriteCompilerResponseFile(
|
||||
responseFilePath,
|
||||
sourcePath,
|
||||
dllPath,
|
||||
references,
|
||||
defineSymbols,
|
||||
allowUnsafeCode);
|
||||
|
||||
try
|
||||
{
|
||||
incrementBuildCount();
|
||||
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = externalCompilerPaths.DotnetHostPath,
|
||||
Arguments = $"{QuoteCommandLineArgument(externalCompilerPaths.CompilerDllPath)} @{QuoteCommandLineArgument(responseFilePath)}",
|
||||
WorkingDirectory = Directory.GetCurrentDirectory(),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
markBuildStarted();
|
||||
|
||||
try
|
||||
{
|
||||
using Process process = ProcessStartHelper.TryStart(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
DynamicCompilationHealthMonitor.ReportOneShotCompilerStartFailure(new
|
||||
{
|
||||
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
||||
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
||||
});
|
||||
|
||||
return OneShotCompileResult.Fallback();
|
||||
}
|
||||
|
||||
OneShotProcessCompletionResult completionResult = await WaitForOneShotCompilerAsync(process, ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
|
||||
completionResult.StandardOutput,
|
||||
completionResult.StandardError,
|
||||
completionResult.ExitCode);
|
||||
if (ShouldRetryWithAssemblyBuilder(process.ExitCode, compilerMessages))
|
||||
{
|
||||
ReportInfrastructureFallback(externalCompilerPaths, process.ExitCode);
|
||||
return OneShotCompileResult.Fallback();
|
||||
}
|
||||
|
||||
return OneShotCompileResult.Successful(compilerMessages);
|
||||
}
|
||||
finally
|
||||
{
|
||||
markBuildFinished();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(responseFilePath))
|
||||
{
|
||||
File.Delete(responseFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteCompilerResponseFile(
|
||||
string responseFilePath,
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
IReadOnlyCollection<string> references,
|
||||
IReadOnlyCollection<string> defineSymbols,
|
||||
bool allowUnsafeCode)
|
||||
{
|
||||
List<string> lines = new List<string>
|
||||
{
|
||||
"-nologo",
|
||||
"-nostdlib+",
|
||||
"-target:library",
|
||||
"-optimize+",
|
||||
"-debug-",
|
||||
allowUnsafeCode ? "-unsafe+" : "-unsafe-",
|
||||
QuoteResponseFileArgument("-out:", dllPath)
|
||||
};
|
||||
|
||||
string defineOption = BuildDefineOption(defineSymbols);
|
||||
if (!string.IsNullOrEmpty(defineOption))
|
||||
{
|
||||
lines.Add(defineOption);
|
||||
}
|
||||
|
||||
foreach (string reference in references)
|
||||
{
|
||||
lines.Add(QuoteResponseFileArgument("-r:", reference));
|
||||
}
|
||||
|
||||
lines.Add(QuoteResponseFilePath(sourcePath));
|
||||
File.WriteAllLines(responseFilePath, lines);
|
||||
}
|
||||
|
||||
internal static void WriteWorkerRequestFile(
|
||||
string requestFilePath,
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
IReadOnlyCollection<string> references,
|
||||
IReadOnlyCollection<string> defineSymbols,
|
||||
bool allowUnsafeCode)
|
||||
{
|
||||
List<string> lines = new List<string> { Path.GetFullPath(sourcePath), Path.GetFullPath(dllPath) };
|
||||
lines.Add(allowUnsafeCode ? "unsafe:1" : "unsafe:0");
|
||||
|
||||
string serializedDefines = SerializeDefineSymbols(defineSymbols);
|
||||
if (!string.IsNullOrEmpty(serializedDefines))
|
||||
{
|
||||
lines.Add($"define:{serializedDefines}");
|
||||
}
|
||||
|
||||
foreach (string reference in references)
|
||||
{
|
||||
lines.Add($"ref:{Path.GetFullPath(reference)}");
|
||||
}
|
||||
|
||||
File.WriteAllLines(Path.GetFullPath(requestFilePath), lines);
|
||||
}
|
||||
|
||||
private static string[] GetActiveDefineSymbols()
|
||||
{
|
||||
string[] activeDefines = EditorUserBuildSettings.activeScriptCompilationDefines;
|
||||
if (activeDefines == null || activeDefines.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
List<string> filteredDefines = new List<string>(activeDefines.Length);
|
||||
foreach (string define in activeDefines)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(define))
|
||||
{
|
||||
filteredDefines.Add(define);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDefines.ToArray();
|
||||
}
|
||||
|
||||
private static string BuildDefineOption(IReadOnlyCollection<string> defineSymbols)
|
||||
{
|
||||
string serializedDefines = SerializeDefineSymbols(defineSymbols);
|
||||
if (string.IsNullOrEmpty(serializedDefines))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $"-define:{serializedDefines}";
|
||||
}
|
||||
|
||||
private static string SerializeDefineSymbols(IReadOnlyCollection<string> defineSymbols)
|
||||
{
|
||||
if (defineSymbols == null || defineSymbols.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
List<string> filteredDefines = new List<string>(defineSymbols.Count);
|
||||
foreach (string defineSymbol in defineSymbols)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(defineSymbol))
|
||||
{
|
||||
filteredDefines.Add(defineSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredDefines.Count == 0 ? string.Empty : string.Join(";", filteredDefines);
|
||||
}
|
||||
|
||||
private static string QuoteResponseFileArgument(string prefix, string value)
|
||||
{
|
||||
return $"{prefix}{QuoteResponseFilePath(value)}";
|
||||
}
|
||||
|
||||
private static string QuoteResponseFilePath(string path)
|
||||
{
|
||||
return $"\"{path}\"";
|
||||
}
|
||||
|
||||
// Infrastructure-level failures (non-zero exit without file-specific diagnostics)
|
||||
// indicate the compiler itself broke, not the user's code.
|
||||
private static bool ShouldRetryWithAssemblyBuilder(
|
||||
int exitCode,
|
||||
IReadOnlyCollection<CompilerMessage> compilerMessages)
|
||||
{
|
||||
if (exitCode == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (CompilerMessage compilerMessage in compilerMessages)
|
||||
{
|
||||
if (compilerMessage.type == CompilerMessageType.Error &&
|
||||
!string.IsNullOrWhiteSpace(compilerMessage.file))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string QuoteCommandLineArgument(string value)
|
||||
{
|
||||
return $"\"{value}\"";
|
||||
}
|
||||
|
||||
internal sealed class OneShotProcessCompletionResult
|
||||
{
|
||||
public string StandardOutput { get; }
|
||||
|
||||
public string StandardError { get; }
|
||||
|
||||
public int ExitCode { get; }
|
||||
|
||||
public OneShotProcessCompletionResult(
|
||||
string standardOutput,
|
||||
string standardError,
|
||||
int exitCode)
|
||||
{
|
||||
StandardOutput = standardOutput;
|
||||
StandardError = standardError;
|
||||
ExitCode = exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<OneShotProcessCompletionResult> WaitForOneShotCompilerAsync(
|
||||
Process process,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
|
||||
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
|
||||
Task waitForExitTask = Task.Run(() => process.WaitForExit());
|
||||
return await AwaitOneShotProcessCompletionAsync(
|
||||
stdoutTask,
|
||||
stderrTask,
|
||||
waitForExitTask,
|
||||
() => process.ExitCode,
|
||||
() => RequestCancellation(process),
|
||||
ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void RequestCancellation(Process process)
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<OneShotProcessCompletionResult> AwaitOneShotProcessCompletionAsync(
|
||||
Task<string> stdoutTask,
|
||||
Task<string> stderrTask,
|
||||
Task waitForExitTask,
|
||||
Func<int> getExitCode,
|
||||
Action requestCancellation,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Task completionTask = Task.WhenAll(stdoutTask, stderrTask, waitForExitTask);
|
||||
Task cancellationTask = Task.Delay(Timeout.Infinite, ct);
|
||||
|
||||
Task finishedTask = await Task.WhenAny(completionTask, cancellationTask).ConfigureAwait(false);
|
||||
if (!ReferenceEquals(finishedTask, completionTask))
|
||||
{
|
||||
requestCancellation();
|
||||
ObserveTaskFault(completionTask);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
await completionTask.ConfigureAwait(false);
|
||||
return new OneShotProcessCompletionResult(
|
||||
stdoutTask.Result,
|
||||
stderrTask.Result,
|
||||
getExitCode());
|
||||
}
|
||||
|
||||
internal static void ReportInfrastructureFallback(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
int exitCode)
|
||||
{
|
||||
DynamicCompilationHealthMonitor.ReportOneShotCompilerStartFailure(new
|
||||
{
|
||||
reason = "infrastructure_failure",
|
||||
exit_code = exitCode,
|
||||
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
||||
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
||||
});
|
||||
}
|
||||
|
||||
private static void ObserveTaskFault(Task task)
|
||||
{
|
||||
_ = task.ContinueWith(
|
||||
static observedTask => _ = observedTask.Exception,
|
||||
CancellationToken.None,
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24f46670e12924be896455941a5956b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,946 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Compilation;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class SharedRoslynCompilerWorkerHost
|
||||
{
|
||||
private const int SharedCompilerWorkerMaxAttempts = 2;
|
||||
private const string SharedCompilerWorkerResultPrefix = "__ULOOP_RESULT__";
|
||||
private const string SharedCompilerWorkerEndMarker = "__ULOOP_END__";
|
||||
private const string SharedCompilerWorkerQuitCommand = "__QUIT__";
|
||||
internal const string CompileRequestPathPrefix = "path-base64:";
|
||||
private const string RoslynWorkerSourceFileName = "RoslynCompilerWorker.cs";
|
||||
private const string RoslynWorkerAssemblyFileName = "RoslynCompilerWorker.dll";
|
||||
private const string RoslynWorkerCompileResponseFileName = "RoslynCompilerWorker.rsp";
|
||||
private const string RoslynWorkerProgramTemplateRelativePath =
|
||||
"Editor/Compilation/Templates/SharedRoslynCompilerWorkerProgram.cs.template";
|
||||
private const string SharedCompilerWorkerResultPrefixToken = "{{SHARED_COMPILER_WORKER_RESULT_PREFIX}}";
|
||||
private const string SharedCompilerWorkerEndMarkerToken = "{{SHARED_COMPILER_WORKER_END_MARKER}}";
|
||||
private const string SharedCompilerWorkerQuitCommandToken = "{{SHARED_COMPILER_WORKER_QUIT_COMMAND}}";
|
||||
private const string CompileRequestPathPrefixToken = "{{COMPILE_REQUEST_PATH_PREFIX}}";
|
||||
private const int SharedCompilerWorkerResponseTimeoutMilliseconds = 30000;
|
||||
private const int WorkerAssemblyBuildTimeoutMilliseconds = 30000;
|
||||
internal const string DotnetMultilevelLookupEnvironmentVariableName = "DOTNET_MULTILEVEL_LOOKUP";
|
||||
internal const string DotnetMultilevelLookupDisabledValue = "0";
|
||||
|
||||
private static readonly object SharedCompilerWorkerLock = new();
|
||||
private static Action<string> s_deleteWorkerDirectory = path => Directory.Delete(path, true);
|
||||
private static Action<Process, string> s_sendCompileRequest = SendCompileRequestCore;
|
||||
private static Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]>
|
||||
s_compileWorkerAssemblyForTests;
|
||||
private static Process _sharedCompilerWorkerProcess;
|
||||
private static string _workerDirectoryPath;
|
||||
|
||||
private sealed class WorkerAttemptResult
|
||||
{
|
||||
public CompilerMessage[] Messages { get; }
|
||||
|
||||
public bool ShouldRetry { get; }
|
||||
|
||||
public string FailureReason { get; }
|
||||
|
||||
public object FailureContext { get; }
|
||||
|
||||
private WorkerAttemptResult(
|
||||
CompilerMessage[] messages,
|
||||
bool shouldRetry,
|
||||
string failureReason,
|
||||
object failureContext)
|
||||
{
|
||||
Messages = messages;
|
||||
ShouldRetry = shouldRetry;
|
||||
FailureReason = failureReason;
|
||||
FailureContext = failureContext;
|
||||
}
|
||||
|
||||
public bool Succeeded => Messages != null;
|
||||
|
||||
public static WorkerAttemptResult Successful(CompilerMessage[] messages)
|
||||
{
|
||||
return new WorkerAttemptResult(messages, false, null, null);
|
||||
}
|
||||
|
||||
public static WorkerAttemptResult RetryableFailure(string failureReason, object failureContext)
|
||||
{
|
||||
return new WorkerAttemptResult(null, true, failureReason, failureContext);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WorkerStartupResult
|
||||
{
|
||||
public bool IsReady { get; }
|
||||
|
||||
public string FailureReason { get; }
|
||||
|
||||
public object FailureContext { get; }
|
||||
|
||||
private WorkerStartupResult(bool isReady, string failureReason, object failureContext)
|
||||
{
|
||||
IsReady = isReady;
|
||||
FailureReason = failureReason;
|
||||
FailureContext = failureContext;
|
||||
}
|
||||
|
||||
public static WorkerStartupResult Ready()
|
||||
{
|
||||
return new WorkerStartupResult(true, null, null);
|
||||
}
|
||||
|
||||
public static WorkerStartupResult Failure(string failureReason, object failureContext)
|
||||
{
|
||||
return new WorkerStartupResult(false, failureReason, failureContext);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WorkerAssemblyBuildResult
|
||||
{
|
||||
public bool StartedSuccessfully { get; }
|
||||
|
||||
public CompilerMessage[] Messages { get; }
|
||||
|
||||
public string FailureReason { get; }
|
||||
|
||||
public object FailureContext { get; }
|
||||
|
||||
private WorkerAssemblyBuildResult(
|
||||
bool startedSuccessfully,
|
||||
CompilerMessage[] messages,
|
||||
string failureReason,
|
||||
object failureContext)
|
||||
{
|
||||
StartedSuccessfully = startedSuccessfully;
|
||||
Messages = messages;
|
||||
FailureReason = failureReason;
|
||||
FailureContext = failureContext;
|
||||
}
|
||||
|
||||
public static WorkerAssemblyBuildResult Started(CompilerMessage[] messages)
|
||||
{
|
||||
return new WorkerAssemblyBuildResult(true, messages, null, null);
|
||||
}
|
||||
|
||||
public static WorkerAssemblyBuildResult StartFailure(string failureReason, object failureContext)
|
||||
{
|
||||
return new WorkerAssemblyBuildResult(false, null, failureReason, failureContext);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class WorkerPaths
|
||||
{
|
||||
public string DirectoryPath { get; }
|
||||
|
||||
public string SourcePath { get; }
|
||||
|
||||
public string AssemblyPath { get; }
|
||||
|
||||
public string CompileResponseFilePath { get; }
|
||||
|
||||
public WorkerPaths(
|
||||
string directoryPath,
|
||||
string sourcePath,
|
||||
string assemblyPath,
|
||||
string compileResponseFilePath)
|
||||
{
|
||||
DirectoryPath = directoryPath;
|
||||
SourcePath = sourcePath;
|
||||
AssemblyPath = assemblyPath;
|
||||
CompileResponseFilePath = compileResponseFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
[InitializeOnLoadMethod]
|
||||
private static void RegisterLifecycle()
|
||||
{
|
||||
AssemblyReloadEvents.beforeAssemblyReload -= ShutdownForReload;
|
||||
AssemblyReloadEvents.beforeAssemblyReload += ShutdownForReload;
|
||||
EditorApplication.quitting -= ShutdownForQuit;
|
||||
EditorApplication.quitting += ShutdownForQuit;
|
||||
}
|
||||
|
||||
public static CompilerMessage[] TryCompile(
|
||||
string requestFilePath,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
lock (SharedCompilerWorkerLock)
|
||||
{
|
||||
return TryCompileWithRetries(
|
||||
requestFilePath,
|
||||
externalCompilerPaths,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompilerMessage[] TryCompileWithRetries(
|
||||
string requestFilePath,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
for (int attempt = 1; attempt <= SharedCompilerWorkerMaxAttempts; attempt++)
|
||||
{
|
||||
WorkerAttemptResult attemptResult = TryCompileOnce(
|
||||
requestFilePath,
|
||||
externalCompilerPaths,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount);
|
||||
|
||||
if (attemptResult.Succeeded)
|
||||
{
|
||||
return attemptResult.Messages;
|
||||
}
|
||||
|
||||
ShutdownWorkerProcessLocked();
|
||||
|
||||
if (attemptResult.ShouldRetry && attempt < SharedCompilerWorkerMaxAttempts)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DynamicCompilationHealthMonitor.ReportSharedWorkerFailure(
|
||||
attemptResult.FailureReason,
|
||||
AppendAttempt(attemptResult.FailureContext, attempt));
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static WorkerAttemptResult TryCompileOnce(
|
||||
string requestFilePath,
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
WorkerStartupResult startupResult = EnsureWorkerReady(externalCompilerPaths);
|
||||
if (!startupResult.IsReady)
|
||||
{
|
||||
return WorkerAttemptResult.RetryableFailure(
|
||||
startupResult.FailureReason,
|
||||
startupResult.FailureContext);
|
||||
}
|
||||
|
||||
return InvokeWorkerOnce(
|
||||
requestFilePath,
|
||||
ct,
|
||||
markBuildStarted,
|
||||
markBuildFinished,
|
||||
incrementBuildCount);
|
||||
}
|
||||
|
||||
private static WorkerStartupResult EnsureWorkerReady(ExternalCompilerPaths externalCompilerPaths)
|
||||
{
|
||||
if (HasLiveWorkerProcess())
|
||||
{
|
||||
return WorkerStartupResult.Ready();
|
||||
}
|
||||
|
||||
WorkerPaths workerPaths = CreateWorkerPaths();
|
||||
SynchronizeWorkerSource(workerPaths);
|
||||
|
||||
WorkerStartupResult workerAssemblyResult = EnsureWorkerAssemblyBuilt(
|
||||
externalCompilerPaths,
|
||||
workerPaths);
|
||||
if (!workerAssemblyResult.IsReady)
|
||||
{
|
||||
return workerAssemblyResult;
|
||||
}
|
||||
|
||||
return StartWorkerProcess(externalCompilerPaths, workerPaths);
|
||||
}
|
||||
|
||||
private static WorkerStartupResult EnsureWorkerAssemblyBuilt(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
WorkerPaths workerPaths)
|
||||
{
|
||||
if (File.Exists(workerPaths.AssemblyPath))
|
||||
{
|
||||
return WorkerStartupResult.Ready();
|
||||
}
|
||||
|
||||
WorkerAssemblyBuildResult buildResult = CompileWorkerAssembly(
|
||||
externalCompilerPaths,
|
||||
workerPaths.SourcePath,
|
||||
workerPaths.AssemblyPath,
|
||||
workerPaths.CompileResponseFilePath);
|
||||
if (!buildResult.StartedSuccessfully)
|
||||
{
|
||||
return WorkerStartupResult.Failure(
|
||||
buildResult.FailureReason,
|
||||
buildResult.FailureContext);
|
||||
}
|
||||
|
||||
if (!HasErrors(buildResult.Messages))
|
||||
{
|
||||
return WorkerStartupResult.Ready();
|
||||
}
|
||||
|
||||
DeleteWorkerAssemblyIfPresent(workerPaths.AssemblyPath);
|
||||
return WorkerStartupResult.Failure(
|
||||
"worker_build_failed",
|
||||
new
|
||||
{
|
||||
first_error = FindFirstErrorMessage(buildResult.Messages),
|
||||
worker_source_path = workerPaths.SourcePath
|
||||
});
|
||||
}
|
||||
|
||||
private static WorkerStartupResult StartWorkerProcess(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
WorkerPaths workerPaths)
|
||||
{
|
||||
ProcessStartInfo startInfo = CreateWorkerStartInfo(externalCompilerPaths, workerPaths);
|
||||
_sharedCompilerWorkerProcess = ProcessStartHelper.TryStart(startInfo);
|
||||
if (_sharedCompilerWorkerProcess == null)
|
||||
{
|
||||
return WorkerStartupResult.Failure(
|
||||
"worker_start_failed",
|
||||
new
|
||||
{
|
||||
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
||||
worker_assembly_path = workerPaths.AssemblyPath
|
||||
});
|
||||
}
|
||||
|
||||
return WorkerStartupResult.Ready();
|
||||
}
|
||||
|
||||
private static WorkerAttemptResult InvokeWorkerOnce(
|
||||
string requestFilePath,
|
||||
CancellationToken ct,
|
||||
Action markBuildStarted,
|
||||
Action markBuildFinished,
|
||||
Action incrementBuildCount)
|
||||
{
|
||||
if (!HasLiveWorkerProcess())
|
||||
{
|
||||
return WorkerAttemptResult.RetryableFailure(
|
||||
"worker_process_missing",
|
||||
new { request_file_path = requestFilePath });
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
incrementBuildCount();
|
||||
markBuildStarted();
|
||||
|
||||
try
|
||||
{
|
||||
SendCompileRequest(requestFilePath);
|
||||
return ReadWorkerResponse(requestFilePath, ct);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return CreateRetryableWorkerCommunicationFailure(requestFilePath, ex);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
return CreateRetryableWorkerCommunicationFailure(requestFilePath, ex);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
ShutdownWorkerProcessLocked();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
markBuildFinished();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendCompileRequest(string requestFilePath)
|
||||
{
|
||||
string absoluteRequestFilePath = Path.GetFullPath(requestFilePath);
|
||||
s_sendCompileRequest(_sharedCompilerWorkerProcess, absoluteRequestFilePath);
|
||||
}
|
||||
|
||||
private static void SendCompileRequestCore(Process workerProcess, string requestFilePath)
|
||||
{
|
||||
string requestCommand = CreateCompileRequestCommand(requestFilePath);
|
||||
workerProcess.StandardInput.WriteLine(requestCommand);
|
||||
workerProcess.StandardInput.Flush();
|
||||
}
|
||||
|
||||
private static string CreateCompileRequestCommand(string requestFilePath)
|
||||
{
|
||||
Debug.Assert(!string.IsNullOrEmpty(requestFilePath), "requestFilePath must not be empty");
|
||||
|
||||
string fullRequestFilePath = Path.GetFullPath(requestFilePath);
|
||||
byte[] requestPathBytes = Encoding.UTF8.GetBytes(fullRequestFilePath);
|
||||
return CompileRequestPathPrefix + Convert.ToBase64String(requestPathBytes);
|
||||
}
|
||||
|
||||
private static WorkerAttemptResult ReadWorkerResponse(
|
||||
string requestFilePath,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string responseHeader = ReadProtocolLine(
|
||||
_sharedCompilerWorkerProcess.StandardOutput,
|
||||
ct);
|
||||
if (string.IsNullOrEmpty(responseHeader))
|
||||
{
|
||||
return WorkerAttemptResult.RetryableFailure(
|
||||
"worker_empty_header",
|
||||
new { request_file_path = requestFilePath });
|
||||
}
|
||||
|
||||
if (!TryParseResponseHeader(responseHeader, out int exitCode))
|
||||
{
|
||||
return WorkerAttemptResult.RetryableFailure(
|
||||
GetResponseHeaderFailureReason(responseHeader),
|
||||
new { header = responseHeader });
|
||||
}
|
||||
|
||||
List<string> outputLines = ReadDiagnosticLines(ct);
|
||||
if (outputLines == null)
|
||||
{
|
||||
return WorkerAttemptResult.RetryableFailure(
|
||||
"worker_missing_end_marker",
|
||||
new { request_file_path = requestFilePath });
|
||||
}
|
||||
|
||||
string combinedOutput = string.Join("\n", outputLines);
|
||||
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(combinedOutput, string.Empty, exitCode);
|
||||
return WorkerAttemptResult.Successful(compilerMessages);
|
||||
}
|
||||
|
||||
private static bool TryParseResponseHeader(string responseHeader, out int exitCode)
|
||||
{
|
||||
exitCode = 0;
|
||||
|
||||
if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string statusText = responseHeader.Substring(SharedCompilerWorkerResultPrefix.Length).Trim();
|
||||
return int.TryParse(statusText, out exitCode);
|
||||
}
|
||||
|
||||
private static string GetResponseHeaderFailureReason(string responseHeader)
|
||||
{
|
||||
if (!responseHeader.StartsWith(SharedCompilerWorkerResultPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return "worker_invalid_header";
|
||||
}
|
||||
|
||||
return "worker_invalid_exit_code";
|
||||
}
|
||||
|
||||
private static List<string> ReadDiagnosticLines(CancellationToken ct)
|
||||
{
|
||||
List<string> outputLines = new List<string>();
|
||||
while (true)
|
||||
{
|
||||
string outputLine = ReadProtocolLine(_sharedCompilerWorkerProcess.StandardOutput, ct);
|
||||
if (outputLine == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (outputLine == SharedCompilerWorkerEndMarker)
|
||||
{
|
||||
return outputLines;
|
||||
}
|
||||
|
||||
outputLines.Add(outputLine);
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkerPaths CreateWorkerPaths()
|
||||
{
|
||||
string workerDirectoryPath = GetWorkerDirectoryPath();
|
||||
Directory.CreateDirectory(workerDirectoryPath);
|
||||
_workerDirectoryPath = workerDirectoryPath;
|
||||
return new WorkerPaths(
|
||||
workerDirectoryPath,
|
||||
Path.Combine(workerDirectoryPath, RoslynWorkerSourceFileName),
|
||||
Path.Combine(workerDirectoryPath, RoslynWorkerAssemblyFileName),
|
||||
Path.Combine(workerDirectoryPath, RoslynWorkerCompileResponseFileName));
|
||||
}
|
||||
|
||||
private static string GetWorkerDirectoryPath()
|
||||
{
|
||||
return Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"uLoopMCPCompilation",
|
||||
$"RoslynWorker-{Process.GetCurrentProcess().Id}");
|
||||
}
|
||||
|
||||
private static void SynchronizeWorkerSource(WorkerPaths workerPaths)
|
||||
{
|
||||
string workerSource = CreateProgramSource();
|
||||
if (File.Exists(workerPaths.SourcePath) && File.ReadAllText(workerPaths.SourcePath) == workerSource)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllText(workerPaths.SourcePath, workerSource);
|
||||
if (File.Exists(workerPaths.AssemblyPath))
|
||||
{
|
||||
File.Delete(workerPaths.AssemblyPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static ProcessStartInfo CreateWorkerStartInfo(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
WorkerPaths workerPaths)
|
||||
{
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = externalCompilerPaths.DotnetHostPath,
|
||||
Arguments = "exec"
|
||||
+ " --runtimeconfig " + QuoteCommandLineArgument(externalCompilerPaths.CompilerRuntimeConfigPath)
|
||||
+ " --depsfile " + QuoteCommandLineArgument(externalCompilerPaths.CompilerDepsFilePath)
|
||||
+ " " + QuoteCommandLineArgument(workerPaths.AssemblyPath),
|
||||
WorkingDirectory = workerPaths.DirectoryPath,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
|
||||
return startInfo;
|
||||
}
|
||||
|
||||
internal static void ConfigureWorkerDotnetRuntimeEnvironment(ProcessStartInfo startInfo)
|
||||
{
|
||||
Debug.Assert(startInfo != null, "startInfo must not be null");
|
||||
|
||||
// Why: global probing can select a system .NET 6 runtime while Unity 6000.4 worker
|
||||
// references come from the bundled .NET 8 runtime, which breaks assembly binding.
|
||||
startInfo.EnvironmentVariables[DotnetMultilevelLookupEnvironmentVariableName] =
|
||||
DotnetMultilevelLookupDisabledValue;
|
||||
}
|
||||
|
||||
private static WorkerAssemblyBuildResult CompileWorkerAssembly(
|
||||
ExternalCompilerPaths externalCompilerPaths,
|
||||
string workerSourcePath,
|
||||
string workerAssemblyPath,
|
||||
string workerCompileResponseFilePath)
|
||||
{
|
||||
if (s_compileWorkerAssemblyForTests != null)
|
||||
{
|
||||
return WorkerAssemblyBuildResult.Started(
|
||||
s_compileWorkerAssemblyForTests(
|
||||
externalCompilerPaths,
|
||||
workerSourcePath,
|
||||
workerAssemblyPath,
|
||||
workerCompileResponseFilePath));
|
||||
}
|
||||
|
||||
WriteWorkerCompilerResponseFile(
|
||||
workerCompileResponseFilePath,
|
||||
workerSourcePath,
|
||||
workerAssemblyPath,
|
||||
BuildWorkerReferenceSet(externalCompilerPaths));
|
||||
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = externalCompilerPaths.DotnetHostPath,
|
||||
Arguments = $"{QuoteCommandLineArgument(externalCompilerPaths.CompilerDllPath)} @{QuoteCommandLineArgument(workerCompileResponseFilePath)}",
|
||||
WorkingDirectory = Path.GetDirectoryName(workerSourcePath),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
ConfigureWorkerDotnetRuntimeEnvironment(startInfo);
|
||||
|
||||
using Process process = ProcessStartHelper.TryStart(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
return WorkerAssemblyBuildResult.StartFailure(
|
||||
"worker_compiler_start_failed",
|
||||
new
|
||||
{
|
||||
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
||||
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
||||
});
|
||||
}
|
||||
|
||||
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync();
|
||||
Task<string> stderrTask = process.StandardError.ReadToEndAsync();
|
||||
if (!process.WaitForExit(WorkerAssemblyBuildTimeoutMilliseconds))
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit(500);
|
||||
}
|
||||
|
||||
Task.WaitAll(stdoutTask, stderrTask);
|
||||
return WorkerAssemblyBuildResult.StartFailure(
|
||||
"worker_compiler_timeout",
|
||||
new
|
||||
{
|
||||
timeout_ms = WorkerAssemblyBuildTimeoutMilliseconds,
|
||||
dotnet_host_path = externalCompilerPaths.DotnetHostPath,
|
||||
compiler_dll_path = externalCompilerPaths.CompilerDllPath
|
||||
});
|
||||
}
|
||||
|
||||
Task.WaitAll(stdoutTask, stderrTask);
|
||||
CompilerMessage[] compilerMessages = ExternalCompilerMessageParser.Parse(
|
||||
stdoutTask.GetAwaiter().GetResult(),
|
||||
stderrTask.GetAwaiter().GetResult(),
|
||||
process.ExitCode);
|
||||
return WorkerAssemblyBuildResult.Started(compilerMessages);
|
||||
}
|
||||
|
||||
private static List<string> BuildWorkerReferenceSet(ExternalCompilerPaths externalCompilerPaths)
|
||||
{
|
||||
string sharedRuntimeDirectoryPath = externalCompilerPaths.NetCoreRuntimeSharedDirectoryPath;
|
||||
List<string> references = new List<string>
|
||||
{
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Private.CoreLib.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Console.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Collections.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.IO.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.Tasks.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Text.Encoding.Extensions.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.Extensions.dll"),
|
||||
Path.Combine(sharedRuntimeDirectoryPath, "netstandard.dll"),
|
||||
externalCompilerPaths.CodeAnalysisDllPath,
|
||||
externalCompilerPaths.CodeAnalysisCSharpDllPath
|
||||
};
|
||||
|
||||
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Collections.Immutable.dll"));
|
||||
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Reflection.Metadata.dll"));
|
||||
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Runtime.CompilerServices.Unsafe.dll"));
|
||||
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Memory.dll"));
|
||||
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Buffers.dll"));
|
||||
AddIfExists(references, Path.Combine(sharedRuntimeDirectoryPath, "System.Threading.Tasks.Extensions.dll"));
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private static void WriteWorkerCompilerResponseFile(
|
||||
string responseFilePath,
|
||||
string sourcePath,
|
||||
string dllPath,
|
||||
IReadOnlyCollection<string> references)
|
||||
{
|
||||
List<string> lines = new List<string>
|
||||
{
|
||||
"-nologo",
|
||||
"-nostdlib+",
|
||||
"-target:exe",
|
||||
"-optimize+",
|
||||
"-debug-",
|
||||
QuoteResponseFileArgument("-out:", dllPath)
|
||||
};
|
||||
|
||||
foreach (string reference in references)
|
||||
{
|
||||
lines.Add(QuoteResponseFileArgument("-r:", reference));
|
||||
}
|
||||
|
||||
lines.Add(QuoteResponseFilePath(sourcePath));
|
||||
File.WriteAllLines(responseFilePath, lines);
|
||||
}
|
||||
|
||||
private static string ReadProtocolLine(StreamReader reader, CancellationToken ct)
|
||||
{
|
||||
Debug.Assert(reader != null, "reader must not be null");
|
||||
|
||||
Task<string> readTask = Task.Run(() => reader.ReadLine());
|
||||
Task timeoutTask = Task.Delay(SharedCompilerWorkerResponseTimeoutMilliseconds, ct);
|
||||
Task completedTask = Task.WhenAny(readTask, timeoutTask).GetAwaiter().GetResult();
|
||||
if (!ReferenceEquals(completedTask, readTask))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
return null;
|
||||
}
|
||||
|
||||
return readTask.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static bool HasLiveWorkerProcess()
|
||||
{
|
||||
return _sharedCompilerWorkerProcess != null && !_sharedCompilerWorkerProcess.HasExited;
|
||||
}
|
||||
|
||||
private static bool HasErrors(IReadOnlyCollection<CompilerMessage> messages)
|
||||
{
|
||||
foreach (CompilerMessage message in messages)
|
||||
{
|
||||
if (message.type == CompilerMessageType.Error)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string FindFirstErrorMessage(IReadOnlyCollection<CompilerMessage> messages)
|
||||
{
|
||||
foreach (CompilerMessage message in messages)
|
||||
{
|
||||
if (message.type == CompilerMessageType.Error)
|
||||
{
|
||||
return message.message;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static void DeleteWorkerAssemblyIfPresent(string assemblyPath)
|
||||
{
|
||||
if (File.Exists(assemblyPath))
|
||||
{
|
||||
File.Delete(assemblyPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShutdownForReload()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
private static void ShutdownForQuit()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
internal static void ShutdownForServerReset()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
internal static void ShutdownForTests()
|
||||
{
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
internal static Action<string> SwapWorkerDirectoryDeleterForTests(Action<string> deleter)
|
||||
{
|
||||
Debug.Assert(deleter != null, "deleter must not be null");
|
||||
|
||||
Action<string> previous = s_deleteWorkerDirectory;
|
||||
s_deleteWorkerDirectory = deleter;
|
||||
return previous;
|
||||
}
|
||||
|
||||
internal static Action<Process, string> SwapCompileRequestSenderForTests(Action<Process, string> sender)
|
||||
{
|
||||
Debug.Assert(sender != null, "sender must not be null");
|
||||
|
||||
Action<Process, string> previous = s_sendCompileRequest;
|
||||
s_sendCompileRequest = sender;
|
||||
return previous;
|
||||
}
|
||||
|
||||
internal static Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]>
|
||||
SwapWorkerAssemblyCompilerForTests(
|
||||
Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]> compiler)
|
||||
{
|
||||
Func<ExternalCompilerPaths, string, string, string, CompilerMessage[]> previous =
|
||||
s_compileWorkerAssemblyForTests;
|
||||
s_compileWorkerAssemblyForTests = compiler;
|
||||
return previous;
|
||||
}
|
||||
|
||||
private static void Shutdown()
|
||||
{
|
||||
lock (SharedCompilerWorkerLock)
|
||||
{
|
||||
ShutdownWorkerProcessLocked();
|
||||
CleanupWorkerDirectoryLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShutdownWorkerProcessLocked()
|
||||
{
|
||||
Process workerProcess = _sharedCompilerWorkerProcess;
|
||||
_sharedCompilerWorkerProcess = null;
|
||||
if (workerProcess == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!workerProcess.HasExited)
|
||||
{
|
||||
workerProcess.StandardInput.WriteLine(SharedCompilerWorkerQuitCommand);
|
||||
workerProcess.StandardInput.Flush();
|
||||
workerProcess.WaitForExit(500);
|
||||
}
|
||||
|
||||
if (!workerProcess.HasExited)
|
||||
{
|
||||
workerProcess.Kill();
|
||||
workerProcess.WaitForExit(500);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
LogWorkerShutdownFailure(ex);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
LogWorkerShutdownFailure(ex);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
LogWorkerShutdownFailure(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
workerProcess.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void CleanupWorkerDirectoryLocked()
|
||||
{
|
||||
string workerDirectoryPath = _workerDirectoryPath ?? GetWorkerDirectoryPath();
|
||||
if (!Directory.Exists(workerDirectoryPath))
|
||||
{
|
||||
_workerDirectoryPath = null;
|
||||
return;
|
||||
}
|
||||
|
||||
TryDeleteWorkerDirectory(workerDirectoryPath);
|
||||
_workerDirectoryPath = null;
|
||||
}
|
||||
|
||||
private static void TryDeleteWorkerDirectory(string workerDirectoryPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
s_deleteWorkerDirectory(workerDirectoryPath);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
LogWorkerDirectoryCleanupFailure(workerDirectoryPath, ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
LogWorkerDirectoryCleanupFailure(workerDirectoryPath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void LogWorkerDirectoryCleanupFailure(string workerDirectoryPath, Exception ex)
|
||||
{
|
||||
VibeLogger.LogWarning(
|
||||
"dynamic_code_shared_worker_cleanup_failed",
|
||||
"execute-dynamic-code shared Roslyn worker directory cleanup failed during shutdown",
|
||||
new
|
||||
{
|
||||
worker_directory_path = workerDirectoryPath,
|
||||
exception_type = ex.GetType().FullName,
|
||||
exception_message = ex.Message
|
||||
},
|
||||
humanNote: "Shared Roslyn worker cleanup could not remove its temporary directory during shutdown.",
|
||||
aiTodo: "Investigate file locks or permission issues if temporary worker directories continue to accumulate.");
|
||||
Debug.LogWarning($"[{McpConstants.PROJECT_NAME}] Failed to delete shared Roslyn worker directory '{workerDirectoryPath}': {ex.Message}");
|
||||
}
|
||||
|
||||
private static WorkerAttemptResult CreateRetryableWorkerCommunicationFailure(
|
||||
string requestFilePath,
|
||||
Exception ex)
|
||||
{
|
||||
return WorkerAttemptResult.RetryableFailure(
|
||||
"worker_communication_failed",
|
||||
new
|
||||
{
|
||||
request_file_path = requestFilePath,
|
||||
exception_type = ex.GetType().FullName,
|
||||
exception_message = ex.Message
|
||||
});
|
||||
}
|
||||
|
||||
private static void LogWorkerShutdownFailure(Exception ex)
|
||||
{
|
||||
VibeLogger.LogWarning(
|
||||
"dynamic_code_shared_worker_shutdown_failed",
|
||||
"execute-dynamic-code shared Roslyn worker shutdown observed a communication failure",
|
||||
new
|
||||
{
|
||||
exception_type = ex.GetType().FullName,
|
||||
exception_message = ex.Message
|
||||
},
|
||||
humanNote: "Shared Roslyn worker shutdown saw a broken communication channel while cleaning up a crashed worker.",
|
||||
aiTodo: "Investigate repeated worker shutdown communication failures if shared compilation stops recovering cleanly.");
|
||||
}
|
||||
|
||||
private static object AppendAttempt(object failureContext, int attempt)
|
||||
{
|
||||
return new
|
||||
{
|
||||
attempt,
|
||||
details = failureContext
|
||||
};
|
||||
}
|
||||
|
||||
private static string QuoteResponseFileArgument(string prefix, string value)
|
||||
{
|
||||
return $"{prefix}{QuoteResponseFilePath(value)}";
|
||||
}
|
||||
|
||||
private static string QuoteResponseFilePath(string path)
|
||||
{
|
||||
return $"\"{path}\"";
|
||||
}
|
||||
|
||||
private static string QuoteCommandLineArgument(string value)
|
||||
{
|
||||
return $"\"{value}\"";
|
||||
}
|
||||
|
||||
private static void AddIfExists(
|
||||
List<string> destination,
|
||||
string referencePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(referencePath) || !File.Exists(referencePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destination.Add(referencePath);
|
||||
}
|
||||
|
||||
private static string CreateProgramSource()
|
||||
{
|
||||
string templatePath = GetWorkerProgramTemplatePath();
|
||||
string templateSource = File.ReadAllText(templatePath, Encoding.UTF8);
|
||||
return templateSource
|
||||
.Replace(SharedCompilerWorkerResultPrefixToken, SharedCompilerWorkerResultPrefix)
|
||||
.Replace(SharedCompilerWorkerEndMarkerToken, SharedCompilerWorkerEndMarker)
|
||||
.Replace(SharedCompilerWorkerQuitCommandToken, SharedCompilerWorkerQuitCommand)
|
||||
.Replace(CompileRequestPathPrefixToken, CompileRequestPathPrefix);
|
||||
}
|
||||
|
||||
private static string GetWorkerProgramTemplatePath()
|
||||
{
|
||||
return Path.Combine(McpConstants.PackageResolvedPath, RoslynWorkerProgramTemplateRelativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b64646e28ac744f7384180d34bf2e5c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Pre-compilation source-level security scanner.
|
||||
/// Blocks dangerous API patterns before code reaches the compiler,
|
||||
/// preventing actually harmful code from executing (e.g. Process.Kill crashing Unity).
|
||||
/// </summary>
|
||||
internal static class SourceSecurityScanner
|
||||
{
|
||||
private static readonly Regex StripCommentsAndStringsRegex = new Regex(
|
||||
@"
|
||||
//[^\n]* | # line comment
|
||||
/\*[\s\S]*?\*/ | # block comment
|
||||
@""(?:""""|[^""])*"" | # verbatim string
|
||||
""(?:\\.|[^\\""\n])*"" | # regular string
|
||||
'(?:\\.|[^\\'\n])*' # char literal
|
||||
",
|
||||
RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
|
||||
|
||||
private static readonly List<DangerousPattern> Patterns = new()
|
||||
{
|
||||
// Process manipulation
|
||||
new("Process", "Start", @"\bProcess\s*\.\s*Start\b"),
|
||||
new("Process", "Kill", @"\b(?:global::\s*)?(?:System\s*\.\s*Diagnostics\s*\.\s*)?Process\s*\.\s*Kill\b"),
|
||||
|
||||
// File system destructive operations
|
||||
new("File", "Delete", @"\bFile\s*\.\s*Delete\b"),
|
||||
new("File", "WriteAllText", @"\bFile\s*\.\s*WriteAllText\b"),
|
||||
new("File", "WriteAllBytes", @"\bFile\s*\.\s*WriteAllBytes\b"),
|
||||
new("File", "Replace", @"\bFile\s*\.\s*Replace\b"),
|
||||
new("File", "OpenWrite", @"\bFile\s*\.\s*OpenWrite\b"),
|
||||
new("File", "AppendAllText", @"\bFile\s*\.\s*AppendAllText\b"),
|
||||
new("Directory", "Delete", @"\bDirectory\s*\.\s*Delete\b"),
|
||||
|
||||
// Network
|
||||
new("HttpClient", "new", @"\bnew\s+HttpClient\b"),
|
||||
new("HttpClient", "usage", @"\bHttpClient\s*\("),
|
||||
new("WebClient", "new", @"\bnew\s+WebClient\b"),
|
||||
new("WebRequest", "Create", @"\bWebRequest\s*\.\s*Create\b"),
|
||||
new("Socket", "new", @"\bnew\s+Socket\b"),
|
||||
new("TcpClient", "new", @"\bnew\s+TcpClient\b|\bTcpClient\s*\("),
|
||||
new("UdpClient", "new", @"\bnew\s+UdpClient\b|\bUdpClient\s*\("),
|
||||
new("ClientWebSocket", "new",
|
||||
@"\bnew\s+(?:global::\s*)?(?:System\s*\.\s*Net\s*\.\s*WebSockets\s*\.\s*)?ClientWebSocket\b|\bClientWebSocket\s*\("),
|
||||
new("Dns", "GetHostEntry", @"\bDns\s*\.\s*GetHostEntry\b"),
|
||||
new("UnityWebRequest", "new", @"\bnew\s+UnityWebRequest\b"),
|
||||
|
||||
// Reflection execution
|
||||
new("Assembly", "Load", @"\bAssembly\s*\.\s*Load\b"),
|
||||
new("Assembly", "LoadFrom", @"\bAssembly\s*\.\s*LoadFrom\b"),
|
||||
new("Assembly", "GetType", @"\b(?:global::\s*)?(?:System\s*\.\s*Reflection\s*\.\s*)?Assembly\s*\.\s*GetType\s*\("),
|
||||
new("Type", "GetType", @"\b(?:global::\s*)?(?:System\s*\.\s*)?Type\s*\.\s*GetType\s*\("),
|
||||
new("Activator", "CreateComInstanceFrom", @"\bActivator\s*\.\s*CreateComInstanceFrom\b"),
|
||||
|
||||
// Environment
|
||||
new("Environment", "Exit", @"\bEnvironment\s*\.\s*Exit\b"),
|
||||
new("Environment", "FailFast", @"\bEnvironment\s*\.\s*FailFast\b"),
|
||||
|
||||
// GC manipulation
|
||||
new("GCSettings", "LatencyMode", @"\bGCSettings\s*\.\s*LatencyMode\s*="),
|
||||
|
||||
// Thread manipulation
|
||||
new("Thread", "Abort", @"\bThread\s*\.\s*Abort\s*\("),
|
||||
|
||||
// Asset deletion
|
||||
new("AssetDatabase", "DeleteAsset", @"\bAssetDatabase\s*\.\s*DeleteAsset\b"),
|
||||
new("FileUtil", "DeleteFileOrDirectory", @"\bFileUtil\s*\.\s*DeleteFileOrDirectory\b"),
|
||||
|
||||
// Unsafe code
|
||||
new("unsafe", "block", @"\bunsafe\s*\{|\bunsafe\s+\w"),
|
||||
new("DllImport", "attribute", @"\bDllImport\b"),
|
||||
new("extern", "declaration", @"\bextern\s+\w"),
|
||||
new("ModuleInitializer", "attribute", @"\bModuleInitializer\b"),
|
||||
};
|
||||
|
||||
public static SecurityValidationResult Scan(string sourceCode)
|
||||
{
|
||||
SecurityValidationResult result = new SecurityValidationResult
|
||||
{
|
||||
IsValid = true,
|
||||
Violations = new List<SecurityViolation>()
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceCode)) return result;
|
||||
|
||||
// Strip comments and string literals to avoid false positives
|
||||
string stripped = StripCommentsAndStrings(sourceCode);
|
||||
|
||||
foreach (DangerousPattern pattern in Patterns)
|
||||
{
|
||||
if (pattern.Regex.IsMatch(stripped))
|
||||
{
|
||||
result.Violations.Add(new SecurityViolation
|
||||
{
|
||||
Type = SecurityViolationType.DangerousApiCall,
|
||||
ApiName = $"{pattern.TypeName}.{pattern.MemberName}",
|
||||
Message = $"Dangerous API detected: {pattern.TypeName}.{pattern.MemberName}",
|
||||
Description = $"Source contains blocked pattern: {pattern.TypeName}.{pattern.MemberName}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result.IsValid = result.Violations.Count == 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string StripCommentsAndStrings(string source)
|
||||
{
|
||||
// Replace string literals and comments with spaces to avoid false positives
|
||||
// while preserving line structure for position tracking
|
||||
return StripCommentsAndStringsRegex.Replace(source, match => new string(' ', match.Length));
|
||||
}
|
||||
|
||||
private sealed class DangerousPattern
|
||||
{
|
||||
public string TypeName { get; }
|
||||
public string MemberName { get; }
|
||||
public Regex Regex { get; }
|
||||
|
||||
public DangerousPattern(string typeName, string memberName, string regexPattern)
|
||||
{
|
||||
TypeName = typeName;
|
||||
MemberName = memberName;
|
||||
Regex = new Regex(regexPattern, RegexOptions.Singleline | RegexOptions.Compiled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52aa81fdca60b4deab0d69d7e58014a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,591 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight C# source scanner that determines top-level structure without Roslyn.
|
||||
/// Tracks string literals, comments, and brace depth to classify source into one of three modes.
|
||||
/// </summary>
|
||||
internal static class SourceShaper
|
||||
{
|
||||
public static SourceShapeResult Analyze(string source)
|
||||
{
|
||||
Debug.Assert(source != null, "source must not be null");
|
||||
|
||||
SourceShapeResult result = new SourceShapeResult();
|
||||
int length = source.Length;
|
||||
int pos = 0;
|
||||
int braceDepth = 0;
|
||||
|
||||
while (pos < length)
|
||||
{
|
||||
pos = SkipWhitespace(source, pos);
|
||||
if (pos >= length) break;
|
||||
|
||||
if (braceDepth == 0)
|
||||
{
|
||||
if (TryMatchLineComment(source, pos, out int afterComment))
|
||||
{
|
||||
pos = afterComment;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryMatchBlockComment(source, pos, out int afterBlock))
|
||||
{
|
||||
pos = afterBlock;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StartsWithKeyword(source, pos, "using"))
|
||||
{
|
||||
int segmentStart = pos;
|
||||
int afterUsing = pos + 5;
|
||||
afterUsing = SkipWhitespace(source, afterUsing);
|
||||
|
||||
if (StartsWithKeyword(source, afterUsing, "static"))
|
||||
{
|
||||
int end = FindSemicolon(source, segmentStart);
|
||||
result.UsingDirectives.Add(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd());
|
||||
pos = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// "using var" and "using (" are using-statements, not using-directives
|
||||
if (afterUsing < length && (StartsWithKeyword(source, afterUsing, "var") || source[afterUsing] == '('))
|
||||
{
|
||||
result.HasTopLevelStatements = true;
|
||||
int end = FindStatementEnd(source, segmentStart, ref braceDepth);
|
||||
result.TopLevelBodyBuilder.AppendLine(source.Substring(segmentStart, end - segmentStart + 1).TrimEnd());
|
||||
pos = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int semiEnd = FindSemicolon(source, segmentStart);
|
||||
result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd());
|
||||
pos = semiEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StartsWithKeyword(source, pos, "namespace"))
|
||||
{
|
||||
result.HasNamespaceDeclaration = true;
|
||||
pos = SkipBlock(source, pos, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsTypeDeclarationKeyword(source, pos))
|
||||
{
|
||||
result.HasTypeDeclaration = true;
|
||||
pos = SkipBlock(source, pos, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StartsWithKeyword(source, pos, "global") && StartsWithKeyword(source, SkipWhitespace(source, pos + 6), "using"))
|
||||
{
|
||||
int segmentStart = pos;
|
||||
int semiEnd = FindSemicolon(source, segmentStart);
|
||||
result.UsingDirectives.Add(source.Substring(segmentStart, semiEnd - segmentStart + 1).TrimEnd());
|
||||
pos = semiEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pos < length && source[pos] == '[')
|
||||
{
|
||||
int afterAttr = SkipAttributeBlock(source, pos);
|
||||
int nextNonWs = SkipWhitespace(source, afterAttr);
|
||||
if (nextNonWs < length && IsTypeDeclarationKeyword(source, nextNonWs))
|
||||
{
|
||||
result.HasTypeDeclaration = true;
|
||||
pos = SkipBlock(source, nextNonWs, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsAccessModifier(source, pos))
|
||||
{
|
||||
int afterMod = SkipAccessModifiers(source, pos);
|
||||
if (IsTypeDeclarationKeyword(source, afterMod))
|
||||
{
|
||||
result.HasTypeDeclaration = true;
|
||||
pos = SkipBlock(source, afterMod, ref braceDepth);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.HasTopLevelStatements = true;
|
||||
int stmtStart = pos;
|
||||
int stmtEnd = FindStatementEnd(source, stmtStart, ref braceDepth);
|
||||
result.TopLevelBodyBuilder.AppendLine(source.Substring(stmtStart, stmtEnd - stmtStart + 1).TrimEnd());
|
||||
pos = stmtEnd + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos = AdvanceInsideBlock(source, pos, ref braceDepth);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string WrapIfNeeded(string source, string namespaceName, string className)
|
||||
{
|
||||
SourceShapeResult shape = Analyze(source);
|
||||
|
||||
// Raw mode: namespace or type declaration without top-level statements → pass through
|
||||
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && !shape.HasTopLevelStatements)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Mixed mode: both type declarations and top-level statements → error
|
||||
if ((shape.HasNamespaceDeclaration || shape.HasTypeDeclaration) && shape.HasTopLevelStatements)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Script mode: wrap top-level statements
|
||||
string body = shape.TopLevelBodyBuilder.ToString().TrimEnd();
|
||||
|
||||
bool hasReturn = TopLevelReturnDetector.HasTopLevelReturn(body);
|
||||
if (!hasReturn)
|
||||
{
|
||||
body = string.IsNullOrWhiteSpace(body)
|
||||
? "return null;"
|
||||
: body + "\nreturn null;";
|
||||
}
|
||||
|
||||
return WrapperTemplate.Build(shape.UsingDirectives, namespaceName, className, body);
|
||||
}
|
||||
|
||||
internal static int SkipWhitespace(string s, int pos)
|
||||
{
|
||||
Debug.Assert(s != null, "s must not be null");
|
||||
Debug.Assert(pos >= 0, "pos must be non-negative");
|
||||
while (pos < s.Length && char.IsWhiteSpace(s[pos])) pos++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
internal static bool StartsWithKeyword(string s, int pos, string keyword)
|
||||
{
|
||||
Debug.Assert(s != null, "s must not be null");
|
||||
Debug.Assert(keyword != null, "keyword must not be null");
|
||||
Debug.Assert(pos >= 0, "pos must be non-negative");
|
||||
if (pos + keyword.Length > s.Length) return false;
|
||||
for (int i = 0; i < keyword.Length; i++)
|
||||
{
|
||||
if (s[pos + i] != keyword[i]) return false;
|
||||
}
|
||||
// Keyword must be followed by non-identifier char
|
||||
int afterPos = pos + keyword.Length;
|
||||
if (afterPos < s.Length && (char.IsLetterOrDigit(s[afterPos]) || s[afterPos] == '_'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTypeDeclarationKeyword(string s, int pos)
|
||||
{
|
||||
return StartsWithKeyword(s, pos, "class") ||
|
||||
StartsWithKeyword(s, pos, "struct") ||
|
||||
StartsWithKeyword(s, pos, "interface") ||
|
||||
StartsWithKeyword(s, pos, "enum") ||
|
||||
StartsWithKeyword(s, pos, "record");
|
||||
}
|
||||
|
||||
private static bool IsAccessModifier(string s, int pos)
|
||||
{
|
||||
return StartsWithKeyword(s, pos, "public") ||
|
||||
StartsWithKeyword(s, pos, "internal") ||
|
||||
StartsWithKeyword(s, pos, "private") ||
|
||||
StartsWithKeyword(s, pos, "protected") ||
|
||||
StartsWithKeyword(s, pos, "static") ||
|
||||
StartsWithKeyword(s, pos, "sealed") ||
|
||||
StartsWithKeyword(s, pos, "abstract") ||
|
||||
StartsWithKeyword(s, pos, "partial");
|
||||
}
|
||||
|
||||
private static int SkipAccessModifiers(string s, int pos)
|
||||
{
|
||||
while (pos < s.Length && IsAccessModifier(s, pos))
|
||||
{
|
||||
int wordEnd = pos;
|
||||
while (wordEnd < s.Length && (char.IsLetterOrDigit(s[wordEnd]) || s[wordEnd] == '_')) wordEnd++;
|
||||
pos = SkipWhitespace(s, wordEnd);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static bool TryMatchLineComment(string s, int pos, out int afterComment)
|
||||
{
|
||||
afterComment = pos;
|
||||
if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '/')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end < s.Length && s[end] != '\n') end++;
|
||||
if (end < s.Length) end++; // skip \n
|
||||
afterComment = end;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryMatchBlockComment(string s, int pos, out int afterBlock)
|
||||
{
|
||||
afterBlock = pos;
|
||||
if (pos + 1 < s.Length && s[pos] == '/' && s[pos + 1] == '*')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
|
||||
afterBlock = end + 2 < s.Length ? end + 2 : s.Length;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int FindSemicolon(string s, int pos)
|
||||
{
|
||||
while (pos < s.Length)
|
||||
{
|
||||
if (s[pos] == ';') return pos;
|
||||
pos = AdvanceOneToken(s, pos);
|
||||
}
|
||||
return s.Length - 1;
|
||||
}
|
||||
|
||||
private static int FindStatementEnd(string s, int pos, ref int braceDepth)
|
||||
{
|
||||
while (pos < s.Length)
|
||||
{
|
||||
char c = s[pos];
|
||||
if (c == '{')
|
||||
{
|
||||
braceDepth++;
|
||||
pos++;
|
||||
while (pos < s.Length && braceDepth > 0)
|
||||
{
|
||||
pos = AdvanceInsideBlock(s, pos, ref braceDepth);
|
||||
}
|
||||
return pos - 1;
|
||||
}
|
||||
if (c == ';') return pos;
|
||||
pos = AdvanceOneToken(s, pos);
|
||||
}
|
||||
return s.Length - 1;
|
||||
}
|
||||
|
||||
private static int SkipBlock(string s, int pos, ref int braceDepth)
|
||||
{
|
||||
// Advance past keywords until we hit the opening brace
|
||||
while (pos < s.Length && s[pos] != '{')
|
||||
{
|
||||
if (s[pos] == ';') return pos + 1; // forward declaration
|
||||
pos = AdvanceOneToken(s, pos);
|
||||
}
|
||||
if (pos < s.Length && s[pos] == '{')
|
||||
{
|
||||
braceDepth++;
|
||||
pos++;
|
||||
while (pos < s.Length && braceDepth > 0)
|
||||
{
|
||||
pos = AdvanceInsideBlock(s, pos, ref braceDepth);
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static int SkipAttributeBlock(string s, int pos)
|
||||
{
|
||||
Debug.Assert(s[pos] == '[', "SkipAttributeBlock must start at '['");
|
||||
int depth = 1;
|
||||
pos++;
|
||||
while (pos < s.Length && depth > 0)
|
||||
{
|
||||
if (s[pos] == '[') depth++;
|
||||
else if (s[pos] == ']') depth--;
|
||||
else pos = AdvanceOneToken(s, pos) - 1; // -1 because loop will pos++ via fallthrough
|
||||
pos++;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static int AdvanceInsideBlock(string s, int pos, ref int braceDepth)
|
||||
{
|
||||
char c = s[pos];
|
||||
if (c == '{') { braceDepth++; return pos + 1; }
|
||||
if (c == '}') { braceDepth--; return pos + 1; }
|
||||
return AdvanceOneToken(s, pos);
|
||||
}
|
||||
|
||||
internal static int AdvanceOneTokenPublic(string s, int pos)
|
||||
{
|
||||
return AdvanceOneToken(s, pos);
|
||||
}
|
||||
|
||||
private static int AdvanceOneToken(string s, int pos)
|
||||
{
|
||||
if (pos >= s.Length) return s.Length;
|
||||
char c = s[pos];
|
||||
|
||||
// Line comment
|
||||
if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '/')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end < s.Length && s[end] != '\n') end++;
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
// Block comment
|
||||
if (c == '/' && pos + 1 < s.Length && s[pos + 1] == '*')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end + 1 < s.Length && !(s[end] == '*' && s[end + 1] == '/')) end++;
|
||||
return end + 2 < s.Length ? end + 2 : s.Length;
|
||||
}
|
||||
|
||||
// Verbatim string (@"...")
|
||||
if (c == '@' && pos + 1 < s.Length && s[pos + 1] == '"')
|
||||
{
|
||||
int end = pos + 2;
|
||||
while (end < s.Length)
|
||||
{
|
||||
if (s[end] == '"')
|
||||
{
|
||||
if (end + 1 < s.Length && s[end + 1] == '"') { end += 2; continue; }
|
||||
return end + 1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
// Raw string literal (""" ... """)
|
||||
if (c == '"' && pos + 2 < s.Length && s[pos + 1] == '"' && s[pos + 2] == '"')
|
||||
{
|
||||
int end = pos + 3;
|
||||
while (end + 2 < s.Length)
|
||||
{
|
||||
if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"') return end + 3;
|
||||
end++;
|
||||
}
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
// Regular string literal ("...")
|
||||
if (c == '"')
|
||||
{
|
||||
int end = pos + 1;
|
||||
while (end < s.Length && s[end] != '"')
|
||||
{
|
||||
if (s[end] == '\\') end++; // skip escaped char
|
||||
end++;
|
||||
}
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
// Char literal ('x')
|
||||
if (c == '\'')
|
||||
{
|
||||
int end = pos + 1;
|
||||
while (end < s.Length && s[end] != '\'')
|
||||
{
|
||||
if (s[end] == '\\') end++;
|
||||
end++;
|
||||
}
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
// Interpolated string ($"...")
|
||||
if (c == '$' && pos + 1 < s.Length && s[pos + 1] == '"')
|
||||
{
|
||||
return SkipInterpolatedString(s, pos + 2);
|
||||
}
|
||||
|
||||
return pos + 1;
|
||||
}
|
||||
|
||||
private static int SkipInterpolatedString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length)
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '{')
|
||||
{
|
||||
if (end + 1 < s.Length && s[end + 1] == '{')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
end = SkipInterpolationHole(s, end + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"' && end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"')
|
||||
{
|
||||
return end + 1;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
private static int SkipInterpolationHole(string s, int pos)
|
||||
{
|
||||
int depth = 1;
|
||||
int end = pos;
|
||||
|
||||
while (end < s.Length && depth > 0)
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '@' && end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end = SkipVerbatimString(s, end + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '$' && end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end = SkipInterpolatedString(s, end + 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"' && end + 2 < s.Length && s[end + 1] == '"' && s[end + 2] == '"')
|
||||
{
|
||||
end = SkipRawString(s, end + 3);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '"')
|
||||
{
|
||||
end = SkipRegularString(s, end + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '\'')
|
||||
{
|
||||
end = SkipCharLiteral(s, end + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '{')
|
||||
{
|
||||
depth++;
|
||||
end++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (s[end] == '}')
|
||||
{
|
||||
depth--;
|
||||
end++;
|
||||
continue;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
private static int SkipRegularString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length && s[end] != '"')
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end++;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
|
||||
private static int SkipVerbatimString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length)
|
||||
{
|
||||
if (s[end] == '"')
|
||||
{
|
||||
if (end + 1 < s.Length && s[end + 1] == '"')
|
||||
{
|
||||
end += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
return end + 1;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
private static int SkipRawString(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end + 2 < s.Length)
|
||||
{
|
||||
if (s[end] == '"' && s[end + 1] == '"' && s[end + 2] == '"')
|
||||
{
|
||||
return end + 3;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return s.Length;
|
||||
}
|
||||
|
||||
private static int SkipCharLiteral(string s, int pos)
|
||||
{
|
||||
int end = pos;
|
||||
while (end < s.Length && s[end] != '\'')
|
||||
{
|
||||
if (s[end] == '\\')
|
||||
{
|
||||
end++;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
|
||||
return end < s.Length ? end + 1 : s.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SourceShapeResult
|
||||
{
|
||||
public List<string> UsingDirectives { get; } = new List<string>();
|
||||
public bool HasNamespaceDeclaration { get; set; }
|
||||
public bool HasTypeDeclaration { get; set; }
|
||||
public bool HasTopLevelStatements { get; set; }
|
||||
public StringBuilder TopLevelBodyBuilder { get; } = new StringBuilder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 660677c36e0474b3fb33948f49201a04
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae70dd3da0e7d5745b800c341d218e0d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,322 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
private const string ResultPrefix = "{{SHARED_COMPILER_WORKER_RESULT_PREFIX}}";
|
||||
private const string EndMarker = "{{SHARED_COMPILER_WORKER_END_MARKER}}";
|
||||
private const string QuitCommand = "{{SHARED_COMPILER_WORKER_QUIT_COMMAND}}";
|
||||
private const string RequestPathPrefix = "{{COMPILE_REQUEST_PATH_PREFIX}}";
|
||||
private const string UnsafePrefix = "unsafe:";
|
||||
private const string DefinePrefix = "define:";
|
||||
private const string ReferencePrefix = "ref:";
|
||||
private static readonly object SyncRoot = new object();
|
||||
private static readonly Dictionary<string, CachedReference> Cache = new Dictionary<string, CachedReference>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static int Main()
|
||||
{
|
||||
string requestPath;
|
||||
while ((requestPath = Console.ReadLine()) != null)
|
||||
{
|
||||
if (requestPath == QuitCommand)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
CompileResponse response = Compile(DecodeRequestPath(requestPath));
|
||||
Console.WriteLine(ResultPrefix + " " + response.ExitCode);
|
||||
foreach (string diagnosticLine in response.DiagnosticLines)
|
||||
{
|
||||
Console.WriteLine(diagnosticLine);
|
||||
}
|
||||
Console.WriteLine(EndMarker);
|
||||
Console.Out.Flush();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string DecodeRequestPath(string requestPath)
|
||||
{
|
||||
int encodedPathIndex = FindRequestPathPrefixIndex(requestPath);
|
||||
if (encodedPathIndex >= 0)
|
||||
{
|
||||
string encodedPath = requestPath.Substring(encodedPathIndex + RequestPathPrefix.Length);
|
||||
if (!IsBase64Payload(encodedPath))
|
||||
{
|
||||
return RecoverRawRequestPath(requestPath);
|
||||
}
|
||||
|
||||
byte[] requestPathBytes = Convert.FromBase64String(encodedPath);
|
||||
return Encoding.UTF8.GetString(requestPathBytes);
|
||||
}
|
||||
|
||||
return RecoverRawRequestPath(requestPath);
|
||||
}
|
||||
|
||||
private static bool IsBase64Payload(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value) || value.Length % 4 != 0 || !HasValidBase64Padding(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
char character = value[i];
|
||||
bool isBase64Character = character >= 'A' && character <= 'Z'
|
||||
|| character >= 'a' && character <= 'z'
|
||||
|| character >= '0' && character <= '9'
|
||||
|| character == '+'
|
||||
|| character == '/'
|
||||
|| character == '=';
|
||||
if (!isBase64Character)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasValidBase64Padding(string value)
|
||||
{
|
||||
int firstPaddingIndex = value.IndexOf('=');
|
||||
if (firstPaddingIndex < 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int paddingLength = value.Length - firstPaddingIndex;
|
||||
if (paddingLength > 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = firstPaddingIndex; i < value.Length; i++)
|
||||
{
|
||||
if (value[i] != '=')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindRequestPathPrefixIndex(string requestPath)
|
||||
{
|
||||
int encodedPathIndex = requestPath.IndexOf(RequestPathPrefix, StringComparison.Ordinal);
|
||||
if (encodedPathIndex <= 0)
|
||||
{
|
||||
return encodedPathIndex;
|
||||
}
|
||||
|
||||
return HasDirectorySeparatorBeforePrefix(requestPath, encodedPathIndex) ? -1 : encodedPathIndex;
|
||||
}
|
||||
|
||||
private static bool HasDirectorySeparatorBeforePrefix(string requestPath, int prefixIndex)
|
||||
{
|
||||
for (int i = 0; i < prefixIndex; i++)
|
||||
{
|
||||
if (requestPath[i] == '\\' || requestPath[i] == '/')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string RecoverRawRequestPath(string requestPath)
|
||||
{
|
||||
string trimmedPath = requestPath.TrimStart('\uFEFF', '\uFFFD');
|
||||
int drivePathIndex = FindWindowsDrivePathIndex(trimmedPath);
|
||||
return drivePathIndex > 0 ? trimmedPath.Substring(drivePathIndex) : trimmedPath;
|
||||
}
|
||||
|
||||
private static int FindWindowsDrivePathIndex(string value)
|
||||
{
|
||||
for (int i = 0; i <= value.Length - 3; i++)
|
||||
{
|
||||
bool isDriveLetter = (value[i] >= 'A' && value[i] <= 'Z') || (value[i] >= 'a' && value[i] <= 'z');
|
||||
if (isDriveLetter && value[i + 1] == ':' && (value[i + 2] == '\\' || value[i + 2] == '/'))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static CompileResponse Compile(string requestPath)
|
||||
{
|
||||
string[] requestLines = File.ReadAllLines(requestPath);
|
||||
string sourcePath = requestLines[0];
|
||||
string dllPath = requestLines[1];
|
||||
bool allowUnsafe = ParseAllowUnsafe(requestLines);
|
||||
string[] defineSymbols = ParseDefineSymbols(requestLines);
|
||||
string source = File.ReadAllText(sourcePath, Encoding.UTF8);
|
||||
SourceText sourceText = SourceText.From(source, Encoding.UTF8);
|
||||
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceText, CreateParseOptions(defineSymbols), sourcePath);
|
||||
List<MetadataReference> references = BuildReferences(requestLines);
|
||||
CSharpCompilation compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(dllPath), new[] { syntaxTree }, references, CreateCompilationOptions(allowUnsafe));
|
||||
using (FileStream peStream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
EmitResult emitResult = compilation.Emit(peStream);
|
||||
int exitCode = 0;
|
||||
List<string> diagnosticLines = new List<string>();
|
||||
foreach (Diagnostic diagnostic in emitResult.Diagnostics)
|
||||
{
|
||||
if (diagnostic.Severity != DiagnosticSeverity.Error && diagnostic.Severity != DiagnosticSeverity.Warning)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
{
|
||||
exitCode = 1;
|
||||
}
|
||||
|
||||
FileLinePositionSpan lineSpan = diagnostic.Location != Location.None ? diagnostic.Location.GetMappedLineSpan() : default(FileLinePositionSpan);
|
||||
int line = lineSpan.StartLinePosition.Line + 1;
|
||||
int column = lineSpan.StartLinePosition.Character + 1;
|
||||
string file = string.IsNullOrEmpty(lineSpan.Path) ? sourcePath : lineSpan.Path;
|
||||
string severity = diagnostic.Severity == DiagnosticSeverity.Warning ? "warning" : "error";
|
||||
diagnosticLines.Add(file + "(" + line + "," + column + "): " + severity + " " + diagnostic.Id + ": " + diagnostic.GetMessage());
|
||||
}
|
||||
|
||||
return new CompileResponse(exitCode, diagnosticLines);
|
||||
}
|
||||
}
|
||||
|
||||
private static CSharpParseOptions CreateParseOptions(string[] defineSymbols)
|
||||
{
|
||||
return defineSymbols == null || defineSymbols.Length == 0
|
||||
? CSharpParseOptions.Default
|
||||
: CSharpParseOptions.Default.WithPreprocessorSymbols(defineSymbols);
|
||||
}
|
||||
|
||||
private static CSharpCompilationOptions CreateCompilationOptions(bool allowUnsafe)
|
||||
{
|
||||
return new CSharpCompilationOptions(
|
||||
OutputKind.DynamicallyLinkedLibrary,
|
||||
optimizationLevel: OptimizationLevel.Release,
|
||||
allowUnsafe: allowUnsafe);
|
||||
}
|
||||
|
||||
private static bool ParseAllowUnsafe(string[] requestLines)
|
||||
{
|
||||
for (int i = 2; i < requestLines.Length; i++)
|
||||
{
|
||||
string line = requestLines[i];
|
||||
if (!line.StartsWith(UnsafePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return string.Equals(line.Substring(UnsafePrefix.Length), "1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string[] ParseDefineSymbols(string[] requestLines)
|
||||
{
|
||||
for (int i = 2; i < requestLines.Length; i++)
|
||||
{
|
||||
string line = requestLines[i];
|
||||
if (!line.StartsWith(DefinePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string serializedSymbols = line.Substring(DefinePrefix.Length);
|
||||
if (string.IsNullOrWhiteSpace(serializedSymbols))
|
||||
{
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
return serializedSymbols.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
private static List<MetadataReference> BuildReferences(string[] requestLines)
|
||||
{
|
||||
List<MetadataReference> references = new List<MetadataReference>(Math.Max(0, requestLines.Length - 2));
|
||||
lock (SyncRoot)
|
||||
{
|
||||
for (int i = 2; i < requestLines.Length; i++)
|
||||
{
|
||||
string referencePath = requestLines[i];
|
||||
if (referencePath.StartsWith(UnsafePrefix, StringComparison.Ordinal)
|
||||
|| referencePath.StartsWith(DefinePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (referencePath.StartsWith(ReferencePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
referencePath = referencePath.Substring(ReferencePrefix.Length);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(referencePath) || !File.Exists(referencePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FileInfo info = new FileInfo(referencePath);
|
||||
long writeTicks = info.LastWriteTimeUtc.Ticks;
|
||||
long fileLength = info.Length;
|
||||
CachedReference cachedReference;
|
||||
if (Cache.TryGetValue(referencePath, out cachedReference)
|
||||
&& cachedReference.WriteTicks == writeTicks
|
||||
&& cachedReference.FileLength == fileLength)
|
||||
{
|
||||
references.Add(cachedReference.Reference);
|
||||
continue;
|
||||
}
|
||||
|
||||
PortableExecutableReference createdReference = MetadataReference.CreateFromFile(referencePath);
|
||||
Cache[referencePath] = new CachedReference(createdReference, writeTicks, fileLength);
|
||||
references.Add(createdReference);
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private sealed class CachedReference
|
||||
{
|
||||
public CachedReference(PortableExecutableReference reference, long writeTicks, long fileLength)
|
||||
{
|
||||
Reference = reference;
|
||||
WriteTicks = writeTicks;
|
||||
FileLength = fileLength;
|
||||
}
|
||||
|
||||
public PortableExecutableReference Reference { get; }
|
||||
public long WriteTicks { get; }
|
||||
public long FileLength { get; }
|
||||
}
|
||||
|
||||
private sealed class CompileResponse
|
||||
{
|
||||
public CompileResponse(int exitCode, List<string> diagnosticLines)
|
||||
{
|
||||
ExitCode = exitCode;
|
||||
DiagnosticLines = diagnosticLines;
|
||||
}
|
||||
|
||||
public int ExitCode { get; }
|
||||
public List<string> DiagnosticLines { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0645dfa092114f4e87246f7d1da5e0c
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects whether top-level code contains a return statement.
|
||||
/// Excludes returns inside lambdas and local functions by tracking brace depth.
|
||||
/// Delegates literal/comment skipping to SourceShaper.AdvanceOneToken to avoid duplication.
|
||||
/// </summary>
|
||||
internal static class TopLevelReturnDetector
|
||||
{
|
||||
public static bool HasTopLevelReturn(string body)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body)) return false;
|
||||
|
||||
int length = body.Length;
|
||||
int pos = 0;
|
||||
int braceDepth = 0;
|
||||
|
||||
while (pos < length)
|
||||
{
|
||||
char c = body[pos];
|
||||
|
||||
if (c == '{') { braceDepth++; pos++; continue; }
|
||||
if (c == '}') { braceDepth--; pos++; continue; }
|
||||
|
||||
if (braceDepth == 0 && c == 'r' && StartsWithReturn(body, pos, length))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Delegate all literal/comment skipping to shared scanner
|
||||
int next = SourceShaper.AdvanceOneTokenPublic(body, pos);
|
||||
if (next == pos) { pos++; continue; }
|
||||
pos = next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool StartsWithReturn(string s, int pos, int length)
|
||||
{
|
||||
// "return" must not be part of a longer identifier (e.g. "myreturn", "returnValue")
|
||||
if (pos > 0 && (char.IsLetterOrDigit(s[pos - 1]) || s[pos - 1] == '_'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (pos + 6 > length) return false;
|
||||
if (s[pos + 1] != 'e' || s[pos + 2] != 't' || s[pos + 3] != 'u' ||
|
||||
s[pos + 4] != 'r' || s[pos + 5] != 'n')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int afterReturn = pos + 6;
|
||||
if (afterReturn < length && (char.IsLetterOrDigit(s[afterReturn]) || s[afterReturn] == '_'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45f8daddb6cae422d9c819746fbf1e46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the namespace/class/method wrapper for top-level user code.
|
||||
/// Method signatures are compatible with CommandRunner's FindExecuteAsyncMethod.
|
||||
/// </summary>
|
||||
internal static class WrapperTemplate
|
||||
{
|
||||
internal const string UserCodeStartMarker = "#line 1 \"user-snippet.cs\"";
|
||||
internal const string UserCodeEndMarker = "#line default";
|
||||
|
||||
public static string Build(
|
||||
IReadOnlyList<string> usingDirectives,
|
||||
string namespaceName,
|
||||
string className,
|
||||
string body,
|
||||
IReadOnlyList<string> preambleLines = null)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("#pragma warning disable CS0162");
|
||||
sb.AppendLine("#pragma warning disable CS1998");
|
||||
sb.AppendLine("using System;");
|
||||
sb.AppendLine("using System.Linq;");
|
||||
sb.AppendLine("using System.Collections.Generic;");
|
||||
sb.AppendLine("using System.Threading;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using UnityEngine;");
|
||||
sb.AppendLine("using UnityEditor;");
|
||||
|
||||
foreach (string directive in usingDirectives)
|
||||
{
|
||||
sb.AppendLine(directive);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"namespace {namespaceName}");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine($" public class {className}");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" public async System.Threading.Tasks.Task<object> ExecuteAsync(");
|
||||
sb.AppendLine(" System.Collections.Generic.Dictionary<string, object> parameters = null,");
|
||||
sb.AppendLine(" System.Threading.CancellationToken ct = default)");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
if (preambleLines != null)
|
||||
{
|
||||
foreach (string preambleLine in preambleLines)
|
||||
{
|
||||
sb.AppendLine($" {preambleLine}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(UserCodeStartMarker);
|
||||
|
||||
foreach (string line in body.Split('\n'))
|
||||
{
|
||||
sb.AppendLine($" {line.TrimEnd('\r')}");
|
||||
}
|
||||
|
||||
sb.AppendLine(UserCodeEndMarker);
|
||||
sb.AppendLine("#line hidden");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" public object Execute(");
|
||||
sb.AppendLine(" System.Collections.Generic.Dictionary<string, object> parameters = null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" return ExecuteAsync(parameters, default).GetAwaiter().GetResult();");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine("}");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0e9c4bb3166948f7a6879b104383c33
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user