From cc9603e1360e27397d19fdb3306cf0b7790f4cc2 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Wed, 27 May 2026 14:09:01 +0800 Subject: [PATCH] fix(stdio): restore 2s socket-release wait to prevent Windows port leak (#1173) PR #688 set toWait.Wait(2000) on StdioBridgeHost.Stop() specifically so the TcpListener could fully release the port on Windows before the post-reload Start() rebinds. PR #787 later reduced this to 500ms without flagging the Windows dependency, and 9.7.1 ships the regressed value. On Windows, 500ms is too short for the OS to free the socket. The post-reload Start() then hits AddressAlreadyInUse, silently falls back to a new port via PortManager.DiscoverNewPort(), and leaves the Python server pinned to the original port. Scene-graph tools start returning busy/timeout indefinitely because OS round-robin sometimes routes them to the orphaned listener. - StdioBridgeHost.Stop: 500 -> 2000 (matches PR #688 intent) - StdioBridgeReloadHandler.OnBeforeAssemblyReload: 500 -> 2000 (matches the StopAsync ceiling; was already too short for the same reason) Reporter (#1173) bisected to Reload Domain ON; Reload Domain OFF is clean across 4+ play cycles, consistent with the socket-release race. --- MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs | 4 +++- .../Editor/Services/Transport/Transports/StdioBridgeHost.cs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs index cfa4d7286..868c94dcb 100644 --- a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs +++ b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs @@ -70,7 +70,9 @@ private static void OnBeforeAssemblyReload() // Stop only stdio before reload. This is centralized here so resume-flag updates // and teardown cannot race each other via separate beforeAssemblyReload handlers. var stopTask = MCPServiceLocator.TransportManager.StopAsync(TransportMode.Stdio); - try { stopTask.Wait(500); } catch { } + // Match StdioBridgeHost.Stop's 2s socket-release wait; 500ms is too short on + // Windows and lets the next reload bind the same port before the OS frees it. + try { stopTask.Wait(2000); } catch { } // Legacy safety: stdio may have been started outside TransportManager state. try { StdioBridgeHost.Stop(); } catch { } diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs index 66721f7b9..2a2cd96ff 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs @@ -396,8 +396,10 @@ public static void Stop() if (toWait != null) { - // CTS is already cancelled; give the listener task a brief moment to exit. - try { toWait.Wait(500); } catch { } + // Windows needs the full 2s for the socket to release after Dispose; with 500ms + // the next Start() can hit AddressAlreadyInUse and silently fall back to a new + // port, orphaning the listener. See #1173 / #688. + try { toWait.Wait(2000); } catch { } } // ProcessCommands stays permanently hooked (guarded by _processCommandsHooked)