@@ -62,6 +62,17 @@ const layerProbeFailure = (
6262
6363const LSOF_TEST_PORT = 43_123 ;
6464
65+ const processOutput = ( stdout : string ) : ProcessRunner . ProcessRunOutput => ( {
66+ stdout,
67+ stderr : "" ,
68+ code : ChildProcessSpawner . ExitCode ( 0 ) ,
69+ timedOut : false ,
70+ stdoutTruncated : false ,
71+ stderrTruncated : false ,
72+ stdoutInvalidUtf8 : false ,
73+ stderrInvalidUtf8 : false ,
74+ } ) ;
75+
6576const ownedScanner = ( processIds : ReadonlyArray < number > = [ 1234 ] ) =>
6677 Effect . tap ( PortScanner . PortDiscovery , ( scanner ) =>
6778 scanner . registerTerminalProcesses ( {
@@ -174,18 +185,19 @@ effectIt.effect("preserves explicit loopback hosts and bounds wildcard rewrites"
174185effectIt . effect ( "projects configured paths independently for simultaneous subscribers" , ( ) => {
175186 const docsUrl = `http://localhost:${ LSOF_TEST_PORT } /docs` ;
176187 const adminUrl = `http://localhost:${ LSOF_TEST_PORT } /admin` ;
188+ let ready = true ;
177189 const fetchFn = ( ( input : Parameters < typeof globalThis . fetch > [ 0 ] ) => {
178190 const url = String ( input ) ;
179191 return Promise . resolve (
180- url === docsUrl || url === adminUrl
192+ ready && ( url === docsUrl || url === adminUrl )
181193 ? new Response ( "app" , { headers : { "content-type" : "text/html" } } )
182194 : new Response ( "not found" , { status : 404 } ) ,
183195 ) ;
184196 } ) as typeof globalThis . fetch ;
185197 const layer = layerLsofScanner ( { pid : ( ) => 1234 , fetch : fetchFn } ) ;
186198
187199 return Effect . gen ( function * ( ) {
188- const scanner = yield * PortScanner . PortDiscovery ;
200+ const scanner = yield * ownedScanner ( ) ;
189201 const docsSnapshots : ReadonlyArray < DiscoveredLocalServer > [ ] = [ ] ;
190202 const adminSnapshots : ReadonlyArray < DiscoveredLocalServer > [ ] = [ ] ;
191203 yield * scanner . subscribe ( { configuredUrls : [ docsUrl ] , initialSnapshot : [ ] } , ( servers ) =>
@@ -198,6 +210,11 @@ effectIt.effect("projects configured paths independently for simultaneous subscr
198210
199211 expect ( docsSnapshots . at ( - 1 ) ?. [ 0 ] ?. url ) . toBe ( docsUrl ) ;
200212 expect ( adminSnapshots . at ( - 1 ) ?. [ 0 ] ?. url ) . toBe ( adminUrl ) ;
213+
214+ ready = false ;
215+ yield * TestClock . adjust ( Duration . seconds ( 15 ) ) ;
216+ expect ( docsSnapshots . at ( - 1 ) ?. [ 0 ] ?. url ) . toBe ( docsUrl ) ;
217+ expect ( adminSnapshots . at ( - 1 ) ?. [ 0 ] ?. url ) . toBe ( adminUrl ) ;
201218 } ) . pipe ( Effect . scoped , Effect . provide ( layer ) ) ;
202219} ) ;
203220
@@ -289,28 +306,45 @@ effectIt.effect("uses the current configured fragment when readiness comes from
289306 } ) . pipe ( Effect . provide ( layer ) ) ;
290307} ) ;
291308
292- effectIt . effect ( "falls back to HTTPS and does not follow redirects while probing" , ( ) => {
293- const redirects : Array < string | undefined > = [ ] ;
294- const fetchFn = ( async (
295- input : Parameters < typeof globalThis . fetch > [ 0 ] ,
296- init ?: Parameters < typeof globalThis . fetch > [ 1 ] ,
297- ) => {
298- redirects . push ( init ?. redirect ) ;
299- if ( String ( input ) . startsWith ( "http:" ) ) throw new TypeError ( "TLS listener" ) ;
300- return new Response ( null , { status : 302 , headers : { location : "https://example.com" } } ) ;
301- } ) as typeof globalThis . fetch ;
302- const layer = layerLsofScanner ( { pid : ( ) => 1234 , fetch : fetchFn } ) ;
309+ effectIt . effect (
310+ "probes only a configured HTTPS URL, keeps it for an owned listener when the check fails, and does not follow redirects" ,
311+ ( ) => {
312+ const configuredUrl = `https://localhost:${ LSOF_TEST_PORT } /app` ;
313+ const requests : Array < readonly [ string , string | undefined ] > = [ ] ;
314+ let respond = async ( ) =>
315+ new Response ( null , { status : 302 , headers : { location : "https://example.com" } } ) ;
316+ const fetchFn = ( async (
317+ input : Parameters < typeof globalThis . fetch > [ 0 ] ,
318+ init ?: Parameters < typeof globalThis . fetch > [ 1 ] ,
319+ ) => {
320+ requests . push ( [ String ( input ) , init ?. redirect ] ) ;
321+ return respond ( ) ;
322+ } ) as typeof globalThis . fetch ;
323+ const layer = layerLsofScanner ( { pid : ( ) => 1234 , fetch : fetchFn } ) ;
303324
304- return Effect . gen ( function * ( ) {
305- const scanner = yield * ownedScanner ( ) ;
306- const servers = yield * scanner . scan ( ) ;
307- expect ( servers ) . toHaveLength ( 1 ) ;
308- expect ( servers [ 0 ] ?. url ) . toBe ( `https://localhost:${ LSOF_TEST_PORT } ` ) ;
309- expect ( redirects ) . toEqual ( [ "manual" , "manual" ] ) ;
310- } ) . pipe ( Effect . provide ( layer ) ) ;
311- } ) ;
325+ return Effect . gen ( function * ( ) {
326+ const scanner = yield * ownedScanner ( ) ;
327+ expect ( yield * scanner . scan ( [ configuredUrl ] ) ) . toMatchObject ( [
328+ { pid : 1234 , url : configuredUrl } ,
329+ ] ) ;
330+
331+ respond = async ( ) => new Response ( "unavailable" , { status : 503 } ) ;
332+ yield * TestClock . adjust ( Duration . seconds ( 15 ) ) ;
333+ expect ( yield * scanner . scan ( [ configuredUrl ] ) ) . toMatchObject ( [
334+ { pid : 1234 , url : configuredUrl } ,
335+ ] ) ;
312336
313- effectIt . effect ( "probes a port only while T3 terminals own every listener on it" , ( ) => {
337+ respond = ( ) => Promise . reject ( new TypeError ( "fetch failed" ) ) ;
338+ yield * TestClock . adjust ( Duration . seconds ( 15 ) ) ;
339+ expect ( yield * scanner . scan ( [ configuredUrl ] ) ) . toMatchObject ( [
340+ { pid : 1234 , url : configuredUrl } ,
341+ ] ) ;
342+ expect ( requests ) . toEqual ( Array . from ( { length : 3 } , ( ) => [ configuredUrl , "manual" ] ) ) ;
343+ } ) . pipe ( Effect . provide ( layer ) ) ;
344+ } ,
345+ ) ;
346+
347+ effectIt . effect ( "discovers owned listeners without sending HTTP or TLS probes" , ( ) => {
314348 const owned = `p1234\ncnode\nn[::1]:${ LSOF_TEST_PORT } \n` ;
315349 const unowned = `p5678\ncrpc\nn127.0.0.1:${ LSOF_TEST_PORT } \n` ;
316350 let stdout = unowned ;
@@ -330,61 +364,63 @@ effectIt.effect("probes a port only while T3 terminals own every listener on it"
330364
331365 stdout = owned ;
332366 expect ( yield * scanner . scan ( ) ) . toHaveLength ( 1 ) ;
333- expect ( requests ) . toEqual ( [ `http://localhost: ${ LSOF_TEST_PORT } /` ] ) ;
367+ expect ( requests ) . toEqual ( [ ] ) ;
334368
335369 yield * scanner . unregisterTerminal ( {
336370 threadId : "scanner-thread" ,
337371 terminalId : "scanner-terminal" ,
338372 } ) ;
339373 expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
340- expect ( requests ) . toHaveLength ( 1 ) ;
374+ expect ( requests ) . toEqual ( [ ] ) ;
341375 } ) . pipe ( Effect . provide ( layer ) ) ;
342376} ) ;
343377
344378effectIt . effect (
345- "excludes HTTP errors, non-navigation responses, and successful non-documents" ,
379+ "excludes configured URLs answering with HTTP errors, non-navigation responses, or non-documents" ,
346380 ( ) => {
347381 let pid = 1 ;
348382 let makeResponse = ( ) =>
349383 new Response ( "not found" , { status : 404 , headers : { "content-type" : "text/html" } } ) ;
350384 const fetchFn = ( ( _input : Parameters < typeof globalThis . fetch > [ 0 ] ) =>
351385 Promise . resolve ( makeResponse ( ) ) ) as typeof globalThis . fetch ;
352386 const layer = layerLsofScanner ( { pid : ( ) => pid , fetch : fetchFn } ) ;
387+ const configuredUrl = `http://localhost:${ LSOF_TEST_PORT } /` ;
353388
354389 return Effect . gen ( function * ( ) {
355- const scanner = yield * ownedScanner ( [ 1 , 2 , 3 , 4 , 5 , 6 , 7 ] ) ;
356- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
390+ const scanner = yield * PortScanner . PortDiscovery ;
391+ const scan = ( ) => scanner . scan ( [ configuredUrl ] ) ;
392+ expect ( yield * scan ( ) ) . toHaveLength ( 0 ) ;
357393
358394 pid += 1 ;
359395 makeResponse = ( ) =>
360396 new Response ( "{}" , { status : 200 , headers : { "content-type" : "application/json" } } ) ;
361- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
397+ expect ( yield * scan ( ) ) . toHaveLength ( 0 ) ;
362398
363399 pid += 1 ;
364400 makeResponse = ( ) =>
365401 new Response ( "ready" , { status : 200 , headers : { "content-type" : "text/plain" } } ) ;
366- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
402+ expect ( yield * scan ( ) ) . toHaveLength ( 0 ) ;
367403
368404 pid += 1 ;
369405 makeResponse = ( ) => new Response ( null , { status : 304 , headers : { location : "/cached" } } ) ;
370- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
406+ expect ( yield * scan ( ) ) . toHaveLength ( 0 ) ;
371407
372408 pid += 1 ;
373409 makeResponse = ( ) =>
374410 new Response ( null , { status : 204 , headers : { "content-type" : "text/html" } } ) ;
375- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
411+ expect ( yield * scan ( ) ) . toHaveLength ( 0 ) ;
376412
377413 pid += 1 ;
378414 makeResponse = ( ) => new Response ( null , { status : 302 } ) ;
379- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 0 ) ;
415+ expect ( yield * scan ( ) ) . toHaveLength ( 0 ) ;
380416
381417 pid += 1 ;
382418 makeResponse = ( ) =>
383419 new Response ( "<html />" , {
384420 status : 200 ,
385421 headers : { "content-type" : "application/xhtml+xml; charset=utf-8" } ,
386422 } ) ;
387- expect ( yield * scanner . scan ( ) ) . toHaveLength ( 1 ) ;
423+ expect ( yield * scan ( ) ) . toHaveLength ( 1 ) ;
388424 } ) . pipe ( Effect . provide ( layer ) ) ;
389425 } ,
390426) ;
@@ -433,10 +469,11 @@ effectIt.effect(
433469 return target === undefined ? missing . readLink ( path ) : Effect . succeed ( target ) ;
434470 } ,
435471 } ) ;
436- const fetchFn = ( ( ) =>
437- Promise . resolve (
438- new Response ( "hello" , { headers : { "content-type" : "text/html" } } ) ,
439- ) ) as typeof globalThis . fetch ;
472+ const requests : string [ ] = [ ] ;
473+ const fetchFn = ( ( input : Parameters < typeof globalThis . fetch > [ 0 ] ) => {
474+ requests . push ( String ( input ) ) ;
475+ return Promise . resolve ( new Response ( "hello" , { headers : { "content-type" : "text/html" } } ) ) ;
476+ } ) as typeof globalThis . fetch ;
440477 const layer = layerProbeFailure (
441478 ( input ) => {
442479 lsofSpawns += 1 ;
@@ -449,37 +486,45 @@ effectIt.effect(
449486 return Effect . gen ( function * ( ) {
450487 const scanner = yield * ownedScanner ( [ 4242 ] ) ;
451488 const first = yield * scanner . scan ( ) ;
452- expect ( first . map ( ( { port , pid , processName } ) => ( { port , pid , processName } ) ) ) . toEqual ( [
453- { port : 8765 , pid : 4242 , processName : "python3" } ,
454- ] ) ;
489+ expect (
490+ first . map ( ( { port, pid, processName , url } ) => ( { port , pid , processName, url } ) ) ,
491+ ) . toEqual ( [ { port : 8765 , pid : 4242 , processName : "python3" , url : "http://localhost:8765" } ] ) ;
455492 yield * scanner . scan ( ) ;
456493 expect ( lsofSpawns ) . toBe ( 1 ) ;
457494 expect ( fdWalks ) . toEqual ( [ "77" , "4242" , "77" , "4242" ] ) ;
458495 yield * scanner . scan ( ) ;
459496 yield * scanner . scan ( ) ;
460497 expect ( fdWalks ) . toEqual ( [ "77" , "4242" , "77" , "4242" , "77" , "4242" ] ) ;
498+ expect ( requests ) . toEqual ( [ ] ) ;
461499 } ) . pipe ( Effect . provide ( layer ) ) ;
462500 } ,
463501) ;
464502
465503effectIt . effect ( "ignores a partial Windows netstat snapshot when one query exits nonzero" , ( ) => {
466- const fetchFn = ( ( ) =>
467- Promise . resolve (
468- new Response ( "hello" , { headers : { "content-type" : "text/html" } } ) ,
469- ) ) as typeof globalThis . fetch ;
504+ let ipv6Fails = true ;
505+ const requests : string [ ] = [ ] ;
506+ const fetchFn = ( ( input : Parameters < typeof globalThis . fetch > [ 0 ] ) => {
507+ requests . push ( String ( input ) ) ;
508+ return Promise . resolve ( new Response ( "hello" , { headers : { "content-type" : "text/html" } } ) ) ;
509+ } ) as typeof globalThis . fetch ;
470510 const layer = PortScanner . layer . pipe (
471511 Layer . provide (
472512 Layer . mergeAll (
473513 layerNoProc ,
474514 Layer . succeed ( ProcessRunner . ProcessRunner , {
475515 run : ( input ) => {
516+ if ( input . command === "tasklist.exe" ) {
517+ return Effect . succeed (
518+ processOutput ( '"node.exe","1234","Console","1","50,000 K"\r\n' ) ,
519+ ) ;
520+ }
476521 const isIpv6 = input . args . includes ( "tcpv6" ) ;
477522 return Effect . succeed ( {
478523 stdout : isIpv6
479524 ? ""
480525 : " TCP 127.0.0.1:5173 0.0.0.0:0 LISTENING 1234\r\n" ,
481526 stderr : "" ,
482- code : isIpv6 ? ChildProcessSpawner . ExitCode ( 1 ) : null ,
527+ code : isIpv6 && ipv6Fails ? ChildProcessSpawner . ExitCode ( 1 ) : null ,
483528 timedOut : false ,
484529 stdoutTruncated : false ,
485530 stderrTruncated : false ,
@@ -502,20 +547,14 @@ effectIt.effect("ignores a partial Windows netstat snapshot when one query exits
502547 processIds : [ 1234 ] ,
503548 } ) ;
504549 expect ( yield * scanner . scan ( ) ) . toEqual ( [ ] ) ;
550+ ipv6Fails = false ;
551+ expect ( yield * scanner . scan ( ) ) . toMatchObject ( [
552+ { port : 5173 , pid : 1234 , processName : "node" , url : "http://localhost:5173" } ,
553+ ] ) ;
554+ expect ( requests ) . toEqual ( [ ] ) ;
505555 } ) . pipe ( Effect . provide ( layer ) ) ;
506556} ) ;
507557
508- const processOutput = ( stdout : string ) : ProcessRunner . ProcessRunOutput => ( {
509- stdout,
510- stderr : "" ,
511- code : ChildProcessSpawner . ExitCode ( 0 ) ,
512- timedOut : false ,
513- stdoutTruncated : false ,
514- stderrTruncated : false ,
515- stdoutInvalidUtf8 : false ,
516- stderrInvalidUtf8 : false ,
517- } ) ;
518-
519558effectIt . effect ( "rechecks the socket owner before stopping a Linux server without lsof" , ( ) => {
520559 let holder = "4242" ;
521560 const missing = FileSystem . makeNoop ( { } ) ;
@@ -569,6 +608,63 @@ effectIt.effect("rechecks the socket owner before stopping a Linux server withou
569608 } ) . pipe ( Effect . provide ( layer ) ) ;
570609} ) ;
571610
611+ effectIt . effect . each ( [ "linux" , "darwin" , "win32" ] as const ) (
612+ "reads no listeners on %s while T3CODE_PORT_DISCOVERY=0, and still probes configured URLs" ,
613+ ( platform ) => {
614+ const configuredUrl = `http://localhost:${ LSOF_TEST_PORT } /docs` ;
615+ const commands : string [ ] = [ ] ;
616+ const fileReads : string [ ] = [ ] ;
617+ const requests : string [ ] = [ ] ;
618+ const missing = FileSystem . makeNoop ( { } ) ;
619+ const layer = PortScanner . layer . pipe (
620+ Layer . provide (
621+ Layer . mergeAll (
622+ FileSystem . layerNoop ( {
623+ readFileString : ( path ) => {
624+ fileReads . push ( path ) ;
625+ return missing . readFileString ( path ) ;
626+ } ,
627+ readDirectory : ( path ) => {
628+ fileReads . push ( path ) ;
629+ return missing . readDirectory ( path ) ;
630+ } ,
631+ } ) ,
632+ Layer . succeed ( ProcessRunner . ProcessRunner , {
633+ run : ( input ) => {
634+ commands . push ( input . command ) ;
635+ return Effect . succeed ( processOutput ( `p1234\ncnode\nn*:${ LSOF_TEST_PORT } \n` ) ) ;
636+ } ,
637+ } ) ,
638+ Layer . succeed ( HostProcess . Platform , platform ) ,
639+ Layer . succeed ( HostProcess . Environment , { T3CODE_PORT_DISCOVERY : "0" } ) ,
640+ FetchHttpClient . layer . pipe (
641+ Layer . provide (
642+ Layer . succeed ( FetchHttpClient . Fetch , ( input ) => {
643+ requests . push ( String ( input ) ) ;
644+ return Promise . resolve (
645+ new Response ( "docs" , { headers : { "content-type" : "text/html" } } ) ,
646+ ) ;
647+ } ) ,
648+ ) ,
649+ ) ,
650+ ) ,
651+ ) ,
652+ ) ;
653+
654+ return Effect . gen ( function * ( ) {
655+ const scanner = yield * ownedScanner ( ) ;
656+ scanner . trackAgent ( ) ;
657+ expect ( yield * scanner . scan ( ) ) . toEqual ( [ ] ) ;
658+ expect ( yield * scanner . scan ( [ configuredUrl ] ) ) . toMatchObject ( [
659+ { port : LSOF_TEST_PORT , pid : null , terminal : null , url : configuredUrl } ,
660+ ] ) ;
661+ expect ( requests ) . toEqual ( [ configuredUrl ] ) ;
662+ expect ( commands ) . toEqual ( [ ] ) ;
663+ expect ( fileReads ) . toEqual ( [ ] ) ;
664+ } ) . pipe ( Effect . provide ( layer ) ) ;
665+ } ,
666+ ) ;
667+
572668effectIt . effect . each ( [ "linux" , "darwin" ] as const ) (
573669 "attributes listeners to the agent thread in their environment on %s, after the agent exits" ,
574670 ( platform ) => {
@@ -672,7 +768,7 @@ effectIt.effect.each(["linux", "darwin"] as const)(
672768 expect ( yield * scanner . scan ( ) ) . toMatchObject ( [
673769 { port : agentPort , pid : 4242 , terminal : null , agent : { threadId : "thread-agent" } } ,
674770 ] ) ;
675- expect ( requests ) . toEqual ( [ `http://localhost: ${ agentPort } /` ] ) ;
771+ expect ( requests ) . toEqual ( [ ] ) ;
676772
677773 expect ( yield * scanner . scan ( ) ) . toHaveLength ( 1 ) ;
678774 expect ( environmentReads ) . toBe ( 2 ) ;
0 commit comments