710 lines
31 KiB
C#
710 lines
31 KiB
C#
using System;
|
|
using System.CodeDom.Compiler;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using MCPForUnity.Editor.Helpers;
|
|
using MCPForUnity.Runtime.Helpers;
|
|
using Microsoft.CSharp;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEngine;
|
|
|
|
namespace MCPForUnity.Editor.Tools
|
|
{
|
|
[McpForUnityTool("execute_code", AutoRegister = false, Group = "scripting_ext")]
|
|
public static class ExecuteCode
|
|
{
|
|
private const int MaxCodeLength = 50000;
|
|
private const int MaxHistoryEntries = 50;
|
|
private const int MaxHistoryCodePreview = 500;
|
|
internal const int WrapperLineOffset = 10;
|
|
private const string WrapperClassName = "MCPDynamicCode";
|
|
private const string WrapperMethodName = "Execute";
|
|
|
|
private const string ActionExecute = "execute";
|
|
private const string ActionGetHistory = "get_history";
|
|
private const string ActionClearHistory = "clear_history";
|
|
private const string ActionReplay = "replay";
|
|
|
|
private static readonly List<HistoryEntry> _history = new List<HistoryEntry>();
|
|
private static string[] _cachedAssemblyPaths;
|
|
|
|
[UnityEditor.InitializeOnLoadMethod]
|
|
private static void OnDomainReload()
|
|
{
|
|
_cachedAssemblyPaths = null;
|
|
RoslynCompiler.ResetCache();
|
|
}
|
|
|
|
private static readonly HashSet<string> _blockedPatterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"System.IO.File.Delete",
|
|
"System.IO.Directory.Delete",
|
|
"FileUtil.DeleteFileOrDirectory",
|
|
"AssetDatabase.DeleteAsset",
|
|
"AssetDatabase.MoveAssetToTrash",
|
|
"EditorApplication.Exit",
|
|
"Process.Start",
|
|
"Process.Kill",
|
|
"while(true)",
|
|
"while (true)",
|
|
"for(;;)",
|
|
"for (;;)",
|
|
};
|
|
|
|
public static object HandleCommand(JObject @params)
|
|
{
|
|
if (@params == null)
|
|
return new ErrorResponse("Parameters cannot be null.");
|
|
|
|
var p = new ToolParams(@params);
|
|
var actionResult = p.GetRequired("action");
|
|
if (!actionResult.IsSuccess)
|
|
return new ErrorResponse(actionResult.ErrorMessage);
|
|
|
|
string action = actionResult.Value.ToLowerInvariant();
|
|
|
|
switch (action)
|
|
{
|
|
case ActionExecute:
|
|
return HandleExecute(@params);
|
|
case ActionGetHistory:
|
|
return HandleGetHistory(@params);
|
|
case ActionClearHistory:
|
|
return HandleClearHistory();
|
|
case ActionReplay:
|
|
return HandleReplay(@params);
|
|
default:
|
|
return new ErrorResponse(
|
|
$"Unknown action: '{action}'. Valid actions: {ActionExecute}, {ActionGetHistory}, {ActionClearHistory}, {ActionReplay}");
|
|
}
|
|
}
|
|
|
|
private static object HandleExecute(JObject @params)
|
|
{
|
|
string code = @params["code"]?.ToString();
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
return new ErrorResponse("Required parameter 'code' is missing or empty.");
|
|
|
|
if (code.Length > MaxCodeLength)
|
|
return new ErrorResponse($"Code exceeds maximum length of {MaxCodeLength} characters.");
|
|
|
|
bool safetyChecks = @params["safety_checks"]?.Value<bool>() ?? true;
|
|
string compiler = @params["compiler"]?.ToString()?.ToLowerInvariant() ?? "auto";
|
|
|
|
if (safetyChecks)
|
|
{
|
|
var violation = CheckBlockedPatterns(code);
|
|
if (violation != null)
|
|
return new ErrorResponse($"Blocked pattern detected: {violation}");
|
|
}
|
|
|
|
try
|
|
{
|
|
var startTime = DateTime.UtcNow;
|
|
var result = CompileAndExecute(code, compiler);
|
|
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
|
|
|
|
AddToHistory(code, result, elapsed, safetyChecks, compiler);
|
|
return result;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
McpLog.Error($"[ExecuteCode] Execution failed: {e}");
|
|
var errorResult = new ErrorResponse($"Execution failed: {e.Message}");
|
|
AddToHistory(code, errorResult, 0, safetyChecks, compiler);
|
|
return errorResult;
|
|
}
|
|
}
|
|
|
|
private static object HandleGetHistory(JObject @params)
|
|
{
|
|
int limit = @params["limit"]?.Value<int>() ?? 10;
|
|
limit = Math.Clamp(limit, 1, MaxHistoryEntries);
|
|
|
|
if (_history.Count == 0)
|
|
return new SuccessResponse("No execution history.", new { total = 0, entries = new object[0] });
|
|
|
|
var entries = _history.Skip(Math.Max(0, _history.Count - limit)).ToList();
|
|
return new SuccessResponse($"Returning {entries.Count} of {_history.Count} history entries.", new
|
|
{
|
|
total = _history.Count,
|
|
entries = entries.Select((e, i) => new
|
|
{
|
|
index = _history.Count - entries.Count + i,
|
|
codePreview = e.code.Length > MaxHistoryCodePreview
|
|
? e.code.Substring(0, MaxHistoryCodePreview) + "..."
|
|
: e.code,
|
|
e.success,
|
|
e.resultPreview,
|
|
e.elapsedMs,
|
|
e.timestamp,
|
|
e.safetyChecksEnabled,
|
|
e.compiler,
|
|
}).ToList(),
|
|
});
|
|
}
|
|
|
|
private static object HandleClearHistory()
|
|
{
|
|
int count = _history.Count;
|
|
_history.Clear();
|
|
return new SuccessResponse($"Cleared {count} history entries.");
|
|
}
|
|
|
|
private static object HandleReplay(JObject @params)
|
|
{
|
|
if (_history.Count == 0)
|
|
return new ErrorResponse("No execution history to replay.");
|
|
|
|
int? index = @params["index"]?.Value<int>();
|
|
if (index == null || index < 0 || index >= _history.Count)
|
|
return new ErrorResponse($"Invalid history index. Valid range: 0-{_history.Count - 1}");
|
|
|
|
var entry = _history[index.Value];
|
|
var replayParams = JObject.FromObject(new
|
|
{
|
|
action = ActionExecute,
|
|
code = entry.code,
|
|
safety_checks = entry.safetyChecksEnabled,
|
|
compiler = entry.compiler ?? "auto",
|
|
});
|
|
return HandleExecute(replayParams);
|
|
}
|
|
|
|
// ──────────────────── Compilation ────────────────────
|
|
|
|
private static object CompileAndExecute(string code, string compiler)
|
|
{
|
|
string wrappedSource = WrapUserCode(code);
|
|
string[] assemblyPaths = GetAssemblyPaths();
|
|
|
|
Assembly compiled;
|
|
string usedCompiler;
|
|
|
|
switch (compiler)
|
|
{
|
|
case "roslyn":
|
|
if (!RoslynCompiler.IsAvailable)
|
|
return new ErrorResponse("Roslyn (Microsoft.CodeAnalysis) is not available. Install it via NuGet or use compiler='codedom'.");
|
|
compiled = RoslynCompiler.Compile(wrappedSource, assemblyPaths, out var roslynErrors);
|
|
if (compiled == null)
|
|
return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(roslynErrors), compiler = "roslyn" });
|
|
usedCompiler = "roslyn";
|
|
break;
|
|
|
|
case "codedom":
|
|
compiled = CodeDomCompile(wrappedSource, assemblyPaths, out var codedomErrors);
|
|
if (compiled == null)
|
|
return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(codedomErrors), compiler = "codedom" });
|
|
usedCompiler = "codedom";
|
|
break;
|
|
|
|
default: // "auto"
|
|
if (RoslynCompiler.IsAvailable)
|
|
{
|
|
compiled = RoslynCompiler.Compile(wrappedSource, assemblyPaths, out var autoErrors);
|
|
if (compiled == null)
|
|
return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(autoErrors), compiler = "roslyn" });
|
|
usedCompiler = "roslyn";
|
|
}
|
|
else
|
|
{
|
|
compiled = CodeDomCompile(wrappedSource, assemblyPaths, out var autoFallbackErrors);
|
|
if (compiled == null)
|
|
return new ErrorResponse("Compilation failed", new { errors = OffsetErrors(autoFallbackErrors), compiler = "codedom" });
|
|
usedCompiler = "codedom";
|
|
}
|
|
break;
|
|
}
|
|
|
|
return InvokeCompiled(compiled, usedCompiler);
|
|
}
|
|
|
|
private static object InvokeCompiled(Assembly assembly, string compilerUsed)
|
|
{
|
|
var type = assembly.GetType(WrapperClassName);
|
|
if (type == null)
|
|
return new ErrorResponse("Internal error: failed to find compiled type.");
|
|
|
|
var method = type.GetMethod(WrapperMethodName, BindingFlags.Public | BindingFlags.Static);
|
|
if (method == null)
|
|
return new ErrorResponse("Internal error: failed to find Execute method.");
|
|
|
|
object result = null;
|
|
Exception executionError = null;
|
|
|
|
try
|
|
{
|
|
result = method.Invoke(null, null);
|
|
}
|
|
catch (TargetInvocationException tie)
|
|
{
|
|
executionError = tie.InnerException ?? tie;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
executionError = e;
|
|
}
|
|
|
|
if (executionError != null)
|
|
return new ErrorResponse($"Runtime error: {executionError.Message}",
|
|
new { exceptionType = executionError.GetType().Name, stackTrace = executionError.StackTrace, compiler = compilerUsed });
|
|
|
|
if (result != null)
|
|
return new SuccessResponse("Code executed successfully.",
|
|
new { result = SerializeResult(result), compiler = compilerUsed });
|
|
|
|
return new SuccessResponse("Code executed successfully.", new { compiler = compilerUsed });
|
|
}
|
|
|
|
private static List<string> OffsetErrors(List<string> errors)
|
|
{
|
|
// Errors already have line numbers adjusted by the compiler-specific code
|
|
return errors;
|
|
}
|
|
|
|
// ──────────────────── CodeDom compiler ────────────────────
|
|
|
|
private static Assembly CodeDomCompile(string source, string[] assemblyPaths, out List<string> errors)
|
|
{
|
|
errors = new List<string>();
|
|
|
|
// CodeDom needs the netstandard-aware filtered paths
|
|
var filtered = FilterAssemblyPathsForCodeDom(assemblyPaths);
|
|
|
|
// CSharpCodeProvider turns every ReferencedAssemblies entry into a literal /r:"..." flag
|
|
// on the csc command line. Projects with ~100+ asmdefs blow past Windows' 32 KB
|
|
// CreateProcess argument limit and fail with "The filename or extension is too long."
|
|
// Route references through a response file (@responsefile is supported by both mcs and
|
|
// Roslyn csc) so we pass exactly one short argument regardless of reference count.
|
|
string responseFilePath = Path.Combine(Path.GetTempPath(), $"mcp-codedom-{Guid.NewGuid():N}.rsp");
|
|
|
|
try
|
|
{
|
|
using (var writer = new StreamWriter(responseFilePath, append: false, Encoding.UTF8))
|
|
{
|
|
foreach (var path in filtered)
|
|
{
|
|
writer.Write("/r:\"");
|
|
writer.Write(path);
|
|
writer.WriteLine("\"");
|
|
}
|
|
}
|
|
|
|
using (var provider = new CSharpCodeProvider())
|
|
{
|
|
var parameters = new CompilerParameters
|
|
{
|
|
GenerateInMemory = true,
|
|
GenerateExecutable = false,
|
|
TreatWarningsAsErrors = false,
|
|
CompilerOptions = "@\"" + responseFilePath + "\"",
|
|
};
|
|
|
|
var results = provider.CompileAssemblyFromSource(parameters, source);
|
|
|
|
if (results.Errors.HasErrors)
|
|
{
|
|
foreach (CompilerError error in results.Errors)
|
|
{
|
|
if (!error.IsWarning)
|
|
{
|
|
int userLine = Math.Max(1, error.Line - WrapperLineOffset);
|
|
errors.Add($"Line {userLine}: {error.ErrorText}");
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return results.CompiledAssembly;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
try { if (File.Exists(responseFilePath)) File.Delete(responseFilePath); }
|
|
catch { /* best effort */ }
|
|
}
|
|
}
|
|
|
|
// CSharpCodeProvider can't resolve type-forwarding, so when netstandard.dll is loaded
|
|
// alongside mscorlib/System.Runtime/System.Collections, types like List<T> appear in
|
|
// multiple assemblies causing "type defined multiple times" errors.
|
|
private static readonly HashSet<string> _codedomDuplicateAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"mscorlib",
|
|
"System.Runtime",
|
|
"System.Private.CoreLib",
|
|
"System.Collections",
|
|
};
|
|
|
|
private static string[] FilterAssemblyPathsForCodeDom(string[] allPaths)
|
|
{
|
|
bool hasNetstandard = allPaths.Any(p =>
|
|
string.Equals(Path.GetFileNameWithoutExtension(p), "netstandard", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (!hasNetstandard)
|
|
return allPaths;
|
|
|
|
return allPaths.Where(p =>
|
|
!_codedomDuplicateAssemblies.Contains(Path.GetFileNameWithoutExtension(p))).ToArray();
|
|
}
|
|
|
|
// ──────────────────── Shared helpers ────────────────────
|
|
|
|
private static string WrapUserCode(string code)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("using System;");
|
|
sb.AppendLine("using System.Collections.Generic;");
|
|
sb.AppendLine("using System.Linq;");
|
|
sb.AppendLine("using System.Reflection;");
|
|
sb.AppendLine("using UnityEngine;");
|
|
sb.AppendLine("using UnityEditor;");
|
|
sb.AppendLine($"public static class {WrapperClassName}");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine($" public static object {WrapperMethodName}()");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(code);
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string[] GetAssemblyPaths()
|
|
{
|
|
if (_cachedAssemblyPaths == null)
|
|
_cachedAssemblyPaths = ResolveAssemblyPaths();
|
|
return _cachedAssemblyPaths;
|
|
}
|
|
|
|
private static string[] ResolveAssemblyPaths()
|
|
{
|
|
var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var assembly in UnityAssembliesCompat.GetLoadedAssemblies())
|
|
{
|
|
try
|
|
{
|
|
if (assembly.IsDynamic) continue;
|
|
var location = assembly.Location;
|
|
if (string.IsNullOrEmpty(location)) continue;
|
|
if (!File.Exists(location)) continue;
|
|
paths.Add(location);
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
// Some assemblies don't support Location property
|
|
}
|
|
}
|
|
|
|
var result = new string[paths.Count];
|
|
paths.CopyTo(result);
|
|
return result;
|
|
}
|
|
|
|
private static string CheckBlockedPatterns(string code)
|
|
{
|
|
foreach (var pattern in _blockedPatterns)
|
|
{
|
|
if (code.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
return $"Code contains blocked pattern: '{pattern}'. Disable safety checks with safety_checks=false if this is intentional.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static void AddToHistory(string code, object result, double elapsedMs, bool safetyChecks, string compiler = "auto")
|
|
{
|
|
string preview;
|
|
if (result is SuccessResponse sr)
|
|
preview = sr.Data?.ToString() ?? sr.Message;
|
|
else if (result is ErrorResponse er)
|
|
preview = er.Error;
|
|
else
|
|
preview = result?.ToString() ?? "null";
|
|
|
|
if (preview != null && preview.Length > 200)
|
|
preview = preview.Substring(0, 200) + "...";
|
|
|
|
_history.Add(new HistoryEntry
|
|
{
|
|
code = code,
|
|
success = result is SuccessResponse,
|
|
resultPreview = preview,
|
|
elapsedMs = Math.Round(elapsedMs, 1),
|
|
timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
|
safetyChecksEnabled = safetyChecks,
|
|
compiler = compiler,
|
|
});
|
|
|
|
while (_history.Count > MaxHistoryEntries)
|
|
_history.RemoveAt(0);
|
|
}
|
|
|
|
private static object SerializeResult(object result)
|
|
{
|
|
if (result == null) return null;
|
|
|
|
var type = result.GetType();
|
|
if (type.IsPrimitive || result is string || result is decimal)
|
|
return result;
|
|
|
|
try
|
|
{
|
|
return JToken.FromObject(result);
|
|
}
|
|
catch
|
|
{
|
|
return result.ToString();
|
|
}
|
|
}
|
|
|
|
private class HistoryEntry
|
|
{
|
|
public string code;
|
|
public bool success;
|
|
public string resultPreview;
|
|
public double elapsedMs;
|
|
public string timestamp;
|
|
public bool safetyChecksEnabled;
|
|
public string compiler;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Roslyn compiler backend accessed entirely via reflection.
|
|
/// No compile-time dependency on Microsoft.CodeAnalysis — works only if the package is installed.
|
|
/// </summary>
|
|
internal static class RoslynCompiler
|
|
{
|
|
private static bool? _isAvailable;
|
|
private static Type _syntaxTreeType;
|
|
private static Type _compilationType;
|
|
private static Type _compilationOptionsType;
|
|
private static Type _parseOptionsType;
|
|
private static Type _metadataReferenceType;
|
|
private static Type _outputKindEnum;
|
|
private static Type _languageVersionEnum;
|
|
private static MethodInfo _parseText;
|
|
private static MethodInfo _createCompilation;
|
|
private static MethodInfo _createFromFile;
|
|
private static MethodInfo _emit;
|
|
private static object _parseOptions;
|
|
private static object _compilationOptions;
|
|
|
|
public static bool IsAvailable
|
|
{
|
|
get
|
|
{
|
|
if (_isAvailable == null)
|
|
_isAvailable = Initialize();
|
|
return _isAvailable.Value;
|
|
}
|
|
}
|
|
|
|
public static void ResetCache()
|
|
{
|
|
_isAvailable = null;
|
|
}
|
|
|
|
private static bool Initialize()
|
|
{
|
|
try
|
|
{
|
|
_syntaxTreeType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree, Microsoft.CodeAnalysis.CSharp");
|
|
_compilationType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpCompilation, Microsoft.CodeAnalysis.CSharp");
|
|
_compilationOptionsType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions, Microsoft.CodeAnalysis.CSharp");
|
|
_parseOptionsType = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpParseOptions, Microsoft.CodeAnalysis.CSharp");
|
|
_metadataReferenceType = Type.GetType("Microsoft.CodeAnalysis.MetadataReference, Microsoft.CodeAnalysis");
|
|
_outputKindEnum = Type.GetType("Microsoft.CodeAnalysis.OutputKind, Microsoft.CodeAnalysis");
|
|
_languageVersionEnum = Type.GetType("Microsoft.CodeAnalysis.CSharp.LanguageVersion, Microsoft.CodeAnalysis.CSharp");
|
|
|
|
if (_syntaxTreeType == null || _compilationType == null || _compilationOptionsType == null ||
|
|
_parseOptionsType == null || _metadataReferenceType == null || _outputKindEnum == null ||
|
|
_languageVersionEnum == null)
|
|
return false;
|
|
|
|
// CSharpSyntaxTree.ParseText(string, CSharpParseOptions, string, Encoding, CancellationToken)
|
|
var syntaxTreeBase = Type.GetType("Microsoft.CodeAnalysis.SyntaxTree, Microsoft.CodeAnalysis");
|
|
_parseText = _syntaxTreeType.GetMethod("ParseText", new[] { typeof(string), _parseOptionsType, typeof(string), typeof(Encoding), typeof(System.Threading.CancellationToken) });
|
|
if (_parseText == null)
|
|
return false;
|
|
|
|
// CSharpCompilation.Create(string, IEnumerable<SyntaxTree>, IEnumerable<MetadataReference>, CSharpCompilationOptions)
|
|
var metadataRefBase = _metadataReferenceType;
|
|
var syntaxTreeEnumerable = typeof(IEnumerable<>).MakeGenericType(syntaxTreeBase);
|
|
var metadataRefEnumerable = typeof(IEnumerable<>).MakeGenericType(metadataRefBase);
|
|
_createCompilation = _compilationType.GetMethod("Create", new[] { typeof(string), syntaxTreeEnumerable, metadataRefEnumerable, _compilationOptionsType });
|
|
if (_createCompilation == null)
|
|
return false;
|
|
|
|
// MetadataReference.CreateFromFile(string, MetadataReferenceProperties, DocumentationProvider)
|
|
_createFromFile = _metadataReferenceType.GetMethods(BindingFlags.Public | BindingFlags.Static)
|
|
.FirstOrDefault(m => m.Name == "CreateFromFile");
|
|
if (_createFromFile == null)
|
|
return false;
|
|
|
|
// Emit has no single-param overload; the simplest is
|
|
// Emit(Stream, Stream, Stream, Stream, IEnumerable<ResourceDescription>, EmitOptions, CancellationToken)
|
|
var compilationBase = Type.GetType("Microsoft.CodeAnalysis.Compilation, Microsoft.CodeAnalysis");
|
|
if (compilationBase == null) return false;
|
|
_emit = compilationBase.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(m => m.Name == "Emit")
|
|
.OrderBy(m => m.GetParameters().Length)
|
|
.FirstOrDefault();
|
|
if (_emit == null)
|
|
return false;
|
|
|
|
// Build CSharpParseOptions — constructor has optional params, use reflection
|
|
var latestValue = Enum.Parse(_languageVersionEnum, "Latest");
|
|
var parseOptionsCtor = _parseOptionsType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0];
|
|
var parseCtorParams = parseOptionsCtor.GetParameters();
|
|
var parseArgs = new object[parseCtorParams.Length];
|
|
for (int i = 0; i < parseCtorParams.Length; i++)
|
|
{
|
|
if (parseCtorParams[i].Name == "languageVersion")
|
|
parseArgs[i] = latestValue;
|
|
else if (parseCtorParams[i].HasDefaultValue)
|
|
parseArgs[i] = parseCtorParams[i].DefaultValue;
|
|
else
|
|
parseArgs[i] = null;
|
|
}
|
|
_parseOptions = parseOptionsCtor.Invoke(parseArgs);
|
|
|
|
// Build CSharpCompilationOptions — use the first constructor (has most defaults)
|
|
var dllKind = Enum.Parse(_outputKindEnum, "DynamicallyLinkedLibrary");
|
|
var compOptionsCtor = _compilationOptionsType.GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0];
|
|
var compCtorParams = compOptionsCtor.GetParameters();
|
|
var compArgs = new object[compCtorParams.Length];
|
|
for (int i = 0; i < compCtorParams.Length; i++)
|
|
{
|
|
if (compCtorParams[i].Name == "outputKind")
|
|
compArgs[i] = dllKind;
|
|
else if (compCtorParams[i].HasDefaultValue)
|
|
compArgs[i] = compCtorParams[i].DefaultValue;
|
|
else
|
|
compArgs[i] = null;
|
|
}
|
|
_compilationOptions = compOptionsCtor.Invoke(compArgs);
|
|
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
McpLog.Warn($"[ExecuteCode] Roslyn initialization failed: {e.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static Assembly Compile(string source, string[] assemblyPaths, out List<string> errors)
|
|
{
|
|
errors = new List<string>();
|
|
|
|
try
|
|
{
|
|
// Parse source
|
|
var syntaxTree = _parseText.Invoke(null, new object[] { source, _parseOptions, null, null, default(System.Threading.CancellationToken) });
|
|
|
|
// Build metadata references
|
|
var metadataRefBase = _metadataReferenceType;
|
|
var listType = typeof(List<>).MakeGenericType(metadataRefBase);
|
|
var refs = (System.Collections.IList)Activator.CreateInstance(listType);
|
|
|
|
foreach (var path in assemblyPaths)
|
|
{
|
|
try
|
|
{
|
|
var cfParams = _createFromFile.GetParameters();
|
|
var cfArgs = new object[cfParams.Length];
|
|
cfArgs[0] = path; // string path
|
|
for (int i = 1; i < cfParams.Length; i++)
|
|
cfArgs[i] = cfParams[i].HasDefaultValue ? cfParams[i].DefaultValue : null;
|
|
var metaRef = _createFromFile.Invoke(null, cfArgs);
|
|
refs.Add(metaRef);
|
|
}
|
|
catch
|
|
{
|
|
// Skip assemblies that can't be loaded as metadata
|
|
}
|
|
}
|
|
|
|
// Build syntax tree array
|
|
var syntaxTreeBase = Type.GetType("Microsoft.CodeAnalysis.SyntaxTree, Microsoft.CodeAnalysis");
|
|
var treeArray = Array.CreateInstance(syntaxTreeBase, 1);
|
|
treeArray.SetValue(syntaxTree, 0);
|
|
|
|
// Create compilation
|
|
var compilation = _createCompilation.Invoke(null, new object[] { "MCPDynamic", treeArray, refs, _compilationOptions });
|
|
|
|
// Emit to memory
|
|
using (var ms = new MemoryStream())
|
|
{
|
|
// Build args for the Emit overload (fill non-stream params with defaults)
|
|
var emitParams = _emit.GetParameters();
|
|
var emitArgs = new object[emitParams.Length];
|
|
emitArgs[0] = ms; // peStream
|
|
for (int i = 1; i < emitParams.Length; i++)
|
|
{
|
|
if (emitParams[i].HasDefaultValue)
|
|
emitArgs[i] = emitParams[i].DefaultValue;
|
|
else
|
|
emitArgs[i] = null;
|
|
}
|
|
var emitResult = _emit.Invoke(compilation, emitArgs);
|
|
|
|
// Check emitResult.Success
|
|
var successProp = emitResult.GetType().GetProperty("Success");
|
|
bool success = (bool)successProp.GetValue(emitResult);
|
|
|
|
if (!success)
|
|
{
|
|
// Read emitResult.Diagnostics
|
|
var diagProp = emitResult.GetType().GetProperty("Diagnostics");
|
|
var diagnostics = (System.Collections.IEnumerable)diagProp.GetValue(emitResult);
|
|
var severityError = Enum.Parse(Type.GetType("Microsoft.CodeAnalysis.DiagnosticSeverity, Microsoft.CodeAnalysis"), "Error");
|
|
|
|
foreach (var diag in diagnostics)
|
|
{
|
|
var sevProp = diag.GetType().GetProperty("Severity");
|
|
var severity = sevProp.GetValue(diag);
|
|
if (!severity.Equals(severityError)) continue;
|
|
|
|
var locProp = diag.GetType().GetProperty("Location");
|
|
var loc = locProp.GetValue(diag);
|
|
var spanProp = loc.GetType().GetMethod("GetLineSpan");
|
|
var lineSpan = spanProp.Invoke(loc, null);
|
|
var startProp = lineSpan.GetType().GetProperty("StartLinePosition");
|
|
var startPos = startProp.GetValue(lineSpan);
|
|
var lineProp = startPos.GetType().GetProperty("Line");
|
|
int line = (int)lineProp.GetValue(startPos);
|
|
|
|
var msgProp = diag.GetType().GetMethod("GetMessage", new[] { typeof(System.Globalization.CultureInfo) });
|
|
string msg = (string)msgProp.Invoke(diag, new object[] { null });
|
|
|
|
int userLine = Math.Max(1, line + 1 - ExecuteCode.WrapperLineOffset);
|
|
errors.Add($"Line {userLine}: {msg}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
ms.Seek(0, SeekOrigin.Begin);
|
|
return Assembly.Load(ms.ToArray());
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Walk to the deepest cause: TargetInvocationException (and friends) wrap the real
|
|
// failure inside .InnerException, and reporting only e.Message hides everything
|
|
// useful (e.g. a missing transitive dep manifests as the generic "Exception has been
|
|
// thrown by the target of an invocation.").
|
|
Exception root = e;
|
|
while (root.InnerException != null) root = root.InnerException;
|
|
errors.Add($"Roslyn compilation error: {root.GetType().Name}: {root.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|