manage_scene: tolerant params + optional buildIndex; add writer IO logs; keep direct write path
parent
397ba32a99
commit
2fd74f5dab
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
@ -25,6 +26,14 @@ namespace MCPForUnity.Editor
|
||||||
private static readonly object startStopLock = new();
|
private static readonly object startStopLock = new();
|
||||||
private static readonly object clientsLock = new();
|
private static readonly object clientsLock = new();
|
||||||
private static readonly System.Collections.Generic.HashSet<TcpClient> activeClients = new();
|
private static readonly System.Collections.Generic.HashSet<TcpClient> activeClients = new();
|
||||||
|
// Single-writer outbox for framed responses
|
||||||
|
private class Outbound
|
||||||
|
{
|
||||||
|
public byte[] Payload;
|
||||||
|
public string Tag;
|
||||||
|
public int? ReqId;
|
||||||
|
}
|
||||||
|
private static readonly BlockingCollection<Outbound> _outbox = new(new ConcurrentQueue<Outbound>());
|
||||||
private static CancellationTokenSource cts;
|
private static CancellationTokenSource cts;
|
||||||
private static Task listenerTask;
|
private static Task listenerTask;
|
||||||
private static int processingCommands = 0;
|
private static int processingCommands = 0;
|
||||||
|
|
@ -44,6 +53,10 @@ namespace MCPForUnity.Editor
|
||||||
private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads
|
private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads
|
||||||
private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients
|
private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients
|
||||||
|
|
||||||
|
// IO diagnostics
|
||||||
|
private static long _ioSeq = 0;
|
||||||
|
private static void IoInfo(string s) { McpLog.Info(s, always: false); }
|
||||||
|
|
||||||
// Debug helpers
|
// Debug helpers
|
||||||
private static bool IsDebugEnabled()
|
private static bool IsDebugEnabled()
|
||||||
{
|
{
|
||||||
|
|
@ -112,6 +125,35 @@ namespace MCPForUnity.Editor
|
||||||
{
|
{
|
||||||
// Record the main thread ID for safe thread checks
|
// Record the main thread ID for safe thread checks
|
||||||
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
|
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
|
||||||
|
// Start single writer thread for framed responses
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var writerThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
foreach (var item in _outbox.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
long seq = Interlocked.Increment(ref _ioSeq);
|
||||||
|
IoInfo($"[IO] ➜ write start seq={seq} tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")}");
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
// Note: We currently have a per-connection 'stream' in the client handler. For simplicity,
|
||||||
|
// writes are performed inline there. This outbox provides single-writer semantics; if a shared
|
||||||
|
// stream is introduced, redirect here accordingly.
|
||||||
|
// No-op: actual write happens in client loop using WriteFrameAsync
|
||||||
|
sw.Stop();
|
||||||
|
IoInfo($"[IO] ✓ write end tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")} durMs={sw.Elapsed.TotalMilliseconds:F1}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) { IsBackground = true, Name = "MCP-Writer" };
|
||||||
|
writerThread.Start();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
// Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env
|
// Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env
|
||||||
// CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode
|
// CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode
|
||||||
if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH")))
|
if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH")))
|
||||||
|
|
@ -579,8 +621,32 @@ namespace MCPForUnity.Editor
|
||||||
{
|
{
|
||||||
try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { }
|
try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { }
|
||||||
}
|
}
|
||||||
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
// Crash-proof and self-reporting writer logs (direct write to this client's stream)
|
||||||
|
long seq = System.Threading.Interlocked.Increment(ref _ioSeq);
|
||||||
|
byte[] responseBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
||||||
|
IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var swDirect = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
await WriteFrameAsync(stream, responseBytes);
|
await WriteFrameAsync(stream, responseBytes);
|
||||||
|
swDirect.Stop();
|
||||||
|
IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,46 @@ namespace MCPForUnity.Editor.Tools
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ManageScene
|
public static class ManageScene
|
||||||
{
|
{
|
||||||
|
private sealed class SceneCommand
|
||||||
|
{
|
||||||
|
public string action { get; set; } = string.Empty;
|
||||||
|
public string name { get; set; } = string.Empty;
|
||||||
|
public string path { get; set; } = string.Empty;
|
||||||
|
public int? buildIndex { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SceneCommand ToSceneCommand(JObject p)
|
||||||
|
{
|
||||||
|
if (p == null) return new SceneCommand();
|
||||||
|
int? BI(JToken t)
|
||||||
|
{
|
||||||
|
if (t == null || t.Type == JTokenType.Null) return null;
|
||||||
|
var s = t.ToString().Trim();
|
||||||
|
if (s.Length == 0) return null;
|
||||||
|
if (int.TryParse(s, out var i)) return i;
|
||||||
|
if (double.TryParse(s, out var d)) return (int)d;
|
||||||
|
return t.Type == JTokenType.Integer ? t.Value<int>() : (int?)null;
|
||||||
|
}
|
||||||
|
return new SceneCommand
|
||||||
|
{
|
||||||
|
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
|
||||||
|
name = p["name"]?.ToString() ?? string.Empty,
|
||||||
|
path = p["path"]?.ToString() ?? string.Empty,
|
||||||
|
buildIndex = BI(p["buildIndex"] ?? p["build_index"])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main handler for scene management actions.
|
/// Main handler for scene management actions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static object HandleCommand(JObject @params)
|
public static object HandleCommand(JObject @params)
|
||||||
{
|
{
|
||||||
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
|
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
|
||||||
string action = @params["action"]?.ToString().ToLower();
|
var cmd = ToSceneCommand(@params);
|
||||||
string name = @params["name"]?.ToString();
|
string action = cmd.action;
|
||||||
string path = @params["path"]?.ToString(); // Relative to Assets/
|
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
|
||||||
int? buildIndex = @params["buildIndex"]?.ToObject<int?>();
|
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
|
||||||
|
int? buildIndex = cmd.buildIndex;
|
||||||
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
|
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
|
||||||
|
|
||||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ def register_manage_scene_tools(mcp: FastMCP):
|
||||||
def manage_scene(
|
def manage_scene(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: str,
|
action: str,
|
||||||
name: str,
|
name: str = "",
|
||||||
path: str,
|
path: str = "",
|
||||||
build_index: Any,
|
build_index: Any = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
|
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
|
||||||
|
|
||||||
|
|
@ -47,15 +47,15 @@ def register_manage_scene_tools(mcp: FastMCP):
|
||||||
except Exception:
|
except Exception:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
build_index = _coerce_int(build_index, default=0)
|
coerced_build_index = _coerce_int(build_index, default=None)
|
||||||
|
|
||||||
params = {
|
params = {"action": action}
|
||||||
"action": action,
|
if name:
|
||||||
"name": name,
|
params["name"] = name
|
||||||
"path": path,
|
if path:
|
||||||
"buildIndex": build_index
|
params["path"] = path
|
||||||
}
|
if coerced_build_index is not None:
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
params["buildIndex"] = coerced_build_index
|
||||||
|
|
||||||
# Use centralized retry helper
|
# Use centralized retry helper
|
||||||
response = send_command_with_retry("manage_scene", params)
|
response = send_command_with_retry("manage_scene", params)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue