From 5da83f918b607d797b1267f5781070b5afdab1c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 17:42:40 +0000 Subject: [PATCH 1/3] Initial plan From 40f2213fd7f4f948c6d4ccf598da903abef62044 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 17:48:17 +0000 Subject: [PATCH 2/3] Add optional exec websocket keepalive pings --- src/exec.ts | 28 ++++++++++++++++++++++++++++ src/exec_test.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/exec.ts b/src/exec.ts index 090802b31fe..1ad4f0637af 100644 --- a/src/exec.ts +++ b/src/exec.ts @@ -7,6 +7,10 @@ import { KubeConfig } from './config.js'; import { isResizable, ResizableStream, TerminalSizeQueue } from './terminal-size-queue.js'; import { WebSocketHandler, WebSocketInterface } from './web-socket-handler.js'; +export interface ExecOptions { + pingIntervalMs?: number; +} + export class Exec { public 'handler': WebSocketInterface; @@ -39,6 +43,7 @@ export class Exec { stdin: stream.Readable | null, tty: boolean, statusCallback?: (status: V1Status) => void, + options?: ExecOptions, ): Promise { const query = { stdout: stdout != null, @@ -60,6 +65,10 @@ export class Exec { } return true; }); + const pingIntervalMs = options?.pingIntervalMs; + if (pingIntervalMs !== undefined && pingIntervalMs > 0) { + this.setupPing(conn, pingIntervalMs); + } if (stdin != null) { WebSocketHandler.handleStandardInput(conn, stdin, WebSocketHandler.StdinStream); } @@ -70,4 +79,23 @@ export class Exec { } return conn; } + + private setupPing(conn: WebSocket.WebSocket, pingIntervalMs: number): void { + const pingableConnection = conn as WebSocket.WebSocket & { + ping?: () => void; + on?: (event: string, listener: () => void) => void; + }; + if (typeof pingableConnection.ping !== 'function') { + return; + } + + const timer = setInterval(() => { + if (conn.readyState === WebSocket.OPEN) { + pingableConnection.ping!(); + } + }, pingIntervalMs); + const clearKeepAlive = () => clearInterval(timer); + pingableConnection.on?.('close', clearKeepAlive); + pingableConnection.on?.('error', clearKeepAlive); + } } diff --git a/src/exec_test.ts b/src/exec_test.ts index 405558b0c11..86c80872a9a 100644 --- a/src/exec_test.ts +++ b/src/exec_test.ts @@ -1,5 +1,6 @@ import { describe, it } from 'node:test'; import { deepStrictEqual, strictEqual } from 'node:assert'; +import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; import WebSocket from 'isomorphic-ws'; import { ReadableStreamBuffer, WritableStreamBuffer } from 'stream-buffers'; import { anyFunction, anything, capture, instance, mock, verify, when } from 'ts-mockito'; @@ -156,5 +157,38 @@ describe('Exec', () => { await closePromise; verify(fakeWebSocket.close()).called(); }); + + it('should optionally send websocket pings', async () => { + const kc = new KubeConfig(); + const pingHandlers: Record void> = {}; + let pingCount = 0; + const fakeConn = { + readyState: WebSocket.OPEN, + ping: () => { + pingCount++; + }, + on: (event: string, listener: () => void) => { + pingHandlers[event] = listener; + }, + } as unknown as WebSocket.WebSocket; + const ws: WebSocketInterface = { + connect: async () => fakeConn, + }; + const exec = new Exec(kc, ws); + + await exec.exec('ns', 'pod', 'container', 'command', null, null, null, false, undefined, { + pingIntervalMs: 5, + }); + await setTimeoutPromise(25); + + strictEqual(pingCount > 0, true); + + strictEqual(typeof pingHandlers.close, 'function'); + pingHandlers.close(); + const pingCountAtClose = pingCount; + await setTimeoutPromise(25); + + strictEqual(pingCount, pingCountAtClose); + }); }); }); From 6e6fe182fcda2562cd1151d0b55b3f4c9231e3b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 17:52:20 +0000 Subject: [PATCH 3/3] Refine exec ping keepalive validation and tests --- src/exec.ts | 12 ++++++------ src/exec_test.ts | 12 +++++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/exec.ts b/src/exec.ts index 1ad4f0637af..32ed0360edb 100644 --- a/src/exec.ts +++ b/src/exec.ts @@ -66,7 +66,7 @@ export class Exec { return true; }); const pingIntervalMs = options?.pingIntervalMs; - if (pingIntervalMs !== undefined && pingIntervalMs > 0) { + if (pingIntervalMs !== undefined && Number.isInteger(pingIntervalMs) && pingIntervalMs > 0) { this.setupPing(conn, pingIntervalMs); } if (stdin != null) { @@ -81,21 +81,21 @@ export class Exec { } private setupPing(conn: WebSocket.WebSocket, pingIntervalMs: number): void { - const pingableConnection = conn as WebSocket.WebSocket & { + const socket = conn as WebSocket.WebSocket & { ping?: () => void; on?: (event: string, listener: () => void) => void; }; - if (typeof pingableConnection.ping !== 'function') { + if (typeof socket.ping !== 'function') { return; } const timer = setInterval(() => { if (conn.readyState === WebSocket.OPEN) { - pingableConnection.ping!(); + socket.ping!(); } }, pingIntervalMs); const clearKeepAlive = () => clearInterval(timer); - pingableConnection.on?.('close', clearKeepAlive); - pingableConnection.on?.('error', clearKeepAlive); + socket.on?.('close', clearKeepAlive); + socket.on?.('error', clearKeepAlive); } } diff --git a/src/exec_test.ts b/src/exec_test.ts index 86c80872a9a..1042a045d20 100644 --- a/src/exec_test.ts +++ b/src/exec_test.ts @@ -1,5 +1,5 @@ import { describe, it } from 'node:test'; -import { deepStrictEqual, strictEqual } from 'node:assert'; +import { deepStrictEqual, ok, strictEqual } from 'node:assert'; import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; import WebSocket from 'isomorphic-ws'; import { ReadableStreamBuffer, WritableStreamBuffer } from 'stream-buffers'; @@ -159,6 +159,8 @@ describe('Exec', () => { }); it('should optionally send websocket pings', async () => { + const pingIntervalMs = 5; + const waitForPingsMs = 25; const kc = new KubeConfig(); const pingHandlers: Record void> = {}; let pingCount = 0; @@ -177,16 +179,16 @@ describe('Exec', () => { const exec = new Exec(kc, ws); await exec.exec('ns', 'pod', 'container', 'command', null, null, null, false, undefined, { - pingIntervalMs: 5, + pingIntervalMs, }); - await setTimeoutPromise(25); + await setTimeoutPromise(waitForPingsMs); - strictEqual(pingCount > 0, true); + ok(pingCount > 0); strictEqual(typeof pingHandlers.close, 'function'); pingHandlers.close(); const pingCountAtClose = pingCount; - await setTimeoutPromise(25); + await setTimeoutPromise(waitForPingsMs); strictEqual(pingCount, pingCountAtClose); });