@@ -178,7 +178,7 @@ const parseLsofOutput = (
178178 raw : string ,
179179 terminalByProcessId : ReadonlyMap < number , TerminalProcessOwner > = new Map ( ) ,
180180) : ReadonlyArray < DiscoveredLocalServer > => {
181- const seen = new Map < string , DiscoveredLocalServer > ( ) ;
181+ const listeners : DiscoveredLocalServer [ ] = [ ] ;
182182 let pid : number | null = null ;
183183 let processName : string | null = null ;
184184
@@ -199,26 +199,18 @@ const parseLsofOutput = (
199199 if ( tag === "n" ) {
200200 const portMatch = parsePortFromLsofName ( value ) ;
201201 if ( portMatch == null ) continue ;
202- const url = `http://localhost:${ portMatch } ` ;
203- const key = `localhost:${ portMatch } ` ;
204- const terminal = pid === null ? null : ( terminalByProcessId . get ( pid ) ?? null ) ;
205- const existing = seen . get ( key ) ;
206- if ( existing ) {
207- if ( terminal === null ) seen . set ( key , { ...existing , terminal : null } ) ;
208- continue ;
209- }
210- seen . set ( key , {
202+ listeners . push ( {
211203 host : "localhost" ,
212204 port : portMatch ,
213- url,
205+ url : `http://localhost: ${ portMatch } ` ,
214206 processName,
215207 pid,
216- terminal,
208+ terminal : pid === null ? null : ( terminalByProcessId . get ( pid ) ?? null ) ,
217209 } ) ;
218210 }
219211 }
220212
221- return Array . from ( seen . values ( ) ) . toSorted ( ( a , b ) => a . port - b . port ) ;
213+ return listeners ;
222214} ;
223215
224216const parsePortFromLsofName = ( name : string ) : number | null => {
@@ -296,28 +288,51 @@ const parseWindowsListenerOutput = (
296288 raw : string ,
297289 terminalByProcessId : ReadonlyMap < number , TerminalProcessOwner > = new Map ( ) ,
298290) : ReadonlyArray < DiscoveredLocalServer > => {
299- const seen = new Map < number , DiscoveredLocalServer > ( ) ;
291+ const listeners : DiscoveredLocalServer [ ] = [ ] ;
300292 for ( const line of raw . split ( / \r ? \n / g) ) {
301293 const parsed = parseWindowsListenerLine ( line ) ;
302294 if ( ! parsed ) continue ;
303- const { port, pid : normalizedPid , processName } = parsed ;
304- const terminal =
305- normalizedPid === null ? null : ( terminalByProcessId . get ( normalizedPid ) ?? null ) ;
306- const existing = seen . get ( port ) ;
307- if ( existing ) {
308- if ( terminal === null ) seen . set ( port , { ...existing , terminal : null } ) ;
309- continue ;
310- }
311- seen . set ( port , {
295+ const { port, pid, processName } = parsed ;
296+ listeners . push ( {
312297 host : "localhost" ,
313298 port,
314299 url : `http://localhost:${ port } ` ,
315300 processName,
316- pid : normalizedPid ,
317- terminal,
301+ pid,
302+ terminal : pid === null ? null : ( terminalByProcessId . get ( pid ) ?? null ) ,
318303 } ) ;
319304 }
320- return [ ...seen . values ( ) ] . toSorted ( ( left , right ) => left . port - right . port ) ;
305+ return listeners ;
306+ } ;
307+
308+ // A port can have several listeners (IPv4 and IPv6, SO_REUSEPORT). It is owned only
309+ // while every listener on it is, so an unrelated process sharing it is never probed.
310+ const collapseListenersByPort = (
311+ listeners : ReadonlyArray < DiscoveredLocalServer > ,
312+ ) : ReadonlyArray < DiscoveredLocalServer > => {
313+ const byPort = new Map < number , DiscoveredLocalServer > ( ) ;
314+ for ( const listener of listeners ) {
315+ const existing = byPort . get ( listener . port ) ;
316+ if ( existing === undefined ) {
317+ byPort . set ( listener . port , listener ) ;
318+ continue ;
319+ }
320+ const kept = existing . pid === null && listener . pid !== null ? listener : existing ;
321+ byPort . set (
322+ listener . port ,
323+ existing . threadId === undefined || listener . threadId === undefined
324+ ? {
325+ host : kept . host ,
326+ port : kept . port ,
327+ url : kept . url ,
328+ processName : kept . processName ,
329+ pid : kept . pid ,
330+ terminal : null ,
331+ }
332+ : kept ,
333+ ) ;
334+ }
335+ return [ ...byPort . values ( ) ] . toSorted ( ( left , right ) => left . port - right . port ) ;
321336} ;
322337
323338const PROC_TCP_LISTEN = "0A" ;
@@ -474,25 +489,17 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
474489 }
475490 yield * Ref . set ( socketOwnersRef , owners ) ;
476491 yield * Ref . set ( socketOwnerMissesRef , misses ) ;
477- const seen = new Map < number , DiscoveredLocalServer > ( ) ;
478- for ( const { port, inode } of listeners ) {
492+ return listeners . map ( ( { port, inode } ) : DiscoveredLocalServer => {
479493 const owner = owners . get ( inode ) ?? null ;
480- const terminal = owner === null ? null : ( terminalByProcessId . get ( owner . pid ) ?? null ) ;
481- const existing = seen . get ( port ) ;
482- if ( existing && ( existing . pid !== null || owner === null ) ) {
483- if ( terminal === null ) seen . set ( port , { ...existing , terminal : null } ) ;
484- continue ;
485- }
486- seen . set ( port , {
494+ return {
487495 host : "localhost" ,
488496 port,
489497 url : `http://localhost:${ port } ` ,
490498 processName : owner ?. processName ?? null ,
491499 pid : owner ?. pid ?? null ,
492- terminal : existing ? null : terminal ,
493- } ) ;
494- }
495- return [ ...seen . values ( ) ] . toSorted ( ( left , right ) => left . port - right . port ) ;
500+ terminal : owner === null ? null : ( terminalByProcessId . get ( owner . pid ) ?? null ) ,
501+ } ;
502+ } ) ;
496503 } ) ;
497504
498505 // An agent's background server runs under the provider CLI, not a terminal, so its
@@ -755,8 +762,8 @@ export const make = Effect.gen(function* PortDiscoveryMake() {
755762 terminalByProcessId . set ( processId , registration . owner ) ;
756763 }
757764 }
758- const owned = ( servers : ReadonlyArray < DiscoveredLocalServer > ) =>
759- withThreadOwners ( servers , state . threadProcesses ) ;
765+ const owned = ( listeners : ReadonlyArray < DiscoveredLocalServer > ) =>
766+ withThreadOwners ( listeners , state . threadProcesses ) . pipe ( Effect . map ( collapseListenersByPort ) ) ;
760767 if (
761768 terminalByProcessId . size === 0 &&
762769 state . threadProcesses . size === 0 &&
0 commit comments