diff --git a/docs/getting-started.md b/docs/getting-started.md index 2c4e6159..4fc132ff 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1048,6 +1048,18 @@ await using var session = await client.CreateSessionAsync(); --- +## Troubleshooting Connection Issues + +If you encounter errors like "The JSON-RPC connection with the remote party was lost before the request could complete", see our [Troubleshooting Connection Errors guide](./troubleshooting-connection-errors.md) for detailed diagnostics and solutions. + +Common issues include: +- CLI not installed or not in PATH +- Not authenticated with GitHub (`copilot auth login`) +- Version incompatibility between SDK and CLI +- Process permissions or antivirus interference + +--- + ## Learn More - [Node.js SDK Reference](../nodejs/README.md) diff --git a/docs/troubleshooting-connection-errors.md b/docs/troubleshooting-connection-errors.md new file mode 100644 index 00000000..b4a0ba33 --- /dev/null +++ b/docs/troubleshooting-connection-errors.md @@ -0,0 +1,221 @@ +# Troubleshooting Connection Errors + +This guide helps you diagnose and fix common connection errors when using the GitHub Copilot SDK. + +## "The JSON-RPC connection with the remote party was lost before the request could complete" + +This error occurs when the SDK cannot communicate with the Copilot CLI. Here are the most common causes and solutions: + +### 1. CLI Not Installed or Not in PATH + +**Symptom**: Error message mentions "Failed to start Copilot CLI process" + +**Solution**: +- Verify the CLI is installed: `copilot --version` +- If not installed, follow the [installation guide](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) +- If installed but not in PATH, specify the full path in your code: + +
+.NET + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + CliPath = "/path/to/copilot" // or "C:\\path\\to\\copilot.exe" on Windows +}); +``` + +
+ +
+Node.js + +```typescript +const client = new CopilotClient({ + cliPath: "/path/to/copilot" +}); +``` + +
+ +
+Python + +```python +client = CopilotClient({ + "cli_path": "/path/to/copilot" +}) +``` + +
+ +### 2. CLI Process Exits Immediately + +**Symptom**: Error message mentions "CLI process exited immediately with code X" or "CLI process exited unexpectedly" + +**Common causes**: +- **Not authenticated**: The CLI requires authentication with GitHub + - Solution: Run `copilot auth login` to authenticate + - Verify authentication: `copilot auth status` + +- **Missing dependencies**: The CLI may require Node.js or other dependencies + - For JavaScript-based CLI: Ensure Node.js 18+ is installed + - Check the error output included in the exception message for clues + +- **Permissions issues**: The CLI executable may not have execute permissions + - On Unix/Linux/Mac: `chmod +x /path/to/copilot` + +### 3. Version Incompatibility + +**Symptom**: Error mentioning "protocol version mismatch" + +**Solution**: Update either the SDK or CLI to compatible versions +- Check the [release notes](https://github.com/github/copilot-sdk/releases) for compatibility information +- Update CLI: Follow the installation guide to get the latest version +- Update SDK: Install the latest SDK package + +### 4. Port Already in Use (TCP Mode) + +**Symptom**: Connection error when using TCP mode + +**Solution**: +- Let the SDK choose a random port (don't specify the port option) +- Or specify a different port: + +```csharp +var client = new CopilotClient(new CopilotClientOptions +{ + UseStdio = false, + Port = 8080 // Choose an available port +}); +``` + +### 5. Timeout Waiting for CLI + +**Symptom**: "Timed out waiting for CLI server to announce its port" + +**Causes**: +- CLI is taking too long to start (slow machine, antivirus scanning, etc.) +- CLI failed to start but didn't exit +- Firewall blocking network communication + +**Solutions**: +- Check if antivirus is scanning the CLI executable +- Try using stdio mode instead of TCP (default in SDK): + ```csharp + var client = new CopilotClient(new CopilotClientOptions { UseStdio = true }); + ``` +- Check firewall settings if using TCP mode + +## Getting More Information + +### Enable Debug Logging + +To see detailed diagnostic information: + +
+.NET + +```csharp +using Microsoft.Extensions.Logging; + +var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Debug); +}); + +var client = new CopilotClient(new CopilotClientOptions +{ + Logger = loggerFactory.CreateLogger(), + LogLevel = "debug" // CLI log level +}); +``` + +
+ +
+Node.js + +```typescript +const client = new CopilotClient({ + logLevel: "debug" +}); +``` + +
+ +
+Python + +```python +import logging +logging.basicConfig(level=logging.DEBUG) + +client = CopilotClient({ + "log_level": "debug" +}) +``` + +
+ +### Check CLI Directly + +Test the CLI independently to isolate SDK issues: + +```bash +# Test basic CLI functionality +copilot --version + +# Check authentication +copilot auth status + +# Start CLI in server mode manually +copilot --server --port 4321 + +# In another terminal, try to connect using the SDK +# with cliUrl: "localhost:4321" +``` + +### Examine Error Output + +The latest versions of the SDK include stderr output from the CLI in error messages when processes fail. Look for: +- Authentication errors +- Missing file or permission errors +- Node.js errors (if CLI is JS-based) +- Network/proxy configuration issues + +## Common Error Messages and Solutions + +| Error Message | Likely Cause | Solution | +|--------------|--------------|----------| +| "Failed to start Copilot CLI process" | CLI not found or not executable | Check installation and PATH | +| "exited immediately with code 1" | Authentication or configuration error | Run `copilot auth login` | +| "exited immediately with code 127" | Command not found | Verify CLI is in PATH | +| "Timed out waiting for CLI server" | CLI failed to start or network issue | Check logs, try stdio mode | +| "protocol version mismatch" | SDK and CLI versions incompatible | Update SDK or CLI | + +## Still Having Issues? + +If you're still experiencing problems: + +1. **Collect diagnostic information**: + - SDK version + - CLI version (`copilot --version`) + - Operating system and version + - Full error message with stack trace + - CLI stderr output (included in recent error messages) + +2. **Create a minimal reproduction**: + - Simplest possible code that reproduces the error + - Share your client configuration options + +3. **Report the issue**: + - Open an issue on the [GitHub repository](https://github.com/github/copilot-sdk) + - Include all diagnostic information collected above + +## Related Documentation + +- [Getting Started Guide](./getting-started.md) +- [API Reference](../dotnet/README.md) +- [Copilot CLI Documentation](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 112e988e..876be1fa 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -596,6 +596,14 @@ internal static async Task InvokeRpcAsync(JsonRpc rpc, string method, obje { return await rpc.InvokeWithCancellationAsync(method, args, cancellationToken); } + catch (StreamJsonRpc.ConnectionLostException ex) + { + throw new IOException( + $"Communication error with Copilot CLI: The connection was lost. " + + $"This usually means the CLI process crashed or exited unexpectedly. " + + $"See the troubleshooting guide at https://github.com/github/copilot-sdk/blob/main/docs/troubleshooting-connection-errors.md for help. " + + $"Details: {ex.Message}", ex); + } catch (StreamJsonRpc.RemoteRpcException ex) { throw new IOException($"Communication error with Copilot CLI: {ex.Message}", ex); @@ -683,21 +691,72 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio startInfo.Environment.Remove("NODE_DEBUG"); var cliProcess = new Process { StartInfo = startInfo }; - cliProcess.Start(); + + try + { + cliProcess.Start(); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to start Copilot CLI process. " + + $"Please ensure the Copilot CLI is installed and accessible at '{cliPath}'. " + + $"Error: {ex.Message}", ex); + } - // Forward stderr to logger - _ = Task.Run(async () => + // Start capturing stderr immediately to avoid race conditions + var stderrLines = new List(); + var stderrLock = new object(); + var stderrTask = Task.Run(async () => { - while (cliProcess != null && !cliProcess.HasExited) + try { - var line = await cliProcess.StandardError.ReadLineAsync(cancellationToken); - if (line != null) + while (!cliProcess.HasExited) { - logger.LogDebug("[CLI] {Line}", line); + var line = await cliProcess.StandardError.ReadLineAsync(cancellationToken); + if (line != null) + { + lock (stderrLock) + { + stderrLines.Add(line); + // Keep only last 50 lines to avoid unbounded growth + if (stderrLines.Count > 50) + { + stderrLines.RemoveAt(0); + } + } + logger.LogDebug("[CLI] {Line}", line); + } } } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogDebug("Error reading CLI stderr: {Error}", ex.Message); + } }, cancellationToken); + // Check if process exited immediately (indicates a startup failure) + // Use a shorter delay and check HasExited to minimize false positives + await Task.Delay(50, cancellationToken); + if (cliProcess.HasExited) + { + var exitCode = cliProcess.ExitCode; + + // Give a moment for stderr task to capture any error output + await Task.Delay(50, cancellationToken); + + string errorOutput; + lock (stderrLock) + { + errorOutput = stderrLines.Count > 0 ? string.Join(Environment.NewLine, stderrLines) : string.Empty; + } + + throw new InvalidOperationException( + $"Copilot CLI process exited immediately with code {exitCode}. " + + $"This may indicate the CLI is not properly installed or configured. " + + (string.IsNullOrEmpty(errorOutput) ? "Run with debug logging enabled for more details." : $"Error output:{Environment.NewLine}{errorOutput}")); + } + var detectedLocalhostTcpPort = (int?)null; if (!options.UseStdio) { @@ -705,18 +764,45 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(30)); - while (!cts.Token.IsCancellationRequested) + try { - var line = await cliProcess.StandardOutput.ReadLineAsync(cts.Token); - if (line == null) throw new Exception("CLI process exited unexpectedly"); - - var match = Regex.Match(line, @"listening on port (\d+)", RegexOptions.IgnoreCase); - if (match.Success) + while (!cts.Token.IsCancellationRequested) { - detectedLocalhostTcpPort = int.Parse(match.Groups[1].Value); - break; + var line = await cliProcess.StandardOutput.ReadLineAsync(cts.Token); + if (line == null) + { + var exitCode = cliProcess.HasExited ? cliProcess.ExitCode : -1; + + // Give a moment for stderr to be captured + await Task.Delay(50, CancellationToken.None); + + string errorOutput; + lock (stderrLock) + { + errorOutput = stderrLines.Count > 0 ? string.Join(Environment.NewLine, stderrLines) : string.Empty; + } + + throw new InvalidOperationException( + $"CLI process exited unexpectedly" + + (cliProcess.HasExited ? $" with code {exitCode}" : "") + + ". " + + (string.IsNullOrEmpty(errorOutput) ? "Check CLI logs for details." : $"Error output:{Environment.NewLine}{errorOutput}")); + } + + var match = Regex.Match(line, @"listening on port (\d+)", RegexOptions.IgnoreCase); + if (match.Success) + { + detectedLocalhostTcpPort = int.Parse(match.Groups[1].Value); + break; + } } } + catch (OperationCanceledException) + { + throw new TimeoutException( + "Timed out waiting for CLI server to announce its port. " + + "The CLI may have failed to start or is not responding."); + } } return (cliProcess, detectedLocalhostTcpPort); @@ -750,6 +836,14 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? if (_options.UseStdio) { if (cliProcess == null) throw new InvalidOperationException("CLI process not started"); + + // Check if process has already exited + if (cliProcess.HasExited) + { + throw new InvalidOperationException( + $"Cannot connect to CLI process - it has already exited with code {cliProcess.ExitCode}."); + } + inputStream = cliProcess.StandardOutput.BaseStream; outputStream = cliProcess.StandardInput.BaseStream; } diff --git a/dotnet/test/ErrorHandlingTests.cs b/dotnet/test/ErrorHandlingTests.cs new file mode 100644 index 00000000..7f850582 --- /dev/null +++ b/dotnet/test/ErrorHandlingTests.cs @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.SDK; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace GitHub.Copilot.SDK.Test; + +/// +/// Tests for error handling and diagnostics improvements. +/// +public class ErrorHandlingTests +{ + [Fact] + public async Task Should_Provide_Helpful_Error_When_CLI_Not_Found() + { + // Arrange: Use a non-existent CLI path + var client = new CopilotClient(new CopilotClientOptions + { + CliPath = "/nonexistent/path/to/copilot", + AutoStart = true + }); + + // Act & Assert: Should throw with helpful error message + var exception = await Assert.ThrowsAsync( + async () => await client.CreateSessionAsync()); + + Assert.Contains("Failed to start Copilot CLI process", exception.Message); + Assert.Contains("Please ensure the Copilot CLI is installed", exception.Message); + } + + [Fact] + public async Task Should_Detect_Immediate_Process_Exit() + { + // Arrange: Use an executable that exits immediately + // Platform-specific command that exits with non-zero code + string exitCommand; + if (OperatingSystem.IsWindows()) + { + exitCommand = "cmd"; + } + else + { + exitCommand = "false"; // Unix command that exits immediately with code 1 + } + + var clientOptions = new CopilotClientOptions + { + AutoStart = true + }; + + if (OperatingSystem.IsWindows()) + { + clientOptions.CliPath = exitCommand; + clientOptions.CliArgs = ["/c", "exit", "1"]; // Exit with code 1 + } + else + { + clientOptions.CliPath = exitCommand; + } + + var client = new CopilotClient(clientOptions); + + // Act & Assert: Should detect the immediate exit + var exception = await Assert.ThrowsAsync( + async () => await client.CreateSessionAsync()); + + // Should mention the process exited immediately + Assert.Contains("exited immediately", exception.Message); + } + + [Fact] + public async Task Should_Provide_Clear_Error_For_Connection_Issues() + { + // Verify that ConnectionLostException is handled and wrapped properly + // by checking that the InvokeRpcAsync method has proper exception handling + + // This is a minimal test that verifies the structure exists + // More comprehensive testing would require integration tests with a real CLI + var method = typeof(CopilotClient).GetMethod( + "InvokeRpcAsync", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + + Assert.NotNull(method); + } +}