@@ -39,6 +39,9 @@ export interface SshProcessMonitorOptions {
3939 remoteSshExtensionId : string ;
4040}
4141
42+ // 1 hour cleanup threshold for old network info files
43+ const CLEANUP_MAX_AGE_MS = 60 * 60 * 1000 ;
44+
4245/**
4346 * Monitors the SSH process for a Coder workspace connection and displays
4447 * network status in the VS Code status bar.
@@ -70,6 +73,50 @@ export class SshProcessMonitor implements vscode.Disposable {
7073 private pendingTimeout : NodeJS . Timeout | undefined ;
7174 private lastStaleSearchTime = 0 ;
7275
76+ /**
77+ * Cleans up network info files older than the specified age.
78+ */
79+ private static async cleanupOldNetworkFiles (
80+ networkInfoPath : string ,
81+ maxAgeMs : number ,
82+ logger : Logger ,
83+ ) : Promise < void > {
84+ try {
85+ const files = await fs . readdir ( networkInfoPath ) ;
86+ const now = Date . now ( ) ;
87+
88+ const deletedFiles : string [ ] = [ ] ;
89+ for ( const file of files ) {
90+ if ( ! file . endsWith ( ".json" ) ) {
91+ continue ;
92+ }
93+
94+ const filePath = path . join ( networkInfoPath , file ) ;
95+ try {
96+ const stats = await fs . stat ( filePath ) ;
97+ const ageMs = now - stats . mtime . getTime ( ) ;
98+
99+ if ( ageMs > maxAgeMs ) {
100+ await fs . unlink ( filePath ) ;
101+ deletedFiles . push ( file ) ;
102+ }
103+ } catch ( error ) {
104+ if ( ( error as NodeJS . ErrnoException ) . code !== "ENOENT" ) {
105+ logger . debug ( `Failed to clean up network info file ${ file } ` , error ) ;
106+ }
107+ }
108+ }
109+
110+ if ( deletedFiles . length > 0 ) {
111+ logger . debug (
112+ `Cleaned up ${ deletedFiles . length } old network info file(s): ${ deletedFiles . join ( ", " ) } ` ,
113+ ) ;
114+ }
115+ } catch {
116+ // Directory may not exist yet, ignore
117+ }
118+ }
119+
73120 private constructor ( options : SshProcessMonitorOptions ) {
74121 this . options = {
75122 ...options ,
@@ -91,6 +138,16 @@ export class SshProcessMonitor implements vscode.Disposable {
91138 */
92139 public static start ( options : SshProcessMonitorOptions ) : SshProcessMonitor {
93140 const monitor = new SshProcessMonitor ( options ) ;
141+
142+ // Clean up old network info files (non-blocking, fire-and-forget)
143+ SshProcessMonitor . cleanupOldNetworkFiles (
144+ options . networkInfoPath ,
145+ CLEANUP_MAX_AGE_MS ,
146+ options . logger ,
147+ ) . catch ( ( ) => {
148+ // Ignore cleanup errors - they shouldn't affect monitoring
149+ } ) ;
150+
94151 monitor . searchForProcess ( ) . catch ( ( err ) => {
95152 options . logger . error ( "Error in SSH process monitor" , err ) ;
96153 } ) ;
@@ -284,48 +341,62 @@ export class SshProcessMonitor implements vscode.Disposable {
284341
285342 /**
286343 * Monitors network info and updates the status bar.
287- * Checks file mtime to detect stale connections and trigger reconnection search .
344+ * Searches for a new process if the file is stale or unreadable .
288345 */
289346 private async monitorNetwork ( ) : Promise < void > {
290347 const { networkInfoPath, networkPollInterval, logger } = this . options ;
291348 const staleThreshold = networkPollInterval * 5 ;
349+ const maxReadFailures = 5 ;
350+ let readFailures = 0 ;
292351
293352 while ( ! this . disposed && this . currentPid !== undefined ) {
294- const networkInfoFile = path . join (
295- networkInfoPath ,
296- ` ${ this . currentPid } .json` ,
297- ) ;
353+ const filePath = path . join ( networkInfoPath , ` ${ this . currentPid } .json` ) ;
354+ let search : { needed : true ; reason : string } | { needed : false } = {
355+ needed : false ,
356+ } ;
298357
299358 try {
300- const stats = await fs . stat ( networkInfoFile ) ;
359+ const stats = await fs . stat ( filePath ) ;
301360 const ageMs = Date . now ( ) - stats . mtime . getTime ( ) ;
361+ readFailures = 0 ;
302362
303363 if ( ageMs > staleThreshold ) {
304- // Prevent tight loop: if we just searched due to stale, wait before searching again
305- const timeSinceLastSearch = Date . now ( ) - this . lastStaleSearchTime ;
306- if ( timeSinceLastSearch < staleThreshold ) {
307- await this . delay ( staleThreshold - timeSinceLastSearch ) ;
308- continue ;
309- }
310-
311- logger . debug (
312- `Network info stale (${ Math . round ( ageMs / 1000 ) } s old), searching for new SSH process` ,
313- ) ;
314-
315- // searchForProcess will update PID if a different process is found
316- this . lastStaleSearchTime = Date . now ( ) ;
317- await this . searchForProcess ( ) ;
318- return ;
364+ search = {
365+ needed : true ,
366+ reason : `Network info stale (${ Math . round ( ageMs / 1000 ) } s old)` ,
367+ } ;
368+ } else {
369+ const content = await fs . readFile ( filePath , "utf8" ) ;
370+ const network = JSON . parse ( content ) as NetworkInfo ;
371+ const isStale = ageMs > networkPollInterval * 2 ;
372+ this . updateStatusBar ( network , isStale ) ;
319373 }
320-
321- const content = await fs . readFile ( networkInfoFile , "utf8" ) ;
322- const network = JSON . parse ( content ) as NetworkInfo ;
323- const isStale = ageMs > this . options . networkPollInterval * 2 ;
324- this . updateStatusBar ( network , isStale ) ;
325374 } catch ( error ) {
375+ readFailures ++ ;
326376 logger . debug (
327- `Failed to read network info: ${ ( error as Error ) . message } ` ,
377+ `Failed to read network info (attempt ${ readFailures } ) : ${ ( error as Error ) . message } ` ,
328378 ) ;
379+ if ( readFailures >= maxReadFailures ) {
380+ search = {
381+ needed : true ,
382+ reason : `Network info missing for ${ readFailures } attempts` ,
383+ } ;
384+ }
385+ }
386+
387+ // Search for new process if needed (with throttling)
388+ if ( search . needed ) {
389+ const timeSinceLastSearch = Date . now ( ) - this . lastStaleSearchTime ;
390+ if ( timeSinceLastSearch < staleThreshold ) {
391+ await this . delay ( staleThreshold - timeSinceLastSearch ) ;
392+ continue ;
393+ }
394+
395+ logger . debug ( `${ search . reason } , searching for new SSH process` ) ;
396+ // searchForProcess will update PID if a different process is found
397+ this . lastStaleSearchTime = Date . now ( ) ;
398+ await this . searchForProcess ( ) ;
399+ return ;
329400 }
330401
331402 await this . delay ( networkPollInterval ) ;
0 commit comments