205 lines
6.9 KiB
C#
205 lines
6.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Codely.Newtonsoft.Json.Linq;
|
|
using UnityEngine;
|
|
using UnityTcp.Editor.Helpers;
|
|
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
|
using Microsoft.CodeAnalysis.Scripting;
|
|
|
|
|
|
namespace UnityTcp.Editor.Tools
|
|
{
|
|
/// <summary>
|
|
/// Executes C# scripts using Microsoft.CodeAnalysis.CSharp.Scripting (Roslyn).
|
|
/// Captures and returns logs generated during script execution.
|
|
/// Ensures execution happens on the main thread.
|
|
/// </summary>
|
|
public static class ExecuteCSharpScript
|
|
{
|
|
private static List<string> _capturedLogs = new List<string>();
|
|
private static bool _isCapturingLogs = false;
|
|
|
|
/// <summary>
|
|
/// Main handler for executing C# scripts.
|
|
/// </summary>
|
|
public static object HandleCommand(JObject @params)
|
|
{
|
|
string script = @params["script"]?.ToString();
|
|
if (string.IsNullOrEmpty(script))
|
|
{
|
|
return Response.Error("'script' parameter is required.");
|
|
}
|
|
|
|
bool captureLogs = @params["capture_logs"]?.ToObject<bool>() ?? true;
|
|
string[] imports = @params["imports"]?.ToObject<string[]>() ?? new string[]
|
|
{
|
|
"System",
|
|
"System.Linq",
|
|
"System.Collections.Generic",
|
|
"UnityEngine",
|
|
"UnityEditor",
|
|
"UnityEditor.SceneManagement",
|
|
"UnityEngine.SceneManagement"
|
|
};
|
|
|
|
try
|
|
{
|
|
Debug.Log($"[ExecuteCSharpScript] Executing C# script (length: {script.Length} chars)");
|
|
|
|
StartLogCapture(captureLogs);
|
|
|
|
object result;
|
|
try
|
|
{
|
|
result = ExecuteScriptInternal(script, imports);
|
|
}
|
|
finally
|
|
{
|
|
// Always stop log capture, even on error
|
|
}
|
|
|
|
var logs = captureLogs ? StopLogCapture() : new List<string>();
|
|
|
|
return Response.Success(
|
|
"C# script executed successfully.",
|
|
new
|
|
{
|
|
result = result?.ToString(),
|
|
logs = logs,
|
|
log_count = logs.Count
|
|
}
|
|
);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
var logs = captureLogs ? StopLogCapture() : new List<string>();
|
|
Debug.LogError($"[ExecuteCSharpScript] Failed to execute script: {e}");
|
|
return Response.Error(
|
|
$"C# script execution failed: {e.Message}",
|
|
new
|
|
{
|
|
logs = logs,
|
|
exception = e.ToString()
|
|
}
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal method to execute the script using Roslyn.
|
|
/// </summary>
|
|
private static object ExecuteScriptInternal(string script, string[] imports)
|
|
{
|
|
try
|
|
{
|
|
// Collect assembly references
|
|
var references = new List<System.Reflection.Assembly>
|
|
{
|
|
typeof(UnityEngine.Debug).Assembly,
|
|
typeof(UnityEditor.EditorApplication).Assembly
|
|
};
|
|
|
|
// Add Assembly-CSharp if it exists (runtime user code)
|
|
var assemblyCSharp = System.AppDomain.CurrentDomain.GetAssemblies()
|
|
.FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp");
|
|
if (assemblyCSharp != null)
|
|
{
|
|
references.Add(assemblyCSharp);
|
|
}
|
|
|
|
// Add Assembly-CSharp-Editor if it exists (editor user code)
|
|
var assemblyCSharpEditor = System.AppDomain.CurrentDomain.GetAssemblies()
|
|
.FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp-Editor");
|
|
if (assemblyCSharpEditor != null)
|
|
{
|
|
references.Add(assemblyCSharpEditor);
|
|
}
|
|
|
|
// Add Unity.InputSystem if it exists (Input System package)
|
|
var inputSystemAssembly = System.AppDomain.CurrentDomain.GetAssemblies()
|
|
.FirstOrDefault(a => a.GetName().Name == "Unity.InputSystem");
|
|
if (inputSystemAssembly != null)
|
|
{
|
|
references.Add(inputSystemAssembly);
|
|
}
|
|
|
|
// Create script options with imports
|
|
var options = ScriptOptions.Default
|
|
.WithReferences(references)
|
|
.WithImports(imports);
|
|
|
|
// Execute the script synchronously
|
|
var scriptTask = CSharpScript.EvaluateAsync(script, options);
|
|
|
|
// Wait for the task to complete
|
|
scriptTask.Wait();
|
|
|
|
return scriptTask.Result;
|
|
}
|
|
catch (AggregateException ae)
|
|
{
|
|
// Unwrap AggregateException to get the actual exception
|
|
if (ae.InnerException != null)
|
|
{
|
|
throw ae.InnerException;
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts capturing Unity logs.
|
|
/// </summary>
|
|
private static void StartLogCapture(bool enabled)
|
|
{
|
|
if (!enabled)
|
|
{
|
|
_isCapturingLogs = false;
|
|
return;
|
|
}
|
|
|
|
_capturedLogs.Clear();
|
|
_isCapturingLogs = true;
|
|
Application.logMessageReceived += OnLogMessageReceived;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops capturing logs and returns the captured log list.
|
|
/// </summary>
|
|
private static List<string> StopLogCapture()
|
|
{
|
|
Application.logMessageReceived -= OnLogMessageReceived;
|
|
_isCapturingLogs = false;
|
|
|
|
var logs = new List<string>(_capturedLogs);
|
|
_capturedLogs.Clear();
|
|
|
|
return logs;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Log message callback handler.
|
|
/// </summary>
|
|
private static void OnLogMessageReceived(string logString, string stackTrace, LogType type)
|
|
{
|
|
if (!_isCapturingLogs)
|
|
return;
|
|
|
|
var logEntry = new StringBuilder();
|
|
logEntry.Append($"[{type}] {logString}");
|
|
|
|
// Include stack trace for errors and exceptions
|
|
if ((type == LogType.Error || type == LogType.Exception) && !string.IsNullOrEmpty(stackTrace))
|
|
{
|
|
logEntry.Append($"\n{stackTrace}");
|
|
}
|
|
|
|
_capturedLogs.Add(logEntry.ToString());
|
|
}
|
|
}
|
|
}
|