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:
Roo Code 2025-06-30 10:15:10 +00:00
parent 3a8ba27615
commit 381ef21996
2 changed files with 129 additions and 9 deletions

View 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)
})
})

View file

@ -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",
}),
})
}