From a99288f25a1ff0939fa7b6581d8d22a9fa75e4fd Mon Sep 17 00:00:00 2001 From: alanw Date: Fri, 16 Jan 2026 03:27:48 +0000 Subject: [PATCH 1/4] fix(resource-monitoring): prevent file descriptor count leak by adding caching --- .../ResourceMonitoringService.test.ts | 57 +++++++++++++++++++ .../src/services/ResourceMonitoringService.ts | 36 ++++++++---- 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/extensions/cli/src/services/ResourceMonitoringService.test.ts b/extensions/cli/src/services/ResourceMonitoringService.test.ts index a0ef2017b..bc67665a0 100644 --- a/extensions/cli/src/services/ResourceMonitoringService.test.ts +++ b/extensions/cli/src/services/ResourceMonitoringService.test.ts @@ -105,4 +105,61 @@ describe("ResourceMonitoringService", () => { await verboseService.cleanup(); process.argv = originalArgv; }); + + it("should cache file descriptor count and avoid lsof command leak", async () => { + service.startMonitoring(100); // 100ms interval for test + + // Wait for multiple collection cycles + await new Promise((resolve) => setTimeout(resolve, 350)); + + const history = service.getResourceHistory(); + expect(history.length).toBeGreaterThan(0); + + // All entries should have the same cached file descriptor count + // (or undefined if lsof failed, which is acceptable) + const fdCounts = history + .map((h) => h.fileDescriptors) + .filter((fd) => fd !== undefined); + + if (fdCounts.length > 0) { + // All cached values should be the same (proves caching works) + const firstFd = fdCounts[0]; + const allSame = fdCounts.every((fd) => fd === firstFd); + expect(allSame).toBe(true); + } + + service.stopMonitoring(); + }); + + it("should update file descriptor count after interval", async () => { + // Use a shorter interval for testing (500ms instead of 5000ms) + // This allows us to verify the update behavior without long waits + const testInterval = 500; + + service.startMonitoring(100); // 100ms collection interval + + // Wait for initial FD check and get first value + await new Promise((resolve) => setTimeout(resolve, 150)); + const initialUsage = service.getCurrentResourceUsage(); + const initialFd = initialUsage.fileDescriptors; + + // Wait for the FD update cycle to complete + // The service should update FD count after testInterval (500ms) + await new Promise((resolve) => setTimeout(resolve, testInterval + 200)); + + // Get the updated FD count + const laterUsage = service.getCurrentResourceUsage(); + const laterFd = laterUsage.fileDescriptors; + + // Both should have values (lsof should work in most environments) + if (initialFd !== undefined && laterFd !== undefined) { + // The FD count should be updated after the interval + // Note: The actual value might be the same if no FDs were opened/closed, + // but the important thing is that the cache was refreshed + expect(laterFd).toBeDefined(); + expect(initialFd).toBeDefined(); + } + + service.stopMonitoring(); + }); }); diff --git a/extensions/cli/src/services/ResourceMonitoringService.ts b/extensions/cli/src/services/ResourceMonitoringService.ts index a114d3027..ed86f7f0b 100644 --- a/extensions/cli/src/services/ResourceMonitoringService.ts +++ b/extensions/cli/src/services/ResourceMonitoringService.ts @@ -45,6 +45,9 @@ class ResourceMonitoringService { private maxHistorySize = 300; // Keep 5 minutes at 1s intervals private lastCpuUsage = process.cpuUsage(); private lastTimestamp = Date.now(); + private lastFdCheckTime = 0; + private fdCheckIntervalMs = 5000; // Check file descriptors every 5 seconds + private cacheFileCount: number | null = null; async initialize(): Promise { // Start monitoring if verbose mode is enabled @@ -127,16 +130,14 @@ class ResourceMonitoringService { }, }; - // Try to get file descriptor count (Unix only) - this.getFileDescriptorCount() - .then((count) => { - if (count !== null) { - usage.fileDescriptors = count; - } - }) - .catch(() => { - // Ignore errors for file descriptor counting - }); + // Initialize file descriptor count on start + this.updateFileDescriptorCount().catch(() => { + // Ignore errors during initialization + }); + + if (this.cacheFileCount !== null) { + usage.fileDescriptors = this.cacheFileCount; + } return usage; } @@ -215,6 +216,14 @@ class ResourceMonitoringService { this.resourceHistory = this.resourceHistory.slice(-this.maxHistorySize); } + // Periodically update file descriptor count to prevent lsof command leak + // Issue: https://github.com/continuedev/continue/issues/9422 + const now = Date.now(); + if (now - this.lastFdCheckTime >= this.fdCheckIntervalMs) { + this.updateFileDescriptorCount(); + this.lastFdCheckTime = now; + } + // Check for potential issues and log warnings this.checkResourceThresholds(usage); } catch (error) { @@ -251,6 +260,13 @@ class ResourceMonitoringService { } } + private async updateFileDescriptorCount(): Promise { + const count = await this.getFileDescriptorCount(); + if (count !== null) { + this.cacheFileCount = count; + } + } + private async getFileDescriptorCount(): Promise { if (process.platform === "win32") { return null; // Not supported on Windows From df83e66d5ce97cfcf28c1a815d567090cceda622 Mon Sep 17 00:00:00 2001 From: alanw Date: Fri, 16 Jan 2026 04:20:46 +0000 Subject: [PATCH 2/4] fix(ResourceMonitoringService): initialize fd check time and count on start --- .../cli/src/services/ResourceMonitoringService.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/extensions/cli/src/services/ResourceMonitoringService.ts b/extensions/cli/src/services/ResourceMonitoringService.ts index ed86f7f0b..a08a15f2f 100644 --- a/extensions/cli/src/services/ResourceMonitoringService.ts +++ b/extensions/cli/src/services/ResourceMonitoringService.ts @@ -45,7 +45,7 @@ class ResourceMonitoringService { private maxHistorySize = 300; // Keep 5 minutes at 1s intervals private lastCpuUsage = process.cpuUsage(); private lastTimestamp = Date.now(); - private lastFdCheckTime = 0; + private lastFdCheckTime = Date.now(); private fdCheckIntervalMs = 5000; // Check file descriptors every 5 seconds private cacheFileCount: number | null = null; @@ -73,6 +73,12 @@ class ResourceMonitoringService { this.lastCpuUsage = process.cpuUsage(); this.lastTimestamp = Date.now(); + // Initialize file descriptor count on start + this.updateFileDescriptorCount().catch(() => { + // Ignore errors during initialization + }); + this.lastFdCheckTime = Date.now(); + this.monitoringInterval = setInterval(() => { this.collectResourceUsage(); }, this.intervalMs); @@ -130,11 +136,7 @@ class ResourceMonitoringService { }, }; - // Initialize file descriptor count on start - this.updateFileDescriptorCount().catch(() => { - // Ignore errors during initialization - }); - + // Use cached file descriptor count to avoid lsof command leak if (this.cacheFileCount !== null) { usage.fileDescriptors = this.cacheFileCount; } From 676e31d28edd634307e289ee103ec6f42a40f416 Mon Sep 17 00:00:00 2001 From: alanw Date: Fri, 16 Jan 2026 05:17:39 +0000 Subject: [PATCH 3/4] fix(ResourceMonitoringService): prevent negative file descriptor counts and remove test cases --- .../ResourceMonitoringService.test.ts | 57 ------------------- .../src/services/ResourceMonitoringService.ts | 6 +- 2 files changed, 5 insertions(+), 58 deletions(-) diff --git a/extensions/cli/src/services/ResourceMonitoringService.test.ts b/extensions/cli/src/services/ResourceMonitoringService.test.ts index bc67665a0..a0ef2017b 100644 --- a/extensions/cli/src/services/ResourceMonitoringService.test.ts +++ b/extensions/cli/src/services/ResourceMonitoringService.test.ts @@ -105,61 +105,4 @@ describe("ResourceMonitoringService", () => { await verboseService.cleanup(); process.argv = originalArgv; }); - - it("should cache file descriptor count and avoid lsof command leak", async () => { - service.startMonitoring(100); // 100ms interval for test - - // Wait for multiple collection cycles - await new Promise((resolve) => setTimeout(resolve, 350)); - - const history = service.getResourceHistory(); - expect(history.length).toBeGreaterThan(0); - - // All entries should have the same cached file descriptor count - // (or undefined if lsof failed, which is acceptable) - const fdCounts = history - .map((h) => h.fileDescriptors) - .filter((fd) => fd !== undefined); - - if (fdCounts.length > 0) { - // All cached values should be the same (proves caching works) - const firstFd = fdCounts[0]; - const allSame = fdCounts.every((fd) => fd === firstFd); - expect(allSame).toBe(true); - } - - service.stopMonitoring(); - }); - - it("should update file descriptor count after interval", async () => { - // Use a shorter interval for testing (500ms instead of 5000ms) - // This allows us to verify the update behavior without long waits - const testInterval = 500; - - service.startMonitoring(100); // 100ms collection interval - - // Wait for initial FD check and get first value - await new Promise((resolve) => setTimeout(resolve, 150)); - const initialUsage = service.getCurrentResourceUsage(); - const initialFd = initialUsage.fileDescriptors; - - // Wait for the FD update cycle to complete - // The service should update FD count after testInterval (500ms) - await new Promise((resolve) => setTimeout(resolve, testInterval + 200)); - - // Get the updated FD count - const laterUsage = service.getCurrentResourceUsage(); - const laterFd = laterUsage.fileDescriptors; - - // Both should have values (lsof should work in most environments) - if (initialFd !== undefined && laterFd !== undefined) { - // The FD count should be updated after the interval - // Note: The actual value might be the same if no FDs were opened/closed, - // but the important thing is that the cache was refreshed - expect(laterFd).toBeDefined(); - expect(initialFd).toBeDefined(); - } - - service.stopMonitoring(); - }); }); diff --git a/extensions/cli/src/services/ResourceMonitoringService.ts b/extensions/cli/src/services/ResourceMonitoringService.ts index a08a15f2f..9e812394b 100644 --- a/extensions/cli/src/services/ResourceMonitoringService.ts +++ b/extensions/cli/src/services/ResourceMonitoringService.ts @@ -276,7 +276,11 @@ class ResourceMonitoringService { try { const { stdout } = await execAsync(`lsof -p ${process.pid} | wc -l`); - return parseInt(stdout.trim(), 10) - 1; // Subtract 1 for header line + const count = parseInt(stdout.trim(), 10) - 1; // Subtract 1 for header line + + // Ensure we don't return negative values + // This can happen if lsof finds no file descriptors + return Math.max(0, count); } catch { return null; } From 056622faa01779b2dd0c99fe5ea758951454822a Mon Sep 17 00:00:00 2001 From: alanw Date: Mon, 19 Jan 2026 01:46:26 +0000 Subject: [PATCH 4/4] fix: simplify file descriptor count calculation --- extensions/cli/src/services/ResourceMonitoringService.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/extensions/cli/src/services/ResourceMonitoringService.ts b/extensions/cli/src/services/ResourceMonitoringService.ts index 9e812394b..a08a15f2f 100644 --- a/extensions/cli/src/services/ResourceMonitoringService.ts +++ b/extensions/cli/src/services/ResourceMonitoringService.ts @@ -276,11 +276,7 @@ class ResourceMonitoringService { try { const { stdout } = await execAsync(`lsof -p ${process.pid} | wc -l`); - const count = parseInt(stdout.trim(), 10) - 1; // Subtract 1 for header line - - // Ensure we don't return negative values - // This can happen if lsof finds no file descriptors - return Math.max(0, count); + return parseInt(stdout.trim(), 10) - 1; // Subtract 1 for header line } catch { return null; }