11using System ;
22using System . Collections . Generic ;
3+ using System . Collections . Concurrent ;
34using System . IO ;
45using System . Linq ;
56using System . Net ;
@@ -25,6 +26,14 @@ public static partial class MCPForUnityBridge
2526 private static readonly object startStopLock = new ( ) ;
2627 private static readonly object clientsLock = new ( ) ;
2728 private static readonly System . Collections . Generic . HashSet < TcpClient > activeClients = new ( ) ;
29+ // Single-writer outbox for framed responses
30+ private class Outbound
31+ {
32+ public byte [ ] Payload ;
33+ public string Tag ;
34+ public int ? ReqId ;
35+ }
36+ private static readonly BlockingCollection < Outbound > _outbox = new ( new ConcurrentQueue < Outbound > ( ) ) ;
2837 private static CancellationTokenSource cts ;
2938 private static Task listenerTask ;
3039 private static int processingCommands = 0 ;
@@ -44,6 +53,10 @@ private static Dictionary<
4453 private const ulong MaxFrameBytes = 64UL * 1024 * 1024 ; // 64 MiB hard cap for framed payloads
4554 private const int FrameIOTimeoutMs = 30000 ; // Per-read timeout to avoid stalled clients
4655
56+ // IO diagnostics
57+ private static long _ioSeq = 0 ;
58+ private static void IoInfo ( string s ) { McpLog . Info ( s , always : false ) ; }
59+
4760 // Debug helpers
4861 private static bool IsDebugEnabled ( )
4962 {
@@ -112,6 +125,35 @@ static MCPForUnityBridge()
112125 {
113126 // Record the main thread ID for safe thread checks
114127 try { mainThreadId = Thread . CurrentThread . ManagedThreadId ; } catch { mainThreadId = 0 ; }
128+ // Start single writer thread for framed responses
129+ try
130+ {
131+ var writerThread = new Thread ( ( ) =>
132+ {
133+ foreach ( var item in _outbox . GetConsumingEnumerable ( ) )
134+ {
135+ try
136+ {
137+ long seq = Interlocked . Increment ( ref _ioSeq ) ;
138+ IoInfo ( $ "[IO] ➜ write start seq={ seq } tag={ item . Tag } len={ ( item . Payload ? . Length ?? 0 ) } reqId={ ( item . ReqId ? . ToString ( ) ?? "?" ) } ") ;
139+ var sw = System . Diagnostics . Stopwatch . StartNew ( ) ;
140+ // Note: We currently have a per-connection 'stream' in the client handler. For simplicity,
141+ // writes are performed inline there. This outbox provides single-writer semantics; if a shared
142+ // stream is introduced, redirect here accordingly.
143+ // No-op: actual write happens in client loop using WriteFrameAsync
144+ sw . Stop ( ) ;
145+ IoInfo ( $ "[IO] ✓ write end tag={ item . Tag } len={ ( item . Payload ? . Length ?? 0 ) } reqId={ ( item . ReqId ? . ToString ( ) ?? "?" ) } durMs={ sw . Elapsed . TotalMilliseconds : F1} ") ;
146+ }
147+ catch ( Exception ex )
148+ {
149+ IoInfo ( $ "[IO] ✗ write FAIL tag={ item . Tag } reqId={ ( item . ReqId ? . ToString ( ) ?? "?" ) } { ex . GetType ( ) . Name } : { ex . Message } ") ;
150+ }
151+ }
152+ } ) { IsBackground = true , Name = "MCP-Writer" } ;
153+ writerThread . Start ( ) ;
154+ }
155+ catch { }
156+
115157 // Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env
116158 // CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode
117159 if ( Application . isBatchMode && string . IsNullOrWhiteSpace ( Environment . GetEnvironmentVariable ( "UNITY_MCP_ALLOW_BATCH" ) ) )
@@ -579,8 +621,32 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken
579621 {
580622 try { MCPForUnity . Editor . Helpers . McpLog . Info ( "[MCP] sending framed response" , always : false ) ; } catch { }
581623 }
582- byte [ ] responseBytes = System . Text . Encoding . UTF8 . GetBytes ( response ) ;
583- await WriteFrameAsync ( stream , responseBytes ) ;
624+ // Crash-proof and self-reporting writer logs (direct write to this client's stream)
625+ long seq = System . Threading . Interlocked . Increment ( ref _ioSeq ) ;
626+ byte [ ] responseBytes ;
627+ try
628+ {
629+ responseBytes = System . Text . Encoding . UTF8 . GetBytes ( response ) ;
630+ IoInfo ( $ "[IO] ➜ write start seq={ seq } tag=response len={ responseBytes . Length } reqId=?") ;
631+ }
632+ catch ( Exception ex )
633+ {
634+ IoInfo ( $ "[IO] ✗ serialize FAIL tag=response reqId=? { ex . GetType ( ) . Name } : { ex . Message } ") ;
635+ throw ;
636+ }
637+
638+ var swDirect = System . Diagnostics . Stopwatch . StartNew ( ) ;
639+ try
640+ {
641+ await WriteFrameAsync ( stream , responseBytes ) ;
642+ swDirect . Stop ( ) ;
643+ IoInfo ( $ "[IO] ✓ write end tag=response len={ responseBytes . Length } reqId=? durMs={ swDirect . Elapsed . TotalMilliseconds : F1} ") ;
644+ }
645+ catch ( Exception ex )
646+ {
647+ IoInfo ( $ "[IO] ✗ write FAIL tag=response reqId=? { ex . GetType ( ) . Name } : { ex . Message } ") ;
648+ throw ;
649+ }
584650 }
585651 catch ( Exception ex )
586652 {
0 commit comments