@@ -1328,15 +1328,22 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
13281328 using var cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
13291329 cts . CancelAfter ( TimeSpan . FromSeconds ( 30 ) ) ;
13301330
1331- while ( ! cts . Token . IsCancellationRequested )
1331+ try
13321332 {
1333- var line = await cliProcess . StandardOutput . ReadLineAsync ( cts . Token ) ?? throw new IOException ( "CLI process exited unexpectedly" ) ;
1334- if ( ListeningOnPortRegex ( ) . Match ( line ) is { Success : true } match )
1333+ while ( true )
13351334 {
1336- detectedLocalhostTcpPort = int . Parse ( match . Groups [ 1 ] . Value , CultureInfo . InvariantCulture ) ;
1337- break ;
1335+ var line = await cliProcess . StandardOutput . ReadLineAsync ( cts . Token ) ?? throw new IOException ( "CLI process exited unexpectedly" ) ;
1336+ if ( ListeningOnPortRegex ( ) . Match ( line ) is { Success : true } match )
1337+ {
1338+ detectedLocalhostTcpPort = int . Parse ( match . Groups [ 1 ] . Value , CultureInfo . InvariantCulture ) ;
1339+ break ;
1340+ }
13381341 }
13391342 }
1343+ catch ( OperationCanceledException ) when ( ! cancellationToken . IsCancellationRequested && cts . IsCancellationRequested )
1344+ {
1345+ throw new IOException ( "Timed out waiting for Copilot CLI to report its TCP listening port." ) ;
1346+ }
13401347 }
13411348
13421349 return ( cliProcess , detectedLocalhostTcpPort , stderrPump ) ;
@@ -1397,63 +1404,87 @@ private static (string FileName, IEnumerable<string> Args) ResolveCliCommand(str
13971404
13981405 private async Task < Connection > ConnectToServerAsync ( Process ? cliProcess , string ? tcpHost , int ? tcpPort , ProcessStderrPump ? stderrPump , CancellationToken cancellationToken )
13991406 {
1400- Stream inputStream , outputStream ;
14011407 TcpClient ? tcpClient = null ;
14021408 NetworkStream ? networkStream = null ;
1409+ JsonRpc ? rpc = null ;
14031410
1404- if ( _options . UseStdio )
1405- {
1406- if ( cliProcess == null ) throw new InvalidOperationException ( "CLI process not started" ) ;
1407- inputStream = cliProcess . StandardOutput . BaseStream ;
1408- outputStream = cliProcess . StandardInput . BaseStream ;
1409- }
1410- else
1411+ try
14111412 {
1412- if ( tcpHost is null || tcpPort is null )
1413+ Stream inputStream , outputStream ;
1414+
1415+ if ( _options . UseStdio )
14131416 {
1414- throw new InvalidOperationException ( "Cannot connect because TCP host or port are not available" ) ;
1417+ if ( cliProcess == null ) throw new InvalidOperationException ( "CLI process not started" ) ;
1418+ inputStream = cliProcess . StandardOutput . BaseStream ;
1419+ outputStream = cliProcess . StandardInput . BaseStream ;
14151420 }
1421+ else
1422+ {
1423+ if ( tcpHost is null || tcpPort is null )
1424+ {
1425+ throw new InvalidOperationException ( "Cannot connect because TCP host or port are not available" ) ;
1426+ }
14161427
1417- tcpClient = new ( ) ;
1418- await tcpClient . ConnectAsync ( tcpHost , tcpPort . Value , cancellationToken ) ;
1419- networkStream = tcpClient . GetStream ( ) ;
1420- inputStream = networkStream ;
1421- outputStream = networkStream ;
1422- }
1428+ tcpClient = new ( ) ;
1429+ await tcpClient . ConnectAsync ( tcpHost , tcpPort . Value , cancellationToken ) ;
1430+ networkStream = tcpClient . GetStream ( ) ;
1431+ inputStream = networkStream ;
1432+ outputStream = networkStream ;
1433+ }
14231434
1424- var rpc = new JsonRpc ( new HeaderDelimitedMessageHandler (
1425- outputStream ,
1426- inputStream ,
1427- CreateSystemTextJsonFormatter ( ) ) )
1428- {
1429- TraceSource = new LoggerTraceSource ( _logger ) ,
1430- } ;
1435+ rpc = new JsonRpc ( new HeaderDelimitedMessageHandler (
1436+ outputStream ,
1437+ inputStream ,
1438+ CreateSystemTextJsonFormatter ( ) ) )
1439+ {
1440+ TraceSource = new LoggerTraceSource ( _logger ) ,
1441+ } ;
14311442
1432- var handler = new RpcHandler ( this ) ;
1433- rpc . AddLocalRpcMethod ( "session.event" , handler . OnSessionEvent ) ;
1434- rpc . AddLocalRpcMethod ( "session.lifecycle" , handler . OnSessionLifecycle ) ;
1435- // Protocol v3 servers send tool calls / permission requests as broadcast events.
1436- // Protocol v2 servers use the older tool.call / permission.request RPC model.
1437- // We always register v2 adapters because handlers are set up before version
1438- // negotiation; a v3 server will simply never send these requests.
1439- rpc . AddLocalRpcMethod ( "tool.call" , handler . OnToolCallV2 ) ;
1440- rpc . AddLocalRpcMethod ( "permission.request" , handler . OnPermissionRequestV2 ) ;
1441- rpc . AddLocalRpcMethod ( "userInput.request" , handler . OnUserInputRequest ) ;
1442- rpc . AddLocalRpcMethod ( "hooks.invoke" , handler . OnHooksInvoke ) ;
1443- rpc . AddLocalRpcMethod ( "systemMessage.transform" , handler . OnSystemMessageTransform ) ;
1444- ClientSessionApiRegistration . RegisterClientSessionApiHandlers ( rpc , sessionId =>
1445- {
1446- var session = GetSession ( sessionId ) ?? throw new ArgumentException ( $ "Unknown session { sessionId } ") ;
1447- return session . ClientSessionApis ;
1448- } ) ;
1449- rpc . StartListening ( ) ;
1443+ var handler = new RpcHandler ( this ) ;
1444+ rpc . AddLocalRpcMethod ( "session.event" , handler . OnSessionEvent ) ;
1445+ rpc . AddLocalRpcMethod ( "session.lifecycle" , handler . OnSessionLifecycle ) ;
1446+ // Protocol v3 servers send tool calls / permission requests as broadcast events.
1447+ // Protocol v2 servers use the older tool.call / permission.request RPC model.
1448+ // We always register v2 adapters because handlers are set up before version
1449+ // negotiation; a v3 server will simply never send these requests.
1450+ rpc . AddLocalRpcMethod ( "tool.call" , handler . OnToolCallV2 ) ;
1451+ rpc . AddLocalRpcMethod ( "permission.request" , handler . OnPermissionRequestV2 ) ;
1452+ rpc . AddLocalRpcMethod ( "userInput.request" , handler . OnUserInputRequest ) ;
1453+ rpc . AddLocalRpcMethod ( "hooks.invoke" , handler . OnHooksInvoke ) ;
1454+ rpc . AddLocalRpcMethod ( "systemMessage.transform" , handler . OnSystemMessageTransform ) ;
1455+ ClientSessionApiRegistration . RegisterClientSessionApiHandlers ( rpc , sessionId =>
1456+ {
1457+ var session = GetSession ( sessionId ) ?? throw new ArgumentException ( $ "Unknown session { sessionId } ") ;
1458+ return session . ClientSessionApis ;
1459+ } ) ;
1460+ rpc . StartListening ( ) ;
1461+
1462+ // Transition state to Disconnected if the JSON-RPC connection drops
1463+ _ = rpc . Completion . ContinueWith ( _ => _disconnected = true , TaskScheduler . Default ) ;
1464+
1465+ _rpc = new ServerRpc ( rpc ) ;
1466+
1467+ return new Connection ( rpc , cliProcess , tcpClient , networkStream , stderrPump ) ;
1468+ }
1469+ catch
1470+ {
1471+ try { rpc ? . Dispose ( ) ; }
1472+ catch ( Exception ex ) { _logger . LogDebug ( ex , "Failed to dispose JSON-RPC connection after startup failure" ) ; }
14501473
1451- // Transition state to Disconnected if the JSON-RPC connection drops
1452- _ = rpc . Completion . ContinueWith ( _ => _disconnected = true , TaskScheduler . Default ) ;
1474+ if ( networkStream is not null )
1475+ {
1476+ try { await networkStream . DisposeAsync ( ) ; }
1477+ catch ( Exception ex ) { _logger . LogDebug ( ex , "Failed to dispose TCP stream after startup failure" ) ; }
1478+ }
14531479
1454- _rpc = new ServerRpc ( rpc ) ;
1480+ if ( tcpClient is not null )
1481+ {
1482+ try { tcpClient . Dispose ( ) ; }
1483+ catch ( Exception ex ) { _logger . LogDebug ( ex , "Failed to dispose TCP client after startup failure" ) ; }
1484+ }
14551485
1456- return new Connection ( rpc , cliProcess , tcpClient , networkStream , stderrPump ) ;
1486+ throw ;
1487+ }
14571488 }
14581489
14591490 [ UnconditionalSuppressMessage ( "Trimming" , "IL2026" , Justification = "Using happy path from https://microsoft.github.io/vs-streamjsonrpc/docs/nativeAOT.html" ) ]
0 commit comments