chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Utilities to await arbitrary awaitable objects using the C# awaitable pattern.
|
||||
/// Supports Task, Task<T>, ValueTask, ValueTask<T>, UniTask, UniTask<T> and any type
|
||||
/// implementing GetAwaiter/OnCompleted/GetResult.
|
||||
/// </summary>
|
||||
internal static class AwaitableHelper
|
||||
{
|
||||
public static async Task<object> AwaitIfNeeded(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type valueType = value.GetType();
|
||||
|
||||
// Task and Task<T>
|
||||
if (typeof(Task).IsAssignableFrom(valueType))
|
||||
{
|
||||
Task task = (Task)value;
|
||||
await task.ConfigureAwait(false);
|
||||
|
||||
if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Task<>))
|
||||
{
|
||||
PropertyInfo resultProperty = valueType.GetProperty("Result");
|
||||
if (resultProperty != null)
|
||||
{
|
||||
object taskResult = resultProperty.GetValue(value);
|
||||
return taskResult;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ValueTask and ValueTask<T> via AsTask if available
|
||||
if (IsValueTask(valueType))
|
||||
{
|
||||
Task asTask = ConvertValueTaskToTask(value);
|
||||
await asTask.ConfigureAwait(false);
|
||||
return null;
|
||||
}
|
||||
if (IsGenericValueTask(valueType))
|
||||
{
|
||||
Task asTask = ConvertGenericValueTaskToTask(value);
|
||||
await asTask.ConfigureAwait(false);
|
||||
|
||||
Type[] genericArgs = valueType.GetGenericArguments();
|
||||
if (genericArgs != null && genericArgs.Length == 1)
|
||||
{
|
||||
Type taskType = typeof(Task<>).MakeGenericType(genericArgs[0]);
|
||||
PropertyInfo resultProperty = taskType.GetProperty("Result");
|
||||
if (resultProperty != null)
|
||||
{
|
||||
object resultValue = resultProperty.GetValue(asTask);
|
||||
return resultValue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Awaitable pattern fallback (e.g., UniTask/UniTask<T> or custom awaitables)
|
||||
MethodInfo getAwaiterMethod = valueType.GetMethod("GetAwaiter", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
if (getAwaiterMethod != null)
|
||||
{
|
||||
object awaiter = getAwaiterMethod.Invoke(value, null);
|
||||
if (awaiter == null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
Type awaiterType = awaiter.GetType();
|
||||
PropertyInfo isCompletedProperty = awaiterType.GetProperty("IsCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
MethodInfo getResultMethod = awaiterType.GetMethod("GetResult", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
MethodInfo onCompletedMethod = awaiterType.GetMethod("OnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null);
|
||||
MethodInfo unsafeOnCompletedMethod = awaiterType.GetMethod("UnsafeOnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null);
|
||||
|
||||
if (isCompletedProperty != null)
|
||||
{
|
||||
bool isCompleted = (bool)isCompletedProperty.GetValue(awaiter);
|
||||
if (isCompleted)
|
||||
{
|
||||
object completedResult = InvokeGetResultSafely(getResultMethod, awaiter);
|
||||
return completedResult;
|
||||
}
|
||||
}
|
||||
|
||||
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
|
||||
|
||||
Action continuation = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
object continuationResult = InvokeGetResultSafely(getResultMethod, awaiter);
|
||||
tcs.TrySetResult(continuationResult);
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
Exception inner = tie.InnerException ?? tie;
|
||||
tcs.TrySetException(inner);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
if (unsafeOnCompletedMethod != null)
|
||||
{
|
||||
unsafeOnCompletedMethod.Invoke(awaiter, new object[] { continuation });
|
||||
}
|
||||
else if (onCompletedMethod != null)
|
||||
{
|
||||
onCompletedMethod.Invoke(awaiter, new object[] { continuation });
|
||||
}
|
||||
else
|
||||
{
|
||||
// No completion registration; treat as completed
|
||||
object immediateResult = InvokeGetResultSafely(getResultMethod, awaiter);
|
||||
return immediateResult;
|
||||
}
|
||||
|
||||
object awaited = await tcs.Task.ConfigureAwait(false);
|
||||
return awaited;
|
||||
}
|
||||
|
||||
// Not awaitable; return as-is
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool IsValueTask(Type type)
|
||||
{
|
||||
return type.FullName == "System.Threading.Tasks.ValueTask";
|
||||
}
|
||||
|
||||
private static bool IsGenericValueTask(Type type)
|
||||
{
|
||||
if (!type.IsGenericType)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Type generic = type.GetGenericTypeDefinition();
|
||||
return generic.FullName == "System.Threading.Tasks.ValueTask`1";
|
||||
}
|
||||
|
||||
private static Task ConvertValueTaskToTask(object valueTask)
|
||||
{
|
||||
Type type = valueTask.GetType();
|
||||
MethodInfo asTaskMethod = type.GetMethod("AsTask", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (asTaskMethod != null)
|
||||
{
|
||||
object taskObj = asTaskMethod.Invoke(valueTask, null);
|
||||
return (Task)taskObj;
|
||||
}
|
||||
|
||||
// Fallback: build a Task from awaiter
|
||||
MethodInfo getAwaiter = type.GetMethod("GetAwaiter", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
if (getAwaiter == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
object awaiter = getAwaiter.Invoke(valueTask, null);
|
||||
Type awaiterType = awaiter.GetType();
|
||||
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
MethodInfo onCompleted = awaiterType.GetMethod("OnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null);
|
||||
MethodInfo unsafeOnCompleted = awaiterType.GetMethod("UnsafeOnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null);
|
||||
MethodInfo getResult = awaiterType.GetMethod("GetResult", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
PropertyInfo isCompletedProperty = awaiterType.GetProperty("IsCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
bool hasIsCompleted = false;
|
||||
bool isCompleted = false;
|
||||
if (isCompletedProperty != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
object isCompletedValue = isCompletedProperty.GetValue(awaiter);
|
||||
if (isCompletedValue is bool boolValue)
|
||||
{
|
||||
isCompleted = boolValue;
|
||||
hasIsCompleted = true;
|
||||
}
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
Exception inner = tie.InnerException ?? tie;
|
||||
tcs.TrySetException(inner);
|
||||
return tcs.Task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
Action cont = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (getResult != null)
|
||||
{
|
||||
getResult.Invoke(awaiter, null);
|
||||
}
|
||||
tcs.TrySetResult(null);
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
Exception inner = tie.InnerException ?? tie;
|
||||
tcs.TrySetException(inner);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
if (hasIsCompleted && isCompleted)
|
||||
{
|
||||
cont();
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
if (unsafeOnCompleted != null)
|
||||
{
|
||||
unsafeOnCompleted.Invoke(awaiter, new object[] { cont });
|
||||
}
|
||||
else if (onCompleted != null)
|
||||
{
|
||||
onCompleted.Invoke(awaiter, new object[] { cont });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasIsCompleted && isCompleted)
|
||||
{
|
||||
cont();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = Task.Run(cont);
|
||||
}
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static Task ConvertGenericValueTaskToTask(object valueTaskT)
|
||||
{
|
||||
Type type = valueTaskT.GetType();
|
||||
MethodInfo asTaskMethod = type.GetMethod("AsTask", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (asTaskMethod != null)
|
||||
{
|
||||
object taskObj = asTaskMethod.Invoke(valueTaskT, null);
|
||||
return (Task)taskObj;
|
||||
}
|
||||
|
||||
// Fallback through awaiter
|
||||
MethodInfo getAwaiter = type.GetMethod("GetAwaiter", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
if (getAwaiter == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
object awaiter = getAwaiter.Invoke(valueTaskT, null);
|
||||
Type awaiterType = awaiter.GetType();
|
||||
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
MethodInfo onCompleted = awaiterType.GetMethod("OnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null);
|
||||
MethodInfo unsafeOnCompleted = awaiterType.GetMethod("UnsafeOnCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Action) }, null);
|
||||
MethodInfo getResult = awaiterType.GetMethod("GetResult", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
|
||||
PropertyInfo isCompletedProperty = awaiterType.GetProperty("IsCompleted", BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
bool hasIsCompleted = false;
|
||||
bool isCompleted = false;
|
||||
if (isCompletedProperty != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
object isCompletedValue = isCompletedProperty.GetValue(awaiter);
|
||||
if (isCompletedValue is bool boolValue)
|
||||
{
|
||||
isCompleted = boolValue;
|
||||
hasIsCompleted = true;
|
||||
}
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
Exception inner = tie.InnerException ?? tie;
|
||||
tcs.TrySetException(inner);
|
||||
return tcs.Task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
Action cont = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (getResult != null)
|
||||
{
|
||||
object _ = getResult.Invoke(awaiter, null);
|
||||
}
|
||||
tcs.TrySetResult(null);
|
||||
}
|
||||
catch (TargetInvocationException tie)
|
||||
{
|
||||
Exception inner = tie.InnerException ?? tie;
|
||||
tcs.TrySetException(inner);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
if (hasIsCompleted && isCompleted)
|
||||
{
|
||||
cont();
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
if (unsafeOnCompleted != null)
|
||||
{
|
||||
unsafeOnCompleted.Invoke(awaiter, new object[] { cont });
|
||||
}
|
||||
else if (onCompleted != null)
|
||||
{
|
||||
onCompleted.Invoke(awaiter, new object[] { cont });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasIsCompleted && isCompleted)
|
||||
{
|
||||
cont();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = Task.Run(cont);
|
||||
}
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private static object InvokeGetResultSafely(MethodInfo getResultMethod, object awaiter)
|
||||
{
|
||||
if (getResultMethod == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
object result = getResultMethod.Invoke(awaiter, null);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d3c6cb5277fec4833a373d410f5caee0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,369 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEditor;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the prepared wrapper entry point while keeping Undo and cancellation handling consistent.
|
||||
/// </summary>
|
||||
public class CommandRunner : ICompiledCommandInvoker
|
||||
{
|
||||
private readonly CompiledCommandEntryPointResolver _entryPointResolver;
|
||||
private bool _isRunning = false;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
public CommandRunner()
|
||||
: this(DynamicCodeServices.CommandEntryPointResolver)
|
||||
{
|
||||
}
|
||||
|
||||
internal CommandRunner(CompiledCommandEntryPointResolver entryPointResolver)
|
||||
{
|
||||
_entryPointResolver = entryPointResolver;
|
||||
}
|
||||
|
||||
private static void LogExecutionError(Exception ex, string correlationId)
|
||||
{
|
||||
VibeLogger.LogError(
|
||||
"command_execution_error",
|
||||
"Command execution failed with exception",
|
||||
new {
|
||||
error = ex.Message,
|
||||
stackTrace = ex.StackTrace
|
||||
},
|
||||
correlationId,
|
||||
"Unexpected execution error occurred",
|
||||
"Investigate execution failures and improve error handling"
|
||||
);
|
||||
}
|
||||
|
||||
private CancellationTokenSource CreateCombinedCancellationTokenSource(ExecutionContext context)
|
||||
{
|
||||
return CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cancellationTokenSource.Token,
|
||||
context.CancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
public ExecutionResult Execute(ExecutionContext context)
|
||||
{
|
||||
string correlationId = McpConstants.GenerateCorrelationId();
|
||||
if (!TryBeginExecution(out int undoGroup))
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource combinedCts = CreateCombinedCancellationTokenSource(context);
|
||||
return ExecuteInternal(context, combinedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return CreateCancelledResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogExecutionError(ex, correlationId);
|
||||
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string> { $"Exception: {ex.Message}" });
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndExecution(undoGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(ExecutionContext context)
|
||||
{
|
||||
string correlationId = McpConstants.GenerateCorrelationId();
|
||||
if (!TryBeginExecution(out int undoGroup))
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource combinedCts = CreateCombinedCancellationTokenSource(context);
|
||||
return await ExecuteInternalAsync(context, combinedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return CreateCancelledResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogExecutionError(ex, correlationId);
|
||||
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string> { $"Exception: {ex.Message}" });
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndExecution(undoGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryBeginExecution(out int undoGroup)
|
||||
{
|
||||
undoGroup = -1;
|
||||
if (_isRunning)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("ExecuteDynamicCode");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EndExecution(int undoGroup)
|
||||
{
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
_isRunning = false;
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateErrorResult(string errorMessage, List<string> logs = null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
ExecutionTime = TimeSpan.Zero,
|
||||
Logs = logs ?? new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateCancelledResult()
|
||||
{
|
||||
return CreateErrorResult(
|
||||
McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED,
|
||||
new List<string> { "Execution cancelled" });
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateSuccessResult(string result, List<string> logs = null)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = true,
|
||||
Result = result,
|
||||
ExecutionTime = TimeSpan.Zero, // Will be set by the caller
|
||||
Logs = logs ?? new List<string> { "Execution completed successfully" }
|
||||
};
|
||||
}
|
||||
|
||||
private static object CreateInstance(Type targetType)
|
||||
{
|
||||
return Activator.CreateInstance(targetType);
|
||||
}
|
||||
|
||||
private static object InvokeExecuteMethod(
|
||||
MethodInfo executeMethod,
|
||||
object instance,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
object[] callArgs = BuildSupportedArguments(executeMethod, parameters, cancellationToken);
|
||||
return executeMethod.Invoke(instance, callArgs);
|
||||
}
|
||||
|
||||
private ExecutionResult ExecuteInternal(ExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.CompiledAssembly == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_NO_COMPILED_ASSEMBLY);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Find executable type from assembly (sync only)
|
||||
(Type targetType, MethodInfo executeMethod) = _entryPointResolver.TryFindExecuteMethod(
|
||||
context.CompiledAssembly);
|
||||
|
||||
if (targetType == null || executeMethod == null)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
McpConstants.ERROR_MESSAGE_NO_EXECUTE_METHOD,
|
||||
new List<string> { "Assembly types checked but no Execute method found" });
|
||||
}
|
||||
|
||||
// Create instance
|
||||
object instance = CreateInstance(targetType);
|
||||
if (instance == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_FAILED_TO_CREATE_INSTANCE);
|
||||
}
|
||||
|
||||
// Check cancellation
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
// Execute method
|
||||
object executionResult = InvokeExecuteMethod(
|
||||
executeMethod,
|
||||
instance,
|
||||
context.Parameters,
|
||||
cancellationToken);
|
||||
|
||||
// Convert result to string
|
||||
string resultString = executionResult?.ToString() ?? "";
|
||||
|
||||
return CreateSuccessResult(resultString);
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
McpConstants.ERROR_MESSAGE_UNSUPPORTED_SIGNATURE,
|
||||
new List<string> { ex.Message });
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
// Retrieve actual exception
|
||||
Exception innerException = ex.InnerException ?? ex;
|
||||
if (innerException is OperationCanceledException)
|
||||
{
|
||||
throw innerException;
|
||||
}
|
||||
|
||||
return CreateErrorResult(
|
||||
innerException.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Target invocation exception: {innerException.Message}",
|
||||
$"Stack trace: {innerException.StackTrace}"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Execution exception: {ex.Message}",
|
||||
$"Stack trace: {ex.StackTrace}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ExecutionResult> ExecuteInternalAsync(ExecutionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context.CompiledAssembly == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_NO_COMPILED_ASSEMBLY);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Prefer async ExecuteAsync; fallback to sync Execute
|
||||
(Type asyncType, MethodInfo executeAsyncMethod) = _entryPointResolver.TryFindExecuteAsyncMethod(
|
||||
context.CompiledAssembly);
|
||||
if (asyncType != null && executeAsyncMethod != null)
|
||||
{
|
||||
object instance = CreateInstance(asyncType);
|
||||
if (instance == null)
|
||||
{
|
||||
return CreateErrorResult(McpConstants.ERROR_MESSAGE_FAILED_TO_CREATE_INSTANCE);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
object[] callArgs = BuildSupportedArguments(
|
||||
executeAsyncMethod,
|
||||
context.Parameters,
|
||||
cancellationToken);
|
||||
object invoked = executeAsyncMethod.Invoke(instance, callArgs);
|
||||
|
||||
object awaitedResult = await io.github.hatayama.uLoopMCP.AwaitableHelper.AwaitIfNeeded(invoked);
|
||||
string resultString = awaitedResult?.ToString() ?? "";
|
||||
|
||||
return CreateSuccessResult(resultString);
|
||||
}
|
||||
|
||||
// Fallback to sync path if no async method found
|
||||
ExecutionResult syncResult = ExecuteInternal(context, cancellationToken);
|
||||
return syncResult;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
Exception innerException = ex.InnerException ?? ex;
|
||||
if (innerException is OperationCanceledException)
|
||||
{
|
||||
throw innerException;
|
||||
}
|
||||
|
||||
return CreateErrorResult(
|
||||
innerException.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Target invocation exception: {innerException.Message}",
|
||||
$"Stack trace: {innerException.StackTrace}"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return CreateErrorResult(
|
||||
ex.Message,
|
||||
new List<string>
|
||||
{
|
||||
$"Execution exception: {ex.Message}",
|
||||
$"Stack trace: {ex.StackTrace}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static object[] BuildSupportedArguments(
|
||||
MethodInfo method,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
System.Reflection.ParameterInfo[] methodParameters = method.GetParameters();
|
||||
if (methodParameters.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (methodParameters.Length == 2 && methodParameters[0].ParameterType == typeof(Dictionary<string, object>) && methodParameters[1].ParameterType == typeof(CancellationToken))
|
||||
{
|
||||
return new object[] { parameters, cancellationToken };
|
||||
}
|
||||
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(Dictionary<string, object>))
|
||||
{
|
||||
return new object[] { parameters };
|
||||
}
|
||||
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(CancellationToken))
|
||||
{
|
||||
return new object[] { cancellationToken };
|
||||
}
|
||||
|
||||
throw new NotSupportedException(
|
||||
"Expected Execute/ExecuteAsync overloads with Dictionary<string,object> and/or CancellationToken");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87cbd690f09b04c29bf289ba05f5b22a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Assembly = System.Reflection.Assembly;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class CompiledCommandEntryPointResolver
|
||||
{
|
||||
private const string WrappedExecuteMethodName = "Execute";
|
||||
private const string WrappedExecuteAsyncMethodName = "ExecuteAsync";
|
||||
|
||||
public (Type targetType, MethodInfo executeMethod) TryFindExecuteMethod(Assembly assembly)
|
||||
{
|
||||
Type wrappedType = assembly.GetType(
|
||||
$"{DynamicCodeConstants.DEFAULT_NAMESPACE}.{DynamicCodeConstants.DEFAULT_CLASS_NAME}",
|
||||
false);
|
||||
if (wrappedType != null)
|
||||
{
|
||||
MethodInfo directMethod = FindPreferredExecuteMethod(wrappedType);
|
||||
if (directMethod != null)
|
||||
{
|
||||
return (wrappedType, directMethod);
|
||||
}
|
||||
}
|
||||
|
||||
Type[] types = assembly.GetTypes();
|
||||
foreach (Type type in types)
|
||||
{
|
||||
MethodInfo method = FindPreferredExecuteMethod(type);
|
||||
if (method != null)
|
||||
{
|
||||
return (type, method);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private static MethodInfo FindPreferredExecuteMethod(Type type)
|
||||
{
|
||||
MethodInfo methodWithParametersAndCancellation = type.GetMethod(
|
||||
WrappedExecuteMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new Type[] { typeof(Dictionary<string, object>), typeof(CancellationToken) },
|
||||
null);
|
||||
if (methodWithParametersAndCancellation != null)
|
||||
{
|
||||
return methodWithParametersAndCancellation;
|
||||
}
|
||||
|
||||
MethodInfo methodWithParameters = type.GetMethod(
|
||||
WrappedExecuteMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new Type[] { typeof(Dictionary<string, object>) },
|
||||
null);
|
||||
if (methodWithParameters != null)
|
||||
{
|
||||
return methodWithParameters;
|
||||
}
|
||||
|
||||
MethodInfo methodWithCancellation = type.GetMethod(
|
||||
WrappedExecuteMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new Type[] { typeof(CancellationToken) },
|
||||
null);
|
||||
if (methodWithCancellation != null)
|
||||
{
|
||||
return methodWithCancellation;
|
||||
}
|
||||
|
||||
return type.GetMethod(
|
||||
WrappedExecuteMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
}
|
||||
|
||||
public (Type targetType, MethodInfo executeAsyncMethod) TryFindExecuteAsyncMethod(Assembly assembly)
|
||||
{
|
||||
Type wrappedType = assembly.GetType(
|
||||
$"{DynamicCodeConstants.DEFAULT_NAMESPACE}.{DynamicCodeConstants.DEFAULT_CLASS_NAME}",
|
||||
false);
|
||||
if (wrappedType != null)
|
||||
{
|
||||
MethodInfo directMethod = FindPreferredExecuteAsyncMethod(wrappedType);
|
||||
if (directMethod != null)
|
||||
{
|
||||
return (wrappedType, directMethod);
|
||||
}
|
||||
}
|
||||
|
||||
Type[] types = assembly.GetTypes();
|
||||
foreach (Type type in types)
|
||||
{
|
||||
MethodInfo method = FindPreferredExecuteAsyncMethod(type);
|
||||
if (method != null)
|
||||
{
|
||||
return (type, method);
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private static MethodInfo FindPreferredExecuteAsyncMethod(Type type)
|
||||
{
|
||||
MethodInfo methodWithParametersAndCancellation = type.GetMethod(
|
||||
WrappedExecuteAsyncMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new Type[] { typeof(Dictionary<string, object>), typeof(CancellationToken) },
|
||||
null);
|
||||
if (methodWithParametersAndCancellation != null)
|
||||
{
|
||||
return methodWithParametersAndCancellation;
|
||||
}
|
||||
|
||||
MethodInfo methodWithParameters = type.GetMethod(
|
||||
WrappedExecuteAsyncMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new Type[] { typeof(Dictionary<string, object>) },
|
||||
null);
|
||||
if (methodWithParameters != null)
|
||||
{
|
||||
return methodWithParameters;
|
||||
}
|
||||
|
||||
MethodInfo methodWithCancellation = type.GetMethod(
|
||||
WrappedExecuteAsyncMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
new Type[] { typeof(CancellationToken) },
|
||||
null);
|
||||
if (methodWithCancellation != null)
|
||||
{
|
||||
return methodWithCancellation;
|
||||
}
|
||||
|
||||
return type.GetMethod(
|
||||
WrappedExecuteAsyncMethodName,
|
||||
BindingFlags.Public | BindingFlags.Instance,
|
||||
null,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfe00f80e2a534cbbbbe513d8a30c96c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides the runtime-facing facade for execute-dynamic-code.
|
||||
/// Entry points depend on this facade instead of reaching into factory and executor wiring directly.
|
||||
/// </summary>
|
||||
internal sealed class DynamicCodeExecutionFacade : IShutdownAwareDynamicCodeExecutionRuntime, IDisposable
|
||||
{
|
||||
private readonly ICompiledAssemblyBuilder _assemblyBuilder;
|
||||
private readonly IDynamicCodeExecutorPool _executorPool;
|
||||
private readonly DynamicCodeExecutionScheduler _executionScheduler;
|
||||
|
||||
public DynamicCodeExecutionFacade(
|
||||
ICompiledAssemblyBuilder assemblyBuilder,
|
||||
IDynamicCodeExecutorPool executorPool)
|
||||
{
|
||||
_assemblyBuilder = assemblyBuilder ?? throw new ArgumentNullException(nameof(assemblyBuilder));
|
||||
_executorPool = executorPool ?? throw new ArgumentNullException(nameof(executorPool));
|
||||
_executionScheduler = new DynamicCodeExecutionScheduler(_executorPool.Dispose);
|
||||
}
|
||||
|
||||
public bool SupportsAutoPrewarm()
|
||||
{
|
||||
_executionScheduler.ThrowIfDisposed();
|
||||
return _assemblyBuilder.SupportsAutoPrewarm();
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(
|
||||
DynamicCodeExecutionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(request != null, "request must not be null");
|
||||
Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty");
|
||||
|
||||
return await _executionScheduler.RunForegroundAsync(
|
||||
innerCancellationToken => ExecuteCoreAsync(request, innerCancellationToken),
|
||||
CreateExecutionInProgressResult,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<(bool Entered, ExecutionResult Result)> TryExecuteIfIdleAsync(
|
||||
DynamicCodeExecutionRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(request != null, "request must not be null");
|
||||
Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty");
|
||||
|
||||
return await _executionScheduler.TryRunIfIdleAsync(
|
||||
request.YieldToForegroundRequests,
|
||||
innerCancellationToken => ExecuteCoreAsync(request, innerCancellationToken),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<ExecutionResult> ExecuteCoreAsync(
|
||||
DynamicCodeExecutionRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Stopwatch executorAcquireStopwatch = Stopwatch.StartNew();
|
||||
IDynamicCodeExecutor executor = _executorPool.GetOrCreate(request.SecurityLevel);
|
||||
executorAcquireStopwatch.Stop();
|
||||
|
||||
Stopwatch executorTotalStopwatch = Stopwatch.StartNew();
|
||||
ExecutionResult result = await executor.ExecuteCodeAsync(
|
||||
request.Code,
|
||||
request.ClassName,
|
||||
request.Parameters,
|
||||
cancellationToken,
|
||||
request.CompileOnly);
|
||||
executorTotalStopwatch.Stop();
|
||||
|
||||
if (result.Timings == null)
|
||||
{
|
||||
result.Timings = new System.Collections.Generic.List<string>();
|
||||
}
|
||||
|
||||
result.Timings.Add($"[Perf] ExecutorAcquire: {executorAcquireStopwatch.Elapsed.TotalMilliseconds:F1}ms");
|
||||
result.Timings.Add($"[Perf] ExecutorTotal: {executorTotalStopwatch.Elapsed.TotalMilliseconds:F1}ms");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateExecutionInProgressResult()
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_IN_PROGRESS
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_executionScheduler.Dispose();
|
||||
}
|
||||
|
||||
public Task ShutdownAsync()
|
||||
{
|
||||
return _executionScheduler.ShutdownAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c6af8485a3554e7481dd34578b5cbfd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCodeExecutionRequest
|
||||
{
|
||||
public string Code { get; set; }
|
||||
|
||||
public string ClassName { get; set; } = DynamicCodeConstants.DEFAULT_CLASS_NAME;
|
||||
|
||||
public object[] Parameters { get; set; }
|
||||
|
||||
public bool CompileOnly { get; set; }
|
||||
|
||||
public DynamicCodeSecurityLevel SecurityLevel { get; set; }
|
||||
|
||||
public bool YieldToForegroundRequests { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 886bd709ab673421ab1fc75385f58560
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCodeExecutionScheduler : IDisposable
|
||||
{
|
||||
private const int BusyHandoffWindowMilliseconds = 50;
|
||||
private const int CancelledPrewarmHandoffWindowMilliseconds = 500;
|
||||
|
||||
private readonly Action _disposeResources;
|
||||
private readonly DynamicCodeExecutionSchedulerHooks _hooks;
|
||||
private readonly int _busyHandoffWindowMilliseconds;
|
||||
private readonly int _cancelledPrewarmHandoffWindowMilliseconds;
|
||||
private readonly SemaphoreSlim _executionSemaphore = new(1, 1);
|
||||
private readonly CancellationTokenSource _lifetimeCancellationTokenSource = new();
|
||||
private readonly TaskCompletionSource<bool> _shutdownCompletionSource =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly object _disposeLock = new();
|
||||
private readonly object _executionStateLock = new();
|
||||
private readonly object _backgroundPrewarmTransitionLock = new();
|
||||
private CancellationTokenSource _backgroundPrewarmCancellationTokenSource;
|
||||
private bool _backgroundPrewarmInProgress;
|
||||
private bool _resourcesDisposed;
|
||||
private bool _disposed;
|
||||
|
||||
public DynamicCodeExecutionScheduler(
|
||||
Action disposeResources,
|
||||
DynamicCodeExecutionSchedulerHooks hooks = null,
|
||||
int busyHandoffWindowMilliseconds = BusyHandoffWindowMilliseconds,
|
||||
int cancelledPrewarmHandoffWindowMilliseconds = CancelledPrewarmHandoffWindowMilliseconds)
|
||||
{
|
||||
_disposeResources = disposeResources ?? throw new ArgumentNullException(nameof(disposeResources));
|
||||
_hooks = hooks ?? new DynamicCodeExecutionSchedulerHooks();
|
||||
_busyHandoffWindowMilliseconds = busyHandoffWindowMilliseconds;
|
||||
_cancelledPrewarmHandoffWindowMilliseconds = cancelledPrewarmHandoffWindowMilliseconds;
|
||||
}
|
||||
|
||||
public void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(DynamicCodeExecutionScheduler));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> RunForegroundAsync<T>(
|
||||
Func<CancellationToken, Task<T>> action,
|
||||
Func<T> createBusyResult,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
bool entered = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!entered)
|
||||
{
|
||||
await _hooks.InvokeAfterBusySemaphoreProbeFailedAsync();
|
||||
if (!TryCancelBackgroundPrewarm())
|
||||
{
|
||||
entered = await TryAcquireAfterBusyHandoffAsync(cancellationToken);
|
||||
if (!entered)
|
||||
{
|
||||
return createBusyResult();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
entered = await TryAcquireAfterBusyHandoffAsync(
|
||||
cancellationToken,
|
||||
_cancelledPrewarmHandoffWindowMilliseconds);
|
||||
if (!entered)
|
||||
{
|
||||
return createBusyResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CancellationTokenSource executionCancellationTokenSource = null;
|
||||
try
|
||||
{
|
||||
_hooks.AfterSemaphoreEntered?.Invoke();
|
||||
ThrowIfDisposed();
|
||||
executionCancellationTokenSource =
|
||||
CreateExecutionCancellationTokenSource(cancellationToken);
|
||||
SetExecutionState(false, executionCancellationTokenSource);
|
||||
return await action(executionCancellationTokenSource.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClearExecutionState(false, executionCancellationTokenSource);
|
||||
executionCancellationTokenSource?.Dispose();
|
||||
DisposeResourcesIfRequested();
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Entered, T Result)> TryRunIfIdleAsync<T>(
|
||||
bool yieldToForegroundRequests,
|
||||
Func<CancellationToken, Task<T>> action,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!yieldToForegroundRequests)
|
||||
{
|
||||
return await TryRunWithoutForegroundYieldAsync(action, cancellationToken);
|
||||
}
|
||||
|
||||
bool entered;
|
||||
CancellationTokenSource executionCancellationTokenSource = null;
|
||||
lock (_backgroundPrewarmTransitionLock)
|
||||
{
|
||||
entered = _executionSemaphore.Wait(0);
|
||||
if (!entered)
|
||||
{
|
||||
return (false, default);
|
||||
}
|
||||
|
||||
ThrowIfDisposed();
|
||||
executionCancellationTokenSource =
|
||||
CreateExecutionCancellationTokenSource(cancellationToken);
|
||||
SetExecutionState(true, executionCancellationTokenSource);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _hooks.InvokeAfterBackgroundExecutionStatePublishedAsync();
|
||||
_hooks.AfterSemaphoreEntered?.Invoke();
|
||||
T result = await action(executionCancellationTokenSource.Token);
|
||||
return (true, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClearExecutionState(true, executionCancellationTokenSource);
|
||||
executionCancellationTokenSource?.Dispose();
|
||||
DisposeResourcesIfRequested();
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_lifetimeCancellationTokenSource.Cancel();
|
||||
if (!_executionSemaphore.Wait(0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DisposeResourcesIfRequested();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public Task ShutdownAsync()
|
||||
{
|
||||
Dispose();
|
||||
return _shutdownCompletionSource.Task;
|
||||
}
|
||||
|
||||
private async Task<(bool Entered, T Result)> TryRunWithoutForegroundYieldAsync<T>(
|
||||
Func<CancellationToken, Task<T>> action,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
bool entered = await _executionSemaphore.WaitAsync(0, cancellationToken);
|
||||
if (!entered)
|
||||
{
|
||||
return (false, default);
|
||||
}
|
||||
|
||||
CancellationTokenSource executionCancellationTokenSource = null;
|
||||
try
|
||||
{
|
||||
_hooks.AfterSemaphoreEntered?.Invoke();
|
||||
ThrowIfDisposed();
|
||||
executionCancellationTokenSource =
|
||||
CreateExecutionCancellationTokenSource(cancellationToken);
|
||||
SetExecutionState(false, executionCancellationTokenSource);
|
||||
T result = await action(executionCancellationTokenSource.Token);
|
||||
return (true, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClearExecutionState(false, executionCancellationTokenSource);
|
||||
executionCancellationTokenSource?.Dispose();
|
||||
DisposeResourcesIfRequested();
|
||||
_executionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCancelBackgroundPrewarm()
|
||||
{
|
||||
lock (_executionStateLock)
|
||||
{
|
||||
if (!_backgroundPrewarmInProgress || _backgroundPrewarmCancellationTokenSource == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_backgroundPrewarmCancellationTokenSource.Cancel();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryAcquireAfterBusyHandoffAsync(
|
||||
CancellationToken cancellationToken,
|
||||
int handoffWindowMilliseconds = -1)
|
||||
{
|
||||
if (handoffWindowMilliseconds < 0)
|
||||
{
|
||||
handoffWindowMilliseconds = _busyHandoffWindowMilliseconds;
|
||||
}
|
||||
|
||||
using CancellationTokenSource handoffCancellationTokenSource =
|
||||
CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
_lifetimeCancellationTokenSource.Token);
|
||||
handoffCancellationTokenSource.CancelAfter(handoffWindowMilliseconds);
|
||||
|
||||
try
|
||||
{
|
||||
await _executionSemaphore.WaitAsync(handoffCancellationTokenSource.Token);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested ||
|
||||
_lifetimeCancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private CancellationTokenSource CreateExecutionCancellationTokenSource(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken,
|
||||
_lifetimeCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private void SetExecutionState(
|
||||
bool yieldToForegroundRequests,
|
||||
CancellationTokenSource executionCancellationTokenSource)
|
||||
{
|
||||
lock (_executionStateLock)
|
||||
{
|
||||
_backgroundPrewarmInProgress = yieldToForegroundRequests;
|
||||
_backgroundPrewarmCancellationTokenSource = yieldToForegroundRequests
|
||||
? executionCancellationTokenSource
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearExecutionState(
|
||||
bool yieldToForegroundRequests,
|
||||
CancellationTokenSource executionCancellationTokenSource)
|
||||
{
|
||||
if (!yieldToForegroundRequests)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_executionStateLock)
|
||||
{
|
||||
if (!ReferenceEquals(_backgroundPrewarmCancellationTokenSource, executionCancellationTokenSource))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_backgroundPrewarmInProgress = false;
|
||||
_backgroundPrewarmCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeResourcesIfRequested()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_disposeLock)
|
||||
{
|
||||
if (_resourcesDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_resourcesDisposed = true;
|
||||
}
|
||||
|
||||
_disposeResources();
|
||||
_lifetimeCancellationTokenSource.Dispose();
|
||||
_shutdownCompletionSource.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e93fcf8a1218496bbcdf40f705298eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCodeExecutionSchedulerHooks
|
||||
{
|
||||
public Action AfterSemaphoreEntered { get; set; }
|
||||
|
||||
public Func<Task> AfterBackgroundExecutionStatePublishedAsync { get; set; }
|
||||
|
||||
public Func<Task> AfterBusySemaphoreProbeFailedAsync { get; set; }
|
||||
|
||||
public async Task InvokeAfterBackgroundExecutionStatePublishedAsync()
|
||||
{
|
||||
if (AfterBackgroundExecutionStatePublishedAsync == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await AfterBackgroundExecutionStatePublishedAsync();
|
||||
}
|
||||
|
||||
public async Task InvokeAfterBusySemaphoreProbeFailedAsync()
|
||||
{
|
||||
if (AfterBusySemaphoreProbeFailedAsync == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await AfterBusySemaphoreProbeFailedAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52481f12acef34843b63f02c25240a42
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,440 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Bridges compilation and execution so the tool layer can stay focused on request and response shaping.
|
||||
/// </summary>
|
||||
internal sealed class DynamicCodeExecutor : IDynamicCodeExecutor
|
||||
{
|
||||
private readonly IDynamicCompilationService _compiler;
|
||||
private readonly ICompiledCommandInvoker _invoker;
|
||||
private readonly DynamicCodeSourcePreparationService _sourcePreparationService;
|
||||
private readonly ExecutionStatistics _statistics;
|
||||
private readonly object _statsLock = new();
|
||||
|
||||
public DynamicCodeExecutor(
|
||||
IDynamicCompilationService compiler,
|
||||
ICompiledCommandInvoker invoker)
|
||||
: this(compiler, invoker, DynamicCodeServices.SourcePreparationService)
|
||||
{
|
||||
}
|
||||
|
||||
internal DynamicCodeExecutor(
|
||||
IDynamicCompilationService compiler,
|
||||
ICompiledCommandInvoker invoker,
|
||||
DynamicCodeSourcePreparationService sourcePreparationService)
|
||||
{
|
||||
_compiler = compiler ?? throw new ArgumentNullException(nameof(compiler));
|
||||
_invoker = invoker ?? throw new ArgumentNullException(nameof(invoker));
|
||||
_sourcePreparationService = sourcePreparationService ?? throw new ArgumentNullException(nameof(sourcePreparationService));
|
||||
_statistics = new ExecutionStatistics();
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteCodeAsync(
|
||||
string code,
|
||||
string className = DynamicCodeConstants.DEFAULT_CLASS_NAME,
|
||||
object[] parameters = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool compileOnly = false)
|
||||
{
|
||||
string correlationId = McpConstants.GenerateCorrelationId();
|
||||
Stopwatch totalStopwatch = Stopwatch.StartNew();
|
||||
Stopwatch sourcePreparationStopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
PreparedDynamicCode preparedCode = _sourcePreparationService.Prepare(
|
||||
code,
|
||||
DynamicCodeConstants.DEFAULT_NAMESPACE,
|
||||
className);
|
||||
sourcePreparationStopwatch.Stop();
|
||||
Stopwatch compileTotalStopwatch = Stopwatch.StartNew();
|
||||
CompilationResult compilationResult = await CompileCodeAsync(code, className, cancellationToken);
|
||||
compileTotalStopwatch.Stop();
|
||||
|
||||
ExecutionResult compilationFailureResult = TryCreateCompilationFailureResult(
|
||||
compilationResult,
|
||||
totalStopwatch);
|
||||
if (compilationFailureResult != null)
|
||||
{
|
||||
AppendExecutorStageTimings(
|
||||
compilationFailureResult.Timings,
|
||||
sourcePreparationStopwatch.Elapsed.TotalMilliseconds,
|
||||
compileTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return compilationFailureResult;
|
||||
}
|
||||
|
||||
if (compileOnly)
|
||||
{
|
||||
ExecutionResult compileOnlyResult = CreateCompileOnlySuccessResult(compilationResult, totalStopwatch.Elapsed);
|
||||
AppendExecutorStageTimings(
|
||||
compileOnlyResult.Timings,
|
||||
sourcePreparationStopwatch.Elapsed.TotalMilliseconds,
|
||||
compileTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
return compileOnlyResult;
|
||||
}
|
||||
|
||||
ExecutionContext context = new ExecutionContext
|
||||
{
|
||||
CompiledAssembly = compilationResult.CompiledAssembly,
|
||||
Parameters = BuildExecutionParameters(parameters, preparedCode.HoistedLiteralBindings),
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
|
||||
Stopwatch executionStopwatch = Stopwatch.StartNew();
|
||||
ExecutionResult executionResult = await _invoker.ExecuteAsync(context);
|
||||
executionStopwatch.Stop();
|
||||
|
||||
executionResult.ExecutionTime = totalStopwatch.Elapsed;
|
||||
executionResult.AutoInjectedNamespaces = compilationResult.AutoInjectedNamespaces;
|
||||
executionResult.Timings = MergeTimings(
|
||||
compilationResult.Timings,
|
||||
executionResult.Timings,
|
||||
$"[Perf] Execution: {executionStopwatch.Elapsed.TotalMilliseconds:F1}ms");
|
||||
AppendExecutorStageTimings(
|
||||
executionResult.Timings,
|
||||
sourcePreparationStopwatch.Elapsed.TotalMilliseconds,
|
||||
compileTotalStopwatch.Elapsed.TotalMilliseconds);
|
||||
AppendCompilationAdvisories(executionResult.Logs, compilationResult.AdvisoryLogs);
|
||||
|
||||
UpdateStatistics(executionResult, totalStopwatch.Elapsed);
|
||||
return executionResult;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
ExecutionResult cancelledResult = CreateCancelledResult(totalStopwatch.Elapsed);
|
||||
UpdateStatistics(cancelledResult, totalStopwatch.Elapsed);
|
||||
return cancelledResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExecutionResult failureResult = CreateUnexpectedErrorResult(ex, totalStopwatch.Elapsed);
|
||||
UpdateStatistics(failureResult, totalStopwatch.Elapsed);
|
||||
LogUnexpectedExecutionException(ex, correlationId, totalStopwatch.ElapsedMilliseconds);
|
||||
return failureResult;
|
||||
}
|
||||
}
|
||||
|
||||
public ExecutionStatistics GetStatistics()
|
||||
{
|
||||
lock (_statsLock)
|
||||
{
|
||||
return new ExecutionStatistics
|
||||
{
|
||||
TotalExecutions = _statistics.TotalExecutions,
|
||||
SuccessfulExecutions = _statistics.SuccessfulExecutions,
|
||||
FailedExecutions = _statistics.FailedExecutions,
|
||||
AverageExecutionTime = _statistics.AverageExecutionTime,
|
||||
SecurityViolations = _statistics.SecurityViolations,
|
||||
CompilationErrors = _statistics.CompilationErrors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_compiler is IDisposable disposableCompiler)
|
||||
{
|
||||
disposableCompiler.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CompilationResult> CompileCodeAsync(
|
||||
string code,
|
||||
string className,
|
||||
CancellationToken ct)
|
||||
{
|
||||
CompilationRequest request = new CompilationRequest
|
||||
{
|
||||
Code = code,
|
||||
ClassName = className,
|
||||
Namespace = DynamicCodeConstants.DEFAULT_NAMESPACE
|
||||
};
|
||||
|
||||
CompilationResult result = await _compiler.CompileAsync(request, ct);
|
||||
if (!result.Success)
|
||||
{
|
||||
lock (_statsLock)
|
||||
{
|
||||
_statistics.CompilationErrors++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ExecutionResult TryCreateCompilationFailureResult(
|
||||
CompilationResult compilationResult,
|
||||
Stopwatch totalStopwatch)
|
||||
{
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
if (compilationResult.HasSecurityViolations)
|
||||
{
|
||||
return CreateSecurityFailureResult(
|
||||
"Security violations detected",
|
||||
totalStopwatch.Elapsed,
|
||||
compilationResult.SecurityViolations,
|
||||
compilationResult.Timings);
|
||||
}
|
||||
|
||||
return CreateCompilationFailureResult(
|
||||
"Compilation error occurred",
|
||||
totalStopwatch.Elapsed,
|
||||
compilationResult);
|
||||
}
|
||||
|
||||
if (compilationResult.HasSecurityViolations)
|
||||
{
|
||||
return CreateSecurityFailureResult(
|
||||
"Security violations detected (Dangerous API call)",
|
||||
totalStopwatch.Elapsed,
|
||||
compilationResult.SecurityViolations,
|
||||
compilationResult.Timings);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateCompileOnlySuccessResult(
|
||||
CompilationResult compilationResult,
|
||||
TimeSpan executionTime)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = true,
|
||||
Result = null,
|
||||
ExecutionTime = executionTime,
|
||||
Logs = MergeCompilationLogs(
|
||||
new List<string> { "Code compiled successfully (no execution)" },
|
||||
compilationResult.AdvisoryLogs),
|
||||
AutoInjectedNamespaces = compilationResult.AutoInjectedNamespaces,
|
||||
Timings = CloneTimings(compilationResult.Timings)
|
||||
};
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateCancelledResult(TimeSpan executionTime)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = McpConstants.ERROR_MESSAGE_EXECUTION_CANCELLED,
|
||||
Logs = new List<string> { "Execution cancelled" },
|
||||
ExecutionTime = executionTime,
|
||||
Timings = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateSecurityFailureResult(
|
||||
string message,
|
||||
TimeSpan executionTime,
|
||||
List<SecurityViolation> violations,
|
||||
List<string> timings)
|
||||
{
|
||||
List<string> violationMessages = BuildViolationMessages(violations);
|
||||
if (violationMessages.Count > 0)
|
||||
{
|
||||
message = $"{message} {string.Join(" ", violationMessages)}";
|
||||
}
|
||||
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = message,
|
||||
Logs = violationMessages,
|
||||
ExecutionTime = executionTime,
|
||||
Timings = timings != null ? new List<string>(timings) : new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateCompilationFailureResult(
|
||||
string message,
|
||||
TimeSpan executionTime,
|
||||
CompilationResult compilationResult)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = message,
|
||||
Logs = MergeCompilationLogs(new List<string>(), compilationResult.AdvisoryLogs),
|
||||
ExecutionTime = executionTime,
|
||||
CompilationErrors = compilationResult.Errors,
|
||||
UpdatedCode = compilationResult.UpdatedCode,
|
||||
AmbiguousTypeCandidates = compilationResult.AmbiguousTypeCandidates,
|
||||
AutoInjectedNamespaces = compilationResult.AutoInjectedNamespaces,
|
||||
Timings = CloneTimings(compilationResult.Timings)
|
||||
};
|
||||
}
|
||||
|
||||
private static ExecutionResult CreateUnexpectedErrorResult(
|
||||
Exception ex,
|
||||
TimeSpan executionTime)
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "An unexpected error occurred during execution",
|
||||
Exception = ex,
|
||||
Logs = new List<string> { ex.ToString() },
|
||||
ExecutionTime = executionTime,
|
||||
Timings = new List<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private static List<string> BuildViolationMessages(List<SecurityViolation> violations)
|
||||
{
|
||||
List<string> violationMessages = new List<string>();
|
||||
if (violations == null)
|
||||
{
|
||||
return violationMessages;
|
||||
}
|
||||
|
||||
foreach (SecurityViolation violation in violations)
|
||||
{
|
||||
string violationMessage = !string.IsNullOrEmpty(violation.Message)
|
||||
? violation.Message
|
||||
: violation.Description;
|
||||
|
||||
if (!string.IsNullOrEmpty(violation.ApiName))
|
||||
{
|
||||
violationMessages.Add($"{violation.Type}: {violationMessage} (API: {violation.ApiName})");
|
||||
continue;
|
||||
}
|
||||
|
||||
violationMessages.Add($"{violation.Type}: {violationMessage}");
|
||||
}
|
||||
|
||||
return violationMessages;
|
||||
}
|
||||
|
||||
private static List<string> MergeCompilationLogs(
|
||||
List<string> logs,
|
||||
List<string> advisoryLogs)
|
||||
{
|
||||
List<string> mergedLogs = logs ?? new List<string>();
|
||||
AppendCompilationAdvisories(mergedLogs, advisoryLogs);
|
||||
return mergedLogs;
|
||||
}
|
||||
|
||||
private static void AppendCompilationAdvisories(
|
||||
List<string> destination,
|
||||
List<string> advisoryLogs)
|
||||
{
|
||||
if (destination == null || advisoryLogs == null || advisoryLogs.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
destination.AddRange(advisoryLogs);
|
||||
}
|
||||
|
||||
private void UpdateStatistics(ExecutionResult result, TimeSpan executionTime)
|
||||
{
|
||||
lock (_statsLock)
|
||||
{
|
||||
_statistics.TotalExecutions++;
|
||||
if (result.Success)
|
||||
{
|
||||
_statistics.SuccessfulExecutions++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_statistics.FailedExecutions++;
|
||||
}
|
||||
|
||||
double totalMilliseconds =
|
||||
_statistics.AverageExecutionTime.TotalMilliseconds * (_statistics.TotalExecutions - 1);
|
||||
totalMilliseconds += executionTime.TotalMilliseconds;
|
||||
_statistics.AverageExecutionTime =
|
||||
TimeSpan.FromMilliseconds(totalMilliseconds / _statistics.TotalExecutions);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> BuildExecutionParameters(
|
||||
object[] parameters,
|
||||
IReadOnlyCollection<HoistedLiteralBinding> hoistedLiteralBindings)
|
||||
{
|
||||
Dictionary<string, object> executionParameters = new Dictionary<string, object>();
|
||||
|
||||
if (hoistedLiteralBindings != null)
|
||||
{
|
||||
foreach (HoistedLiteralBinding binding in hoistedLiteralBindings)
|
||||
{
|
||||
executionParameters[binding.ParameterName] = binding.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (parameters != null)
|
||||
{
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
executionParameters[$"param{i}"] = parameters[i];
|
||||
}
|
||||
}
|
||||
|
||||
return executionParameters;
|
||||
}
|
||||
|
||||
private static List<string> MergeTimings(
|
||||
List<string> compilationTimings,
|
||||
List<string> executionTimings,
|
||||
string executionEntry)
|
||||
{
|
||||
List<string> mergedTimings = new List<string>();
|
||||
if (compilationTimings != null)
|
||||
{
|
||||
mergedTimings.AddRange(compilationTimings);
|
||||
}
|
||||
|
||||
if (executionTimings != null)
|
||||
{
|
||||
mergedTimings.AddRange(executionTimings);
|
||||
}
|
||||
|
||||
mergedTimings.Add(executionEntry);
|
||||
return mergedTimings;
|
||||
}
|
||||
|
||||
private static void AppendExecutorStageTimings(
|
||||
List<string> timings,
|
||||
double sourcePreparationMilliseconds,
|
||||
double compileTotalMilliseconds)
|
||||
{
|
||||
if (timings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timings.Add($"[Perf] SourcePrepare: {sourcePreparationMilliseconds:F1}ms");
|
||||
timings.Add($"[Perf] CompileTotal: {compileTotalMilliseconds:F1}ms");
|
||||
}
|
||||
|
||||
private static List<string> CloneTimings(List<string> timings)
|
||||
{
|
||||
return timings != null ? new List<string>(timings) : new List<string>();
|
||||
}
|
||||
|
||||
private static void LogUnexpectedExecutionException(
|
||||
Exception ex,
|
||||
string correlationId,
|
||||
long executionTimeMilliseconds)
|
||||
{
|
||||
VibeLogger.LogError(
|
||||
"execute_code_exception",
|
||||
"Unexpected error during code execution",
|
||||
new
|
||||
{
|
||||
exception_type = ex.GetType().Name,
|
||||
execution_time_ms = executionTimeMilliseconds
|
||||
},
|
||||
correlationId,
|
||||
"Dynamic Code Execution Error",
|
||||
"Investigating Unexpected Exception");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e3877040f39e48859d7c5ff4ba2ee9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using io.github.hatayama.uLoopMCP.Factory;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal sealed class DynamicCodeExecutorPool : IDynamicCodeExecutorPool
|
||||
{
|
||||
private readonly IDynamicCodeExecutorProvider _executorProvider;
|
||||
private readonly Dictionary<DynamicCodeSecurityLevel, IDynamicCodeExecutor> _executorsBySecurityLevel = new();
|
||||
private readonly object _executorsLock = new();
|
||||
private bool _disposed;
|
||||
|
||||
public DynamicCodeExecutorPool(IDynamicCodeExecutorProvider executorProvider)
|
||||
{
|
||||
_executorProvider = executorProvider ?? throw new ArgumentNullException(nameof(executorProvider));
|
||||
}
|
||||
|
||||
public IDynamicCodeExecutor GetOrCreate(DynamicCodeSecurityLevel securityLevel)
|
||||
{
|
||||
lock (_executorsLock)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (_executorsBySecurityLevel.TryGetValue(securityLevel, out IDynamicCodeExecutor executor))
|
||||
{
|
||||
return executor;
|
||||
}
|
||||
|
||||
IDynamicCodeExecutor createdExecutor = _executorProvider.Create(securityLevel);
|
||||
if (createdExecutor is DynamicCodeExecutorStub)
|
||||
{
|
||||
return createdExecutor;
|
||||
}
|
||||
|
||||
_executorsBySecurityLevel.Add(securityLevel, createdExecutor);
|
||||
return createdExecutor;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_executorsLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
foreach (IDynamicCodeExecutor executor in _executorsBySecurityLevel.Values)
|
||||
{
|
||||
executor.Dispose();
|
||||
}
|
||||
|
||||
_executorsBySecurityLevel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(DynamicCodeExecutorPool));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efc1a63dcd8bd4709a312dae272824fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Fallback stub when no compilation provider is registered.
|
||||
/// Returns an error for all execution attempts.
|
||||
/// </summary>
|
||||
public class DynamicCodeExecutorStub : IDynamicCodeExecutor
|
||||
{
|
||||
private readonly ExecutionStatistics _statistics;
|
||||
|
||||
public DynamicCodeExecutorStub()
|
||||
{
|
||||
_statistics = new ExecutionStatistics();
|
||||
}
|
||||
|
||||
/// <summary>Execute code asynchronously (always returns a Roslyn required error)</summary>
|
||||
public Task<ExecutionResult> ExecuteCodeAsync(
|
||||
string code,
|
||||
string className = DynamicCodeConstants.DEFAULT_CLASS_NAME,
|
||||
object[] parameters = null,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool compileOnly = false)
|
||||
{
|
||||
return Task.FromResult(CreateCompilationProviderUnavailableResult());
|
||||
}
|
||||
|
||||
/// <summary>Retrieve execution statistics</summary>
|
||||
public ExecutionStatistics GetStatistics()
|
||||
{
|
||||
return new ExecutionStatistics
|
||||
{
|
||||
TotalExecutions = _statistics.TotalExecutions,
|
||||
SuccessfulExecutions = _statistics.SuccessfulExecutions,
|
||||
FailedExecutions = _statistics.FailedExecutions,
|
||||
AverageExecutionTime = _statistics.AverageExecutionTime,
|
||||
SecurityViolations = _statistics.SecurityViolations,
|
||||
CompilationErrors = _statistics.CompilationErrors
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private ExecutionResult CreateCompilationProviderUnavailableResult()
|
||||
{
|
||||
return new ExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "COMPILATION_PROVIDER_UNAVAILABLE: No compilation provider is registered. Check initialization.",
|
||||
ExecutionTime = TimeSpan.Zero,
|
||||
Statistics = _statistics
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b527905c494c4f658cf6aa696fd05ba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,96 @@
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicCodeForegroundWarmupState
|
||||
{
|
||||
private static readonly object SyncRoot = new();
|
||||
private static ForegroundWarmupStatus _status = ForegroundWarmupStatus.Pending;
|
||||
|
||||
internal static bool TryBegin()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (_status != ForegroundWarmupStatus.Pending)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_status = ForegroundWarmupStatus.Running;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void MarkCompleted()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
if (_status == ForegroundWarmupStatus.Completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.Assert(
|
||||
_status == ForegroundWarmupStatus.Running,
|
||||
"Foreground warmup must only complete after it has started.");
|
||||
_status = ForegroundWarmupStatus.Completed;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void MarkCompletedByBackgroundWarmup()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
// Why: startup and recovery already run their own hidden prewarm before users see the
|
||||
// first foreground request, so that successful background path should satisfy the
|
||||
// one-shot fallback as well.
|
||||
// Why not force the foreground fallback to run anyway: that makes warmed startups pay
|
||||
// a second hidden execute-dynamic-code request and pushes the first user-visible call
|
||||
// back into the hundreds of milliseconds we already worked to remove.
|
||||
_status = ForegroundWarmupStatus.Completed;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void MarkCompletedByForegroundExecution()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
// Why: a real foreground execution succeeding after a transient hidden-warmup miss
|
||||
// proves the user-visible path is already usable for the next request.
|
||||
// Why not insist on the hidden warmup succeeding first: that keeps Pending alive
|
||||
// after the exact success case we care about and injects another needless warmup.
|
||||
_status = ForegroundWarmupStatus.Completed;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetAfterIncompleteAttempt()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
// Why: a failed hidden warmup leaves the next foreground execution cold, so the next
|
||||
// request still needs one more chance to pay that startup tax before users do.
|
||||
// Why not mark every attempt as completed: that would permanently skip the fallback
|
||||
// after a transient startup failure and bring back the slow first real execution.
|
||||
if (_status == ForegroundWarmupStatus.Completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_status = ForegroundWarmupStatus.Pending;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Reset()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_status = ForegroundWarmupStatus.Pending;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ForegroundWarmupStatus
|
||||
{
|
||||
Pending,
|
||||
Running,
|
||||
Completed
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0526ab1aadb530f45ad38b57cd8872fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal static class DynamicCodeStartupTelemetry
|
||||
{
|
||||
private static readonly object SyncRoot = new();
|
||||
|
||||
private static long _serverReadyTimestamp;
|
||||
private static long _prewarmStateTimestamp;
|
||||
private static long _prewarmStartedTimestamp;
|
||||
private static long _prewarmFinishedTimestamp;
|
||||
private static string _prewarmState = "NotRequested";
|
||||
private static string _prewarmDetail = string.Empty;
|
||||
|
||||
public static void MarkServerReady()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_serverReadyTimestamp = Stopwatch.GetTimestamp();
|
||||
_prewarmStateTimestamp = 0;
|
||||
_prewarmStartedTimestamp = 0;
|
||||
_prewarmFinishedTimestamp = 0;
|
||||
_prewarmState = "NotRequested";
|
||||
_prewarmDetail = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public static void MarkPrewarmQueued()
|
||||
{
|
||||
UpdatePrewarmState("Queued", string.Empty, false);
|
||||
}
|
||||
|
||||
public static void MarkPrewarmStarted()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_prewarmStartedTimestamp = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
UpdatePrewarmState("Running", string.Empty, false);
|
||||
}
|
||||
|
||||
public static void MarkPrewarmCompleted()
|
||||
{
|
||||
UpdatePrewarmState("Completed", string.Empty, true);
|
||||
}
|
||||
|
||||
public static void MarkPrewarmSkipped(string detail)
|
||||
{
|
||||
UpdatePrewarmState("Skipped", detail, true);
|
||||
}
|
||||
|
||||
public static void MarkPrewarmYielded(string detail)
|
||||
{
|
||||
UpdatePrewarmState("Yielded", detail, true);
|
||||
}
|
||||
|
||||
public static void MarkPrewarmFailed(string detail)
|
||||
{
|
||||
UpdatePrewarmState("Failed", detail, true);
|
||||
}
|
||||
|
||||
public static List<string> CreateTimingEntries()
|
||||
{
|
||||
long serverReadyTimestamp;
|
||||
long prewarmStateTimestamp;
|
||||
long prewarmStartedTimestamp;
|
||||
long prewarmFinishedTimestamp;
|
||||
string prewarmState;
|
||||
string prewarmDetail;
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
serverReadyTimestamp = _serverReadyTimestamp;
|
||||
prewarmStateTimestamp = _prewarmStateTimestamp;
|
||||
prewarmStartedTimestamp = _prewarmStartedTimestamp;
|
||||
prewarmFinishedTimestamp = _prewarmFinishedTimestamp;
|
||||
prewarmState = _prewarmState;
|
||||
prewarmDetail = _prewarmDetail;
|
||||
}
|
||||
|
||||
List<string> entries = new List<string>();
|
||||
if (serverReadyTimestamp > 0)
|
||||
{
|
||||
entries.Add($"[Perf] ServerReadyAge: {ToMilliseconds(serverReadyTimestamp, Stopwatch.GetTimestamp()):F1}ms");
|
||||
}
|
||||
|
||||
entries.Add($"[Perf] WarmReady: {string.Equals(prewarmState, "Completed")}");
|
||||
entries.Add($"[Perf] PrewarmState: {prewarmState}");
|
||||
|
||||
if (prewarmStateTimestamp > 0)
|
||||
{
|
||||
entries.Add($"[Perf] PrewarmStateAge: {ToMilliseconds(prewarmStateTimestamp, Stopwatch.GetTimestamp()):F1}ms");
|
||||
}
|
||||
|
||||
if (prewarmStartedTimestamp > 0 && prewarmFinishedTimestamp > 0)
|
||||
{
|
||||
entries.Add($"[Perf] PrewarmDuration: {ToMilliseconds(prewarmStartedTimestamp, prewarmFinishedTimestamp):F1}ms");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(prewarmDetail))
|
||||
{
|
||||
entries.Add($"[Perf] PrewarmDetail: {prewarmDetail}");
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
internal static void Reset()
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_serverReadyTimestamp = 0;
|
||||
_prewarmStateTimestamp = 0;
|
||||
_prewarmStartedTimestamp = 0;
|
||||
_prewarmFinishedTimestamp = 0;
|
||||
_prewarmState = "NotRequested";
|
||||
_prewarmDetail = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdatePrewarmState(string state, string detail, bool terminal)
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
_prewarmState = state;
|
||||
_prewarmDetail = detail ?? string.Empty;
|
||||
_prewarmStateTimestamp = Stopwatch.GetTimestamp();
|
||||
if (terminal)
|
||||
{
|
||||
_prewarmFinishedTimestamp = _prewarmStateTimestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
_prewarmFinishedTimestamp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static double ToMilliseconds(long startTimestamp, long endTimestamp)
|
||||
{
|
||||
long elapsedTicks = endTimestamp - startTimestamp;
|
||||
return elapsedTicks * 1000d / Stopwatch.Frequency;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 599aef35be3c04521875f8da755af486
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface ICompiledCommandInvoker
|
||||
{
|
||||
Task<ExecutionResult> ExecuteAsync(ExecutionContext context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4050a9c7cf29d48d6a66b387e05b426a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface IDynamicCodeExecutionRuntime
|
||||
{
|
||||
bool SupportsAutoPrewarm();
|
||||
|
||||
Task<ExecutionResult> ExecuteAsync(
|
||||
DynamicCodeExecutionRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<(bool Entered, ExecutionResult Result)> TryExecuteIfIdleAsync(
|
||||
DynamicCodeExecutionRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d59eb9afd26542f1b81f6c5e9b25351
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface IDynamicCodeExecutorPool : IDisposable
|
||||
{
|
||||
IDynamicCodeExecutor GetOrCreate(DynamicCodeSecurityLevel securityLevel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e8a43b23ac844b3682caf7578b7a248
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace io.github.hatayama.uLoopMCP
|
||||
{
|
||||
internal interface IShutdownAwareDynamicCodeExecutionRuntime : IDynamicCodeExecutionRuntime
|
||||
{
|
||||
Task ShutdownAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f1dc8c5e37f647b584a09de3f269b0f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user