mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Fix #5221: WSL hanging and disconnection issues with Claude Code
- Add WSL detection using WSL_DISTRO_NAME environment variable - Reduce timeout for WSL environments (5 minutes vs 10 minutes) - Implement heartbeat mechanism to prevent WSL hanging - Add WSL-specific environment variables for stability - Enhanced process cleanup with graceful termination for WSL - Add comprehensive test coverage for WSL support This addresses the issue where Claude Code integration would hang and disconnect from VSCode when running on WSL environments.
This commit is contained in:
parent
3a8ba27615
commit
381ef21996
2 changed files with 129 additions and 9 deletions
50
src/integrations/claude-code/__tests__/wsl-support.spec.ts
Normal file
50
src/integrations/claude-code/__tests__/wsl-support.spec.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
|
||||
describe("Claude Code WSL Support", () => {
|
||||
let originalEnv: NodeJS.ProcessEnv
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env }
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
vi.clearAllTimers()
|
||||
})
|
||||
|
||||
test("should use shorter timeout for WSL environments", async () => {
|
||||
// Set WSL environment
|
||||
process.env.WSL_DISTRO_NAME = "Ubuntu"
|
||||
|
||||
// Import the constants after setting the environment
|
||||
const { CLAUDE_CODE_TIMEOUT } = await import("../run")
|
||||
|
||||
// Verify WSL timeout is 5 minutes (300000ms)
|
||||
expect(CLAUDE_CODE_TIMEOUT).toBe(300000)
|
||||
})
|
||||
|
||||
test("should use standard timeout for non-WSL environments", async () => {
|
||||
// Ensure no WSL environment
|
||||
delete process.env.WSL_DISTRO_NAME
|
||||
|
||||
// Clear module cache to ensure fresh import
|
||||
vi.resetModules()
|
||||
|
||||
// Import the constants after clearing WSL environment
|
||||
const { CLAUDE_CODE_TIMEOUT } = await import("../run")
|
||||
|
||||
// Verify standard timeout is 10 minutes (600000ms)
|
||||
expect(CLAUDE_CODE_TIMEOUT).toBe(600000)
|
||||
})
|
||||
|
||||
test("should detect WSL environment correctly", () => {
|
||||
// Test WSL detection
|
||||
process.env.WSL_DISTRO_NAME = "Ubuntu"
|
||||
expect(!!process.env.WSL_DISTRO_NAME).toBe(true)
|
||||
|
||||
// Test non-WSL detection
|
||||
delete process.env.WSL_DISTRO_NAME
|
||||
expect(!!process.env.WSL_DISTRO_NAME).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -21,12 +21,32 @@ type ProcessState = {
|
|||
}
|
||||
|
||||
export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator<ClaudeCodeMessage | string> {
|
||||
const process = runProcess(options)
|
||||
const claudeProcess = runProcess(options)
|
||||
const isWSL = !!process.env.WSL_DISTRO_NAME
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdout,
|
||||
input: claudeProcess.stdout,
|
||||
})
|
||||
|
||||
// WSL-specific heartbeat to prevent hanging
|
||||
let heartbeatInterval: NodeJS.Timeout | null = null
|
||||
if (isWSL) {
|
||||
heartbeatInterval = setInterval(() => {
|
||||
// Send a gentle signal to keep the process alive
|
||||
if (!claudeProcess.killed && claudeProcess.pid) {
|
||||
try {
|
||||
claudeProcess.kill(0) // Signal 0 checks if process is alive without killing it
|
||||
} catch (error) {
|
||||
// Process is dead, clear the heartbeat
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval)
|
||||
heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}, CLAUDE_CODE_WSL_HEARTBEAT_INTERVAL)
|
||||
}
|
||||
|
||||
try {
|
||||
const processState: ProcessState = {
|
||||
error: null,
|
||||
|
|
@ -35,15 +55,15 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
|
|||
partialData: null,
|
||||
}
|
||||
|
||||
process.stderr.on("data", (data) => {
|
||||
claudeProcess.stderr.on("data", (data) => {
|
||||
processState.stderrLogs += data.toString()
|
||||
})
|
||||
|
||||
process.on("close", (code) => {
|
||||
claudeProcess.on("close", (code) => {
|
||||
processState.exitCode = code
|
||||
})
|
||||
|
||||
process.on("error", (err) => {
|
||||
claudeProcess.on("error", (err) => {
|
||||
processState.error = err
|
||||
})
|
||||
|
||||
|
|
@ -69,7 +89,7 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
|
|||
yield processState.partialData
|
||||
}
|
||||
|
||||
const { exitCode } = await process
|
||||
const { exitCode } = await claudeProcess
|
||||
if (exitCode !== null && exitCode !== 0) {
|
||||
const errorOutput = processState.error?.message || processState.stderrLogs?.trim()
|
||||
throw new Error(
|
||||
|
|
@ -77,9 +97,35 @@ export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator
|
|||
)
|
||||
}
|
||||
} finally {
|
||||
// Clean up heartbeat
|
||||
if (heartbeatInterval) {
|
||||
clearInterval(heartbeatInterval)
|
||||
}
|
||||
|
||||
rl.close()
|
||||
if (!process.killed) {
|
||||
process.kill()
|
||||
|
||||
// Enhanced process cleanup for WSL
|
||||
if (!claudeProcess.killed) {
|
||||
if (isWSL) {
|
||||
// For WSL, try graceful termination first, then force kill
|
||||
try {
|
||||
claudeProcess.kill("SIGTERM")
|
||||
// Give it a moment to terminate gracefully
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
if (!claudeProcess.killed) {
|
||||
claudeProcess.kill("SIGKILL")
|
||||
}
|
||||
} catch (error) {
|
||||
// If graceful termination fails, force kill
|
||||
try {
|
||||
claudeProcess.kill("SIGKILL")
|
||||
} catch (killError) {
|
||||
console.warn("Failed to kill Claude Code process:", killError)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
claudeProcess.kill()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,10 +151,13 @@ const claudeCodeTools = [
|
|||
"WebSearch",
|
||||
].join(",")
|
||||
|
||||
const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
|
||||
// WSL environments are more prone to hanging, so use shorter timeout
|
||||
export const CLAUDE_CODE_TIMEOUT = process.env.WSL_DISTRO_NAME ? 300000 : 600000 // 5 minutes for WSL, 10 minutes otherwise
|
||||
export const CLAUDE_CODE_WSL_HEARTBEAT_INTERVAL = 30000 // 30 seconds heartbeat for WSL
|
||||
|
||||
function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) {
|
||||
const claudePath = path || "claude"
|
||||
const isWSL = !!process.env.WSL_DISTRO_NAME
|
||||
|
||||
const args = [
|
||||
"-p",
|
||||
|
|
@ -129,18 +178,39 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
|
|||
args.push("--model", modelId)
|
||||
}
|
||||
|
||||
// WSL-specific environment variables to improve stability
|
||||
const wslEnvVars = isWSL
|
||||
? {
|
||||
// Prevent WSL from going to sleep
|
||||
WSLENV: "CLAUDE_CODE_MAX_OUTPUT_TOKENS/u",
|
||||
// Force UTF-8 encoding to prevent character encoding issues
|
||||
LC_ALL: "C.UTF-8",
|
||||
LANG: "C.UTF-8",
|
||||
// Disable Windows path translation that can cause issues
|
||||
WSLPATH_DISABLE: "1",
|
||||
}
|
||||
: {}
|
||||
|
||||
return execa(claudePath, args, {
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: {
|
||||
...process.env,
|
||||
...wslEnvVars,
|
||||
// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
|
||||
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
|
||||
},
|
||||
cwd,
|
||||
maxBuffer: 1024 * 1024 * 1000,
|
||||
timeout: CLAUDE_CODE_TIMEOUT,
|
||||
// WSL-specific options
|
||||
...(isWSL && {
|
||||
// Use a more aggressive cleanup strategy for WSL
|
||||
cleanup: true,
|
||||
// Kill the process group to ensure all child processes are terminated
|
||||
killSignal: "SIGKILL",
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue