Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Compilation/DynamicCodeCompiler.cs

320 lines
14 KiB
C#

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");
}
}
}