Skip to content

Commit ba2077c

Browse files
committed
fix: improve IPC stability in headless Docker environments with xvfb
- Add robust error handling and recovery mechanisms to IPC server/client - Implement graceful shutdown handling for IPC connections - Add connection timeouts and retry logic for headless environments - Enhance logging for debugging in virtual display scenarios - Handle SIGTERM, SIGINT, and SIGHUP signals properly - Add socket cleanup and directory creation for Docker containers - Implement reconnection logic with exponential backoff This fix addresses the issue where VSCode gets killed (signal 9) when running RooCode in headless Docker environments with xvfb during IPC operations. The improvements make IPC communication more resilient to the challenges of virtual display environments. Fixes #7814
1 parent 195f4eb commit ba2077c

File tree

4 files changed

+644
-105
lines changed

4 files changed

+644
-105
lines changed

packages/ipc/src/ipc-client.ts

Lines changed: 230 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,27 @@ import {
1212
ipcMessageSchema,
1313
} from "@roo-code/types"
1414

15+
// Configuration for headless environments
16+
const HEADLESS_CONFIG = {
17+
// Increase retry attempts for headless environments
18+
maxRetries: 10,
19+
// Increase retry delay for slower environments
20+
retryDelay: 1000,
21+
// Connection timeout for headless environments (ms)
22+
connectionTimeout: 30000,
23+
// Enable verbose logging in headless mode
24+
verboseLogging: process.env.DISPLAY === ":99" || process.env.XVFB_DISPLAY !== undefined,
25+
}
26+
1527
export class IpcClient extends EventEmitter<IpcClientEvents> {
1628
private readonly _socketPath: string
1729
private readonly _id: string
1830
private readonly _log: (...args: unknown[]) => void
1931
private _isConnected = false
2032
private _clientId?: string
33+
private _connectionTimeout?: NodeJS.Timeout
34+
private _shutdownInProgress = false
35+
private _reconnectAttempts = 0
2136

2237
constructor(socketPath: string, log = console.log) {
2338
super()
@@ -26,91 +41,277 @@ export class IpcClient extends EventEmitter<IpcClientEvents> {
2641
this._id = `roo-code-evals-${crypto.randomBytes(6).toString("hex")}`
2742
this._log = log
2843

44+
// Configure IPC for headless environments
2945
ipc.config.silent = true
46+
ipc.config.retry = HEADLESS_CONFIG.retryDelay
47+
ipc.config.maxRetries = HEADLESS_CONFIG.maxRetries
48+
ipc.config.stopRetrying = false
49+
50+
this.setupConnection()
51+
this.setupShutdownHandlers()
52+
}
53+
54+
private setupConnection() {
55+
try {
56+
ipc.connectTo(this._id, this.socketPath, () => {
57+
ipc.of[this._id]?.on("connect", () => this.onConnect())
58+
ipc.of[this._id]?.on("disconnect", () => this.onDisconnect())
59+
ipc.of[this._id]?.on("message", (data) => this.onMessage(data))
60+
ipc.of[this._id]?.on("error", (error) => this.onError(error))
61+
})
62+
63+
// Set connection timeout for headless environments
64+
if (HEADLESS_CONFIG.verboseLogging) {
65+
this._connectionTimeout = setTimeout(() => {
66+
if (!this._isConnected && !this._shutdownInProgress) {
67+
this.log(
68+
`[client#setupConnection] Connection timeout after ${HEADLESS_CONFIG.connectionTimeout}ms`,
69+
)
70+
this.handleConnectionFailure()
71+
}
72+
}, HEADLESS_CONFIG.connectionTimeout)
73+
}
74+
} catch (error) {
75+
this.log(`[client#setupConnection] Error setting up connection: ${error}`)
76+
this.handleConnectionFailure()
77+
}
78+
}
79+
80+
private setupShutdownHandlers() {
81+
const gracefulShutdown = async (signal: string) => {
82+
if (this._shutdownInProgress) {
83+
return
84+
}
85+
86+
this._shutdownInProgress = true
87+
this.log(`[IpcClient] Received ${signal}, initiating graceful shutdown...`)
88+
89+
try {
90+
await this.shutdown()
91+
} catch (error) {
92+
this.log(`[IpcClient] Error during shutdown: ${error}`)
93+
}
94+
}
95+
96+
// Handle various termination signals
97+
process.once("SIGTERM", () => gracefulShutdown("SIGTERM"))
98+
process.once("SIGINT", () => gracefulShutdown("SIGINT"))
99+
process.once("SIGHUP", () => gracefulShutdown("SIGHUP"))
100+
}
101+
102+
private handleConnectionFailure() {
103+
if (this._shutdownInProgress) {
104+
return
105+
}
106+
107+
this._reconnectAttempts++
108+
109+
if (this._reconnectAttempts >= HEADLESS_CONFIG.maxRetries) {
110+
this.log(
111+
`[client#handleConnectionFailure] Max reconnection attempts (${HEADLESS_CONFIG.maxRetries}) reached`,
112+
)
113+
this.emit(IpcMessageType.Disconnect)
114+
return
115+
}
116+
117+
this.log(
118+
`[client#handleConnectionFailure] Attempting reconnection ${this._reconnectAttempts}/${HEADLESS_CONFIG.maxRetries}`,
119+
)
120+
121+
// Clear existing connection
122+
if (ipc.of[this._id]) {
123+
ipc.disconnect(this._id)
124+
}
30125

31-
ipc.connectTo(this._id, this.socketPath, () => {
32-
ipc.of[this._id]?.on("connect", () => this.onConnect())
33-
ipc.of[this._id]?.on("disconnect", () => this.onDisconnect())
34-
ipc.of[this._id]?.on("message", (data) => this.onMessage(data))
35-
})
126+
// Wait before reconnecting
127+
setTimeout(() => {
128+
if (!this._shutdownInProgress) {
129+
this.setupConnection()
130+
}
131+
}, HEADLESS_CONFIG.retryDelay * this._reconnectAttempts)
132+
}
133+
134+
private onError(error: unknown) {
135+
this.log(`[client#onError] IPC client error: ${error}`)
136+
137+
// In headless environments, try to recover from errors
138+
if (HEADLESS_CONFIG.verboseLogging && !this._shutdownInProgress) {
139+
this.log("[client#onError] Attempting to recover from error in headless environment...")
140+
this.handleConnectionFailure()
141+
}
36142
}
37143

38144
private onConnect() {
39-
if (this._isConnected) {
145+
if (this._isConnected || this._shutdownInProgress) {
40146
return
41147
}
42148

149+
// Clear connection timeout
150+
if (this._connectionTimeout) {
151+
clearTimeout(this._connectionTimeout)
152+
this._connectionTimeout = undefined
153+
}
154+
43155
this.log("[client#onConnect]")
44156
this._isConnected = true
157+
this._reconnectAttempts = 0 // Reset reconnection attempts on successful connection
45158
this.emit(IpcMessageType.Connect)
46159
}
47160

48161
private onDisconnect() {
49-
if (!this._isConnected) {
162+
if (!this._isConnected || this._shutdownInProgress) {
50163
return
51164
}
52165

53166
this.log("[client#onDisconnect]")
54167
this._isConnected = false
168+
this._clientId = undefined
169+
170+
// Clear connection timeout
171+
if (this._connectionTimeout) {
172+
clearTimeout(this._connectionTimeout)
173+
this._connectionTimeout = undefined
174+
}
175+
55176
this.emit(IpcMessageType.Disconnect)
177+
178+
// Attempt reconnection in headless environments
179+
if (HEADLESS_CONFIG.verboseLogging && !this._shutdownInProgress) {
180+
this.log("[client#onDisconnect] Attempting reconnection in headless environment...")
181+
this.handleConnectionFailure()
182+
}
56183
}
57184

58185
private onMessage(data: unknown) {
59-
if (typeof data !== "object") {
60-
this._log("[client#onMessage] invalid data", data)
186+
if (this._shutdownInProgress) {
187+
this.log("[client#onMessage] Ignoring message - shutdown in progress")
61188
return
62189
}
63190

64-
const result = ipcMessageSchema.safeParse(data)
191+
try {
192+
if (typeof data !== "object") {
193+
this._log("[client#onMessage] invalid data", data)
194+
return
195+
}
65196

66-
if (!result.success) {
67-
this.log("[client#onMessage] invalid payload", result.error, data)
68-
return
69-
}
197+
const result = ipcMessageSchema.safeParse(data)
198+
199+
if (!result.success) {
200+
this.log("[client#onMessage] invalid payload", result.error, data)
201+
return
202+
}
70203

71-
const payload = result.data
204+
const payload = result.data
72205

73-
if (payload.origin === IpcOrigin.Server) {
74-
switch (payload.type) {
75-
case IpcMessageType.Ack:
76-
this._clientId = payload.data.clientId
77-
this.emit(IpcMessageType.Ack, payload.data)
78-
break
79-
case IpcMessageType.TaskEvent:
80-
this.emit(IpcMessageType.TaskEvent, payload.data)
81-
break
206+
if (payload.origin === IpcOrigin.Server) {
207+
switch (payload.type) {
208+
case IpcMessageType.Ack:
209+
this._clientId = payload.data.clientId
210+
this.emit(IpcMessageType.Ack, payload.data)
211+
break
212+
case IpcMessageType.TaskEvent:
213+
this.emit(IpcMessageType.TaskEvent, payload.data)
214+
break
215+
}
216+
}
217+
} catch (error) {
218+
this.log(`[client#onMessage] Error processing message: ${error}`)
219+
if (HEADLESS_CONFIG.verboseLogging) {
220+
this.log(`[client#onMessage] Message data: ${JSON.stringify(data)}`)
82221
}
83222
}
84223
}
85224

86225
private log(...args: unknown[]) {
87-
this._log(...args)
226+
// Add timestamp and process info in headless mode
227+
if (HEADLESS_CONFIG.verboseLogging) {
228+
const timestamp = new Date().toISOString()
229+
const processInfo = `[PID:${process.pid}]`
230+
this._log(timestamp, processInfo, ...args)
231+
} else {
232+
this._log(...args)
233+
}
88234
}
89235

90236
public sendCommand(command: TaskCommand) {
237+
if (this._shutdownInProgress) {
238+
this.log("[client#sendCommand] Cannot send command - shutdown in progress")
239+
return
240+
}
241+
242+
if (!this._clientId) {
243+
this.log("[client#sendCommand] Cannot send command - no client ID")
244+
return
245+
}
246+
91247
const message: IpcMessage = {
92248
type: IpcMessageType.TaskCommand,
93249
origin: IpcOrigin.Client,
94-
clientId: this._clientId!,
250+
clientId: this._clientId,
95251
data: command,
96252
}
97253

98254
this.sendMessage(message)
99255
}
100256

101257
public sendMessage(message: IpcMessage) {
102-
ipc.of[this._id]?.emit("message", message)
258+
if (this._shutdownInProgress) {
259+
this.log("[client#sendMessage] Cannot send message - shutdown in progress")
260+
return
261+
}
262+
263+
try {
264+
const connection = ipc.of[this._id]
265+
if (connection) {
266+
connection.emit("message", message)
267+
} else {
268+
this.log("[client#sendMessage] IPC connection not available")
269+
}
270+
} catch (error) {
271+
this.log(`[client#sendMessage] Error sending message: ${error}`)
272+
}
103273
}
104274

105275
public disconnect() {
106276
try {
107-
ipc.disconnect(this._id)
108-
// @TODO: Should we set _disconnect here?
277+
this._isConnected = false
278+
this._clientId = undefined
279+
280+
if (this._connectionTimeout) {
281+
clearTimeout(this._connectionTimeout)
282+
this._connectionTimeout = undefined
283+
}
284+
285+
if (ipc.of[this._id]) {
286+
ipc.disconnect(this._id)
287+
}
109288
} catch (error) {
110289
this.log("[client#disconnect] error disconnecting", error)
111290
}
112291
}
113292

293+
public async shutdown(): Promise<void> {
294+
this.log("[IpcClient] Starting graceful shutdown...")
295+
296+
try {
297+
this._shutdownInProgress = true
298+
299+
// Clear connection timeout
300+
if (this._connectionTimeout) {
301+
clearTimeout(this._connectionTimeout)
302+
this._connectionTimeout = undefined
303+
}
304+
305+
// Disconnect from server
306+
this.disconnect()
307+
308+
this.log("[IpcClient] Graceful shutdown completed")
309+
} catch (error) {
310+
this.log(`[IpcClient] Error during shutdown: ${error}`)
311+
throw error
312+
}
313+
}
314+
114315
public get socketPath() {
115316
return this._socketPath
116317
}

0 commit comments

Comments
 (0)