diff --git a/Editor/DI/ServiceContainer.cs b/Editor/DI/ServiceContainer.cs
index 21522e7..d9aac16 100644
--- a/Editor/DI/ServiceContainer.cs
+++ b/Editor/DI/ServiceContainer.cs
@@ -208,8 +208,9 @@ public void Dispose()
{
lock (_lock)
{
- foreach (var disposable in _disposables)
+ for (var i = _disposables.Count - 1; i >= 0; i--)
{
+ var disposable = _disposables[i];
try { disposable.Dispose(); } catch { }
}
_disposables.Clear();
@@ -319,8 +320,9 @@ public void Dispose()
{
lock (_lock)
{
- foreach (var disposable in _disposables)
+ for (var i = _disposables.Count - 1; i >= 0; i--)
{
+ var disposable = _disposables[i];
try { disposable.Dispose(); } catch { }
}
_disposables.Clear();
diff --git a/Editor/MCP/Server/HttpMCPTransport.cs b/Editor/MCP/Server/HttpMCPTransport.cs
index 39457e7..aca2e36 100644
--- a/Editor/MCP/Server/HttpMCPTransport.cs
+++ b/Editor/MCP/Server/HttpMCPTransport.cs
@@ -4,7 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
-using System.Net.Http;
+using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -14,31 +14,26 @@
namespace Funplay.Editor.MCP.Server
{
///
- /// HTTP transport implementation for MCP using System.Net.HttpListener.
+ /// HTTP transport implementation for MCP using a loopback TCP listener.
/// Listens for JSON-RPC requests over HTTP.
///
internal class HttpMCPTransport : IMCPTransport
{
- private HttpListener _listener;
+ private TcpListener _listener;
private CancellationTokenSource _cts;
private readonly int _port;
- private readonly string _expectedServerName;
- private readonly string _expectedProjectIdentity;
private bool _isRunning;
- private bool _ownsListener;
private const int StartRetryAttempts = 40;
private const int StartRetryDelayMs = 250;
- private const int ExistingServerProbeTimeoutMs = 500;
+ private const int MaxHeaderBytes = 64 * 1024;
public bool IsRunning => _isRunning;
- public bool IsAttachedToExistingServer => _isRunning && !_ownsListener;
+ public bool IsAttachedToExistingServer => false;
public event Action> OnRequestReceived;
public HttpMCPTransport(int port, string expectedServerName = null, string expectedProjectIdentity = null)
{
_port = port;
- _expectedServerName = expectedServerName;
- _expectedProjectIdentity = expectedProjectIdentity;
}
public async Task StartAsync(CancellationToken ct = default)
@@ -51,14 +46,12 @@ public async Task StartAsync(CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
- _listener = new HttpListener();
- _listener.Prefixes.Add($"http://127.0.0.1:{_port}/");
- _listener.Prefixes.Add($"http://localhost:{_port}/");
+ _listener = new TcpListener(IPAddress.Loopback, _port);
+ _listener.Server.NoDelay = true;
_listener.Start();
_cts = new CancellationTokenSource();
_isRunning = true;
- _ownsListener = true;
_ = Task.Run(() => ListenLoopAsync(_cts.Token), _cts.Token);
@@ -74,9 +67,6 @@ public async Task StartAsync(CancellationToken ct = default)
catch (Exception ex) when (IsAddressInUse(ex))
{
CleanupFailedStart();
- if (await TryAttachToExistingFunplayServerAsync(ct))
- return true;
-
if (attempt >= StartRetryAttempts)
{
Debug.LogError($"[Funplay MCP Server] Failed to start HTTP transport: {ex.Message}");
@@ -103,7 +93,6 @@ public async Task StartAsync(CancellationToken ct = default)
}
_isRunning = false;
- _ownsListener = false;
return false;
}
@@ -120,17 +109,10 @@ public void Stop()
try
{
- if (_ownsListener)
- {
- _cts?.Cancel();
- _listener?.Stop();
- _listener?.Close();
- PluginDebugLogger.Log("[Funplay MCP Server] HTTP transport stopped");
- }
- else if (_isRunning)
- {
- PluginDebugLogger.Log("[Funplay MCP Server] Detached from existing HTTP transport");
- }
+ _isRunning = false;
+ _cts?.Cancel();
+ CloseListener();
+ PluginDebugLogger.Log("[Funplay MCP Server] HTTP transport stopped");
}
catch (ObjectDisposedException)
{
@@ -142,8 +124,6 @@ public void Stop()
}
finally
{
- _isRunning = false;
- _ownsListener = false;
_listener = null;
_cts?.Dispose();
_cts = null;
@@ -154,7 +134,8 @@ private void CleanupFailedStart()
{
try
{
- _listener?.Close();
+ _isRunning = false;
+ CloseListener();
}
catch
{
@@ -163,7 +144,6 @@ private void CleanupFailedStart()
finally
{
_listener = null;
- _ownsListener = false;
}
}
@@ -180,228 +160,232 @@ private static async Task DelayBeforeRetryAsync(CancellationToken ct)
}
}
- private async Task TryAttachToExistingFunplayServerAsync(CancellationToken ct)
- {
- if (string.IsNullOrEmpty(_expectedServerName) || string.IsNullOrEmpty(_expectedProjectIdentity))
- return false;
-
- try
- {
- var responseText = await SendInitializeProbeAsync(ct);
- if (!IsExpectedFunplayServer(responseText, _expectedServerName, _expectedProjectIdentity))
- return false;
-
- _isRunning = true;
- _ownsListener = false;
- PluginDebugLogger.Log(
- $"[Funplay MCP Server] Reusing existing HTTP transport on http://127.0.0.1:{_port}/");
- return true;
- }
- catch (OperationCanceledException)
- {
- return false;
- }
- catch
- {
- return false;
- }
- }
-
- private async Task SendInitializeProbeAsync(CancellationToken ct)
- {
- const string body = "{\"jsonrpc\":\"2.0\",\"id\":\"funplay-existing-server-probe\",\"method\":\"initialize\",\"params\":{}}";
- try
- {
- using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct))
- using (var client = new HttpClient { Timeout = TimeSpan.FromMilliseconds(ExistingServerProbeTimeoutMs) })
- using (var content = new StringContent(body, Encoding.UTF8, "application/json"))
- {
- timeoutCts.CancelAfter(ExistingServerProbeTimeoutMs);
- var response = await client.PostAsync($"http://127.0.0.1:{_port}/", content, timeoutCts.Token);
- if (response.StatusCode != HttpStatusCode.OK)
- return string.Empty;
-
- return await response.Content.ReadAsStringAsync();
- }
- }
- catch (OperationCanceledException) when (!ct.IsCancellationRequested)
- {
- return string.Empty;
- }
- catch (HttpRequestException)
- {
- return string.Empty;
- }
- }
-
- private static bool IsExpectedFunplayServer(
- string responseText,
- string expectedServerName,
- string expectedProjectIdentity)
- {
- if (string.IsNullOrEmpty(responseText) ||
- string.IsNullOrEmpty(expectedServerName) ||
- string.IsNullOrEmpty(expectedProjectIdentity))
- {
- return false;
- }
-
- var response = SimpleJsonHelper.Deserialize(responseText) as Dictionary;
- var result = GetDictionary(response, "result");
- var serverInfo = GetDictionary(result, "serverInfo");
- var funplayInfo = GetDictionary(result, "funplay");
- var serverName = GetString(serverInfo, "name");
- var projectIdentity = GetString(funplayInfo, "projectIdentity");
-
- return string.Equals(serverName, expectedServerName, StringComparison.Ordinal) &&
- string.Equals(projectIdentity, expectedProjectIdentity, StringComparison.Ordinal);
- }
-
- private static Dictionary GetDictionary(Dictionary source, string key)
- {
- if (source != null &&
- source.TryGetValue(key, out var value) &&
- value is Dictionary dictionary)
- {
- return dictionary;
- }
-
- return null;
- }
-
- private static string GetString(Dictionary source, string key)
- {
- return source != null && source.TryGetValue(key, out var value)
- ? value?.ToString()
- : null;
- }
-
private static bool IsAddressInUse(Exception ex)
{
var message = ex?.Message ?? string.Empty;
if (message.IndexOf("Only one usage", StringComparison.OrdinalIgnoreCase) >= 0 ||
- message.IndexOf("Address already in use", StringComparison.OrdinalIgnoreCase) >= 0 ||
- message.IndexOf("another listener", StringComparison.OrdinalIgnoreCase) >= 0 ||
- message.IndexOf("prefix is already registered", StringComparison.OrdinalIgnoreCase) >= 0)
+ message.IndexOf("Address already in use", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
- return ex is HttpListenerException listenerException &&
- (listenerException.ErrorCode == 48 ||
- listenerException.ErrorCode == 98 ||
- listenerException.ErrorCode == 183 ||
- listenerException.ErrorCode == 10048);
+ return ex is SocketException socketException &&
+ (socketException.ErrorCode == 48 ||
+ socketException.ErrorCode == 98 ||
+ socketException.ErrorCode == 183 ||
+ socketException.ErrorCode == 10048);
}
private async Task ListenLoopAsync(CancellationToken ct)
{
- while (!ct.IsCancellationRequested && _isRunning)
+ var stoppedUnexpectedly = false;
+ try
{
- try
- {
- var context = await _listener.GetContextAsync();
- _ = Task.Run(() => HandleRequestAsync(context, ct), ct);
- }
- catch (HttpListenerException ex) when (ex.ErrorCode == 995)
- {
- break;
- }
- catch (ObjectDisposedException)
+ while (!ct.IsCancellationRequested && _isRunning)
{
- break;
+ try
+ {
+ var client = await _listener.AcceptTcpClientAsync();
+ _ = Task.Run(() => HandleClientAsync(client, ct), ct);
+ }
+ catch (SocketException ex) when (ex.SocketErrorCode == SocketError.Interrupted ||
+ ex.SocketErrorCode == SocketError.OperationAborted)
+ {
+ stoppedUnexpectedly = !ct.IsCancellationRequested && _isRunning;
+ break;
+ }
+ catch (ObjectDisposedException)
+ {
+ stoppedUnexpectedly = !ct.IsCancellationRequested && _isRunning;
+ break;
+ }
+ catch (Exception ex)
+ {
+ if (!ct.IsCancellationRequested && _isRunning)
+ {
+ Debug.LogError($"[Funplay MCP Server] Error in listen loop: {ex.Message}");
+ stoppedUnexpectedly = true;
+ }
+ break;
+ }
}
- catch (Exception ex)
+ }
+ finally
+ {
+ if (stoppedUnexpectedly)
{
- if (!ct.IsCancellationRequested)
- Debug.LogError($"[Funplay MCP Server] Error in listen loop: {ex.Message}");
- break;
+ _isRunning = false;
+ CloseListener();
}
}
}
- private async Task HandleRequestAsync(HttpListenerContext context, CancellationToken ct)
+ private async Task HandleClientAsync(TcpClient client, CancellationToken ct)
{
MCPRequest request = null;
+ NetworkStream stream = null;
try
{
- if (context.Request.HttpMethod == "OPTIONS")
+ using (client)
{
- await SendOptionsResponseAsync(context.Response);
- return;
- }
+ stream = client.GetStream();
+ var httpRequest = await ReadHttpRequestAsync(stream, ct);
+ if (httpRequest == null)
+ return;
- if (context.Request.HttpMethod != "POST")
- {
- await SendMethodNotAllowedAsync(context.Response, "POST, OPTIONS");
- return;
- }
+ if (httpRequest.Method == "OPTIONS")
+ {
+ await SendOptionsResponseAsync(stream, ct);
+ return;
+ }
- request = await ParseRequestAsync(context.Request);
- if (request == null)
- {
- await SendErrorResponseAsync(context.Response, null, -32700, "Parse error");
- return;
- }
+ if (httpRequest.Method != "POST")
+ {
+ await SendMethodNotAllowedAsync(stream, "POST, OPTIONS", ct);
+ return;
+ }
- var responseTcs = new TaskCompletionSource();
- OnRequestReceived?.Invoke(request, r => responseTcs.TrySetResult(r));
+ request = ParseJsonRequest(httpRequest.Body);
+ if (request == null)
+ {
+ await SendErrorResponseAsync(stream, null, -32700, "Parse error", ct);
+ return;
+ }
- using (var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
- using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token))
- {
- try
+ var requestReceived = OnRequestReceived;
+ if (requestReceived == null)
{
- var responseTask = responseTcs.Task;
- var completedTask = await Task.WhenAny(responseTask, Task.Delay(-1, linkedCts.Token));
- if (completedTask == responseTask)
+ await SendErrorResponseAsync(stream, request.Id, -32000, "MCP server is stopping or not ready.", ct);
+ return;
+ }
+
+ var responseTcs = new TaskCompletionSource();
+ requestReceived.Invoke(request, r => responseTcs.TrySetResult(r));
+
+ using (var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)))
+ using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token))
+ {
+ try
{
- var response = await responseTask;
- if (response == null)
+ var responseTask = responseTcs.Task;
+ var completedTask = await Task.WhenAny(responseTask, Task.Delay(-1, linkedCts.Token));
+ if (completedTask == responseTask)
{
- await SendAcceptedAsync(context.Response);
+ var response = await responseTask;
+ if (response == null)
+ {
+ await SendAcceptedAsync(stream, ct);
+ }
+ else
+ {
+ await SendResponseAsync(stream, response, ct);
+ }
}
else
{
- await SendResponseAsync(context.Response, response);
+ throw new OperationCanceledException();
}
}
- else
+ catch (OperationCanceledException)
{
- throw new OperationCanceledException();
+ var errResponse = timeoutCts.IsCancellationRequested
+ ? CreateErrorResponse(request.Id, -32000, "Request timeout")
+ : CreateErrorResponse(request.Id, -32000, "Request cancelled");
+ await SendResponseAsync(stream, errResponse, CancellationToken.None);
}
}
- catch (OperationCanceledException)
- {
- var errResponse = timeoutCts.IsCancellationRequested
- ? CreateErrorResponse(request.Id, -32000, "Request timeout")
- : CreateErrorResponse(request.Id, -32000, "Request cancelled");
- await SendResponseAsync(context.Response, errResponse);
- }
}
}
+ catch (OperationCanceledException) when (ct.IsCancellationRequested)
+ {
+ }
catch (Exception ex)
{
Debug.LogError($"[Funplay MCP Server] Error handling request: {ex.Message}");
- await SendErrorResponseAsync(context.Response, request?.Id, -32603, $"Internal error: {ex.Message}");
+ if (stream != null)
+ await SendErrorResponseAsync(stream, request?.Id, -32603, $"Internal error: {ex.Message}", CancellationToken.None);
}
}
- private async Task ParseRequestAsync(HttpListenerRequest request)
+ private async Task ReadHttpRequestAsync(NetworkStream stream, CancellationToken ct)
{
- try
+ var buffer = new byte[8192];
+ var rawRequest = new MemoryStream();
+ var headerEnd = -1;
+
+ while (headerEnd < 0)
{
- using (var reader = new StreamReader(request.InputStream, Encoding.UTF8))
- {
- var json = await reader.ReadToEndAsync();
- return ParseJsonRequest(json);
- }
+ var read = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
+ if (read == 0)
+ return null;
+
+ rawRequest.Write(buffer, 0, read);
+ if (rawRequest.Length > MaxHeaderBytes)
+ throw new InvalidOperationException("HTTP header is too large.");
+
+ headerEnd = FindHeaderEnd(rawRequest.GetBuffer(), (int)rawRequest.Length);
}
- catch (Exception ex)
- {
- Debug.LogError($"[Funplay MCP Server] Failed to parse request: {ex.Message}");
+
+ var requestBytes = rawRequest.ToArray();
+ var headerText = Encoding.ASCII.GetString(requestBytes, 0, headerEnd);
+ var lines = headerText.Split(new[] { "\r\n" }, StringSplitOptions.None);
+ if (lines.Length == 0)
+ return null;
+
+ var requestLineParts = lines[0].Split(' ');
+ if (requestLineParts.Length < 1)
return null;
+
+ var contentLength = 0;
+ for (var i = 1; i < lines.Length; i++)
+ {
+ var separator = lines[i].IndexOf(':');
+ if (separator <= 0)
+ continue;
+
+ var name = lines[i].Substring(0, separator).Trim();
+ if (!string.Equals(name, "Content-Length", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ int.TryParse(lines[i].Substring(separator + 1).Trim(), out contentLength);
+ break;
+ }
+
+ var bodyStart = headerEnd + 4;
+ var bodyBytes = new byte[contentLength];
+ var copied = Math.Min(contentLength, requestBytes.Length - bodyStart);
+ if (copied > 0)
+ Buffer.BlockCopy(requestBytes, bodyStart, bodyBytes, 0, copied);
+
+ while (copied < contentLength)
+ {
+ var read = await stream.ReadAsync(bodyBytes, copied, contentLength - copied, ct);
+ if (read == 0)
+ break;
+ copied += read;
}
+
+ return new HttpRequestData
+ {
+ Method = requestLineParts[0],
+ Body = Encoding.UTF8.GetString(bodyBytes, 0, copied)
+ };
+ }
+
+ private static int FindHeaderEnd(byte[] buffer, int length)
+ {
+ for (var i = 3; i < length; i++)
+ {
+ if (buffer[i - 3] == '\r' &&
+ buffer[i - 2] == '\n' &&
+ buffer[i - 1] == '\r' &&
+ buffer[i] == '\n')
+ {
+ return i - 3;
+ }
+ }
+
+ return -1;
}
private MCPRequest ParseJsonRequest(string json)
@@ -426,24 +410,12 @@ private MCPRequest ParseJsonRequest(string json)
}
}
- private async Task SendResponseAsync(HttpListenerResponse response, MCPResponse mcpResponse)
+ private async Task SendResponseAsync(NetworkStream stream, MCPResponse mcpResponse, CancellationToken ct)
{
try
{
var json = SerializeResponse(mcpResponse);
- var bytes = Encoding.UTF8.GetBytes(json);
-
- response.ContentType = "application/json";
- response.ContentEncoding = Encoding.UTF8;
- response.ContentLength64 = bytes.Length;
- response.StatusCode = 200;
-
- response.Headers.Add("Access-Control-Allow-Origin", "*");
- response.Headers.Add("Access-Control-Allow-Methods", "POST, OPTIONS");
- response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
-
- await response.OutputStream.WriteAsync(bytes, 0, bytes.Length);
- response.OutputStream.Close();
+ await SendRawResponseAsync(stream, 200, "OK", "application/json; charset=utf-8", json, ct);
}
catch (Exception ex)
{
@@ -451,43 +423,50 @@ private async Task SendResponseAsync(HttpListenerResponse response, MCPResponse
}
}
- private Task SendOptionsResponseAsync(HttpListenerResponse response)
+ private Task SendOptionsResponseAsync(NetworkStream stream, CancellationToken ct)
{
- response.StatusCode = (int)HttpStatusCode.NoContent;
- response.ContentLength64 = 0;
- response.Headers.Add("Access-Control-Allow-Origin", "*");
- response.Headers.Add("Access-Control-Allow-Methods", "POST, OPTIONS");
- response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
- response.OutputStream.Close();
- return Task.CompletedTask;
+ return SendRawResponseAsync(stream, (int)HttpStatusCode.NoContent, "No Content", "text/plain", string.Empty, ct);
}
- private Task SendMethodNotAllowedAsync(HttpListenerResponse response, string allowHeader)
+ private Task SendMethodNotAllowedAsync(NetworkStream stream, string allowHeader, CancellationToken ct)
{
- response.StatusCode = (int)HttpStatusCode.MethodNotAllowed;
- response.ContentLength64 = 0;
- response.Headers.Add("Allow", allowHeader);
- response.Headers.Add("Access-Control-Allow-Origin", "*");
- response.Headers.Add("Access-Control-Allow-Methods", "POST, OPTIONS");
- response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
- response.OutputStream.Close();
- return Task.CompletedTask;
+ return SendRawResponseAsync(stream, (int)HttpStatusCode.MethodNotAllowed, "Method Not Allowed", "text/plain", string.Empty, ct, "Allow: " + allowHeader + "\r\n");
}
- private Task SendAcceptedAsync(HttpListenerResponse response)
+ private Task SendAcceptedAsync(NetworkStream stream, CancellationToken ct)
{
- response.StatusCode = (int)HttpStatusCode.Accepted;
- response.ContentLength64 = 0;
- response.Headers.Add("Access-Control-Allow-Origin", "*");
- response.Headers.Add("Access-Control-Allow-Methods", "POST, OPTIONS");
- response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
- response.OutputStream.Close();
- return Task.CompletedTask;
+ return SendRawResponseAsync(stream, (int)HttpStatusCode.Accepted, "Accepted", "text/plain", string.Empty, ct);
+ }
+
+ private async Task SendErrorResponseAsync(NetworkStream stream, object requestId, int code, string message, CancellationToken ct)
+ {
+ await SendResponseAsync(stream, CreateErrorResponse(requestId, code, message), ct);
}
- private async Task SendErrorResponseAsync(HttpListenerResponse response, object requestId, int code, string message)
+ private static async Task SendRawResponseAsync(
+ NetworkStream stream,
+ int statusCode,
+ string reasonPhrase,
+ string contentType,
+ string body,
+ CancellationToken ct,
+ string extraHeaders = "")
{
- await SendResponseAsync(response, CreateErrorResponse(requestId, code, message));
+ var bodyBytes = Encoding.UTF8.GetBytes(body ?? string.Empty);
+ var header =
+ $"HTTP/1.1 {statusCode} {reasonPhrase}\r\n" +
+ $"Content-Type: {contentType}\r\n" +
+ $"Content-Length: {bodyBytes.Length}\r\n" +
+ "Connection: close\r\n" +
+ "Access-Control-Allow-Origin: *\r\n" +
+ "Access-Control-Allow-Methods: POST, OPTIONS\r\n" +
+ "Access-Control-Allow-Headers: Content-Type\r\n" +
+ extraHeaders +
+ "\r\n";
+ var headerBytes = Encoding.ASCII.GetBytes(header);
+ await stream.WriteAsync(headerBytes, 0, headerBytes.Length, ct);
+ if (bodyBytes.Length > 0)
+ await stream.WriteAsync(bodyBytes, 0, bodyBytes.Length, ct);
}
private MCPResponse CreateErrorResponse(object requestId, int code, string message)
@@ -529,8 +508,20 @@ private string SerializeResponse(MCPResponse response)
public void Dispose()
{
Stop();
- _cts?.Dispose();
- _listener = null;
+ }
+
+ private void CloseListener()
+ {
+ if (_listener == null)
+ return;
+
+ try { _listener.Stop(); } catch { }
+ }
+
+ private sealed class HttpRequestData
+ {
+ public string Method { get; set; }
+ public string Body { get; set; }
}
}
}
diff --git a/Editor/MCP/Server/MCPServerDomainReloadHandler.cs b/Editor/MCP/Server/MCPServerDomainReloadHandler.cs
index 12df87a..41d1b64 100644
--- a/Editor/MCP/Server/MCPServerDomainReloadHandler.cs
+++ b/Editor/MCP/Server/MCPServerDomainReloadHandler.cs
@@ -17,6 +17,8 @@ internal static class MCPServerDomainReloadHandler
{
private const string WasRunningKey = "Funplay_MCPServer_WasRunning";
private const string PortKey = "Funplay_MCPServer_Port";
+ private const string RestartDeadlineTicksKey = "Funplay_MCPServer_RestartDeadlineTicks";
+ private static readonly TimeSpan RestartRetryWindow = TimeSpan.FromMinutes(5);
private static bool _restartScheduled;
private static bool _restartInProgress;
@@ -43,6 +45,7 @@ internal static void PrepareForReload(IServiceProvider services)
PluginDebugLogger.Log("[Funplay MCP Server] Saving state before domain reload");
SessionState.SetBool(WasRunningKey, true);
SessionState.SetInt(PortKey, mcpServer.Port);
+ SessionState.SetString(RestartDeadlineTicksKey, DateTime.UtcNow.Add(RestartRetryWindow).Ticks.ToString());
}
catch (Exception ex)
{
@@ -116,14 +119,21 @@ private static async void RestartWhenEditorIsReady()
if (!SessionState.GetBool(WasRunningKey, false))
{
- EditorApplication.update -= RestartWhenEditorIsReady;
- _restartScheduled = false;
+ ClearScheduledRestart();
return;
}
if (EditorApplication.isCompiling)
return;
+ if (RestartDeadlineExpired())
+ {
+ Debug.LogError("[Funplay MCP Server] Timed out restarting after domain reload.");
+ ClearPendingRestart();
+ ClearScheduledRestart();
+ return;
+ }
+
var services = RootScopeServices.Services;
if (services == null)
return;
@@ -133,8 +143,6 @@ private static async void RestartWhenEditorIsReady()
if (mcpServer == null)
return;
- EditorApplication.update -= RestartWhenEditorIsReady;
- _restartScheduled = false;
_restartInProgress = true;
try
@@ -146,8 +154,22 @@ private static async void RestartWhenEditorIsReady()
if (!mcpServer.IsRunning)
{
var started = await mcpServer.StartAsync();
- if (!started)
+ if (started)
+ {
+ ClearPendingRestart();
+ ClearScheduledRestart();
+ }
+ else if (RestartDeadlineExpired())
+ {
Debug.LogError("[Funplay MCP Server] Failed to restart after domain reload.");
+ ClearPendingRestart();
+ ClearScheduledRestart();
+ }
+ }
+ else
+ {
+ ClearPendingRestart();
+ ClearScheduledRestart();
}
}
catch (Exception ex)
@@ -156,10 +178,34 @@ private static async void RestartWhenEditorIsReady()
}
finally
{
- SessionState.EraseBool(WasRunningKey);
- SessionState.EraseInt(PortKey);
_restartInProgress = false;
}
}
+
+ private static bool RestartDeadlineExpired()
+ {
+ var deadlineText = SessionState.GetString(RestartDeadlineTicksKey, string.Empty);
+ if (!long.TryParse(deadlineText, out var deadlineTicks))
+ {
+ SessionState.SetString(RestartDeadlineTicksKey, DateTime.UtcNow.Add(RestartRetryWindow).Ticks.ToString());
+ return false;
+ }
+
+ return DateTime.UtcNow.Ticks >= deadlineTicks;
+ }
+
+ private static void ClearPendingRestart()
+ {
+ SessionState.EraseBool(WasRunningKey);
+ SessionState.EraseInt(PortKey);
+ SessionState.EraseString(RestartDeadlineTicksKey);
+ }
+
+ private static void ClearScheduledRestart()
+ {
+ EditorApplication.delayCall -= RestartWhenEditorIsReady;
+ EditorApplication.update -= RestartWhenEditorIsReady;
+ _restartScheduled = false;
+ }
}
}
diff --git a/Editor/Threading/EditorThreadHelper.cs b/Editor/Threading/EditorThreadHelper.cs
index 81f80a8..6293398 100644
--- a/Editor/Threading/EditorThreadHelper.cs
+++ b/Editor/Threading/EditorThreadHelper.cs
@@ -32,6 +32,9 @@ public EditorThreadHelper(IEditorStateService editorStateService)
public Task ExecuteOnEditorThreadAsync(Action action)
{
+ if (_disposed)
+ return CreateCanceledTask();
+
if (IsMainThread)
{
try
@@ -52,6 +55,9 @@ public Task ExecuteOnEditorThreadAsync(Action action)
public Task ExecuteOnEditorThreadAsync(Func func)
{
+ if (_disposed)
+ return CreateCanceledTask();
+
if (IsMainThread)
{
try
@@ -76,6 +82,9 @@ public Task ExecuteOnEditorThreadAsync(Func func)
public Task ExecuteAsyncOnEditorThreadAsync(Func> asyncFunc)
{
+ if (_disposed)
+ return CreateCanceledTask();
+
if (IsMainThread)
{
return asyncFunc();
@@ -83,16 +92,28 @@ public Task ExecuteAsyncOnEditorThreadAsync(Func> asyncFunc)
var outerTcs = new TaskCompletionSource();
var tcs = new TaskCompletionSource