@@ -125,7 +125,7 @@ const cmdStartSource = extractFunctionBySignature(
125125) ;
126126const releaseRunPortIfNeededSource = extractFunctionBySignature (
127127 cliContent ,
128- 'function releaseRunPortIfNeeded(port, deps = {}) {' ,
128+ 'function releaseRunPortIfNeeded(port, host, deps = {}) {' ,
129129 'releaseRunPortIfNeeded'
130130) ;
131131const resolveWebHost = instantiateFunction ( resolveWebHostSource , 'resolveWebHost' , {
@@ -175,7 +175,7 @@ test('releaseRunPortIfNeeded skips non-default ports', () => {
175175 console : { log ( ) { } }
176176 } ) ;
177177
178- const result = releaseRunPortIfNeeded ( 3999 ) ;
178+ const result = releaseRunPortIfNeeded ( 3999 , '0.0.0.0' ) ;
179179 assert . deepStrictEqual ( result , {
180180 attempted : false ,
181181 released : false ,
@@ -218,7 +218,7 @@ test('releaseRunPortIfNeeded clears default port only after lsof pids map to man
218218 console : { log ( message ) { logs . push ( message ) ; } }
219219 } ) ;
220220
221- const result = releaseRunPortIfNeeded ( 3737 ) ;
221+ const result = releaseRunPortIfNeeded ( 3737 , '0.0.0.0' ) ;
222222 assert . deepStrictEqual ( calls , [
223223 [ 'lsof' , [ '-ti' , 'tcp:3737' ] ] ,
224224 [ 'ps' , [ '-ef' ] ]
@@ -232,7 +232,7 @@ test('releaseRunPortIfNeeded clears default port only after lsof pids map to man
232232 assert . deepStrictEqual ( logs , [ '~ 已释放端口 3737 占用' ] ) ;
233233} ) ;
234234
235- test ( 'releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable' , ( ) => {
235+ test ( 'releaseRunPortIfNeeded falls back to non-destructive fuser pids when lsof is unavailable' , ( ) => {
236236 const calls = [ ] ;
237237 const killed = [ ] ;
238238 const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
@@ -242,6 +242,9 @@ test('releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable', ()
242242 if ( command === 'lsof' ) {
243243 return { error : { code : 'ENOENT' } , status : null , stdout : '' , stderr : '' } ;
244244 }
245+ if ( command === 'fuser' ) {
246+ return { status : 0 , stdout : '' , stderr : '2222 3333' } ;
247+ }
245248 if ( command === 'ps' ) {
246249 return {
247250 status : 0 ,
@@ -264,9 +267,10 @@ test('releaseRunPortIfNeeded falls back to ps scan when lsof is unavailable', ()
264267 console : { log ( ) { } }
265268 } ) ;
266269
267- const result = releaseRunPortIfNeeded ( 3737 ) ;
270+ const result = releaseRunPortIfNeeded ( 3737 , '0.0.0.0' ) ;
268271 assert . deepStrictEqual ( calls , [
269272 [ 'lsof' , [ '-ti' , 'tcp:3737' ] ] ,
273+ [ 'fuser' , [ '3737/tcp' ] ] ,
270274 [ 'ps' , [ '-ef' ] ]
271275 ] ) ;
272276 assert . deepStrictEqual ( killed , [
@@ -314,7 +318,7 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
314318 console : { log ( ) { } }
315319 } ) ;
316320
317- const result = releaseRunPortIfNeeded ( 3737 ) ;
321+ const result = releaseRunPortIfNeeded ( 3737 , '0.0.0.0' ) ;
318322 assert . deepStrictEqual ( calls , [
319323 [ 'lsof' , [ '-ti' , 'tcp:3737' ] ] ,
320324 [ 'ps' , [ '-ef' ] ]
@@ -327,13 +331,104 @@ test('releaseRunPortIfNeeded falls back to ps scan for managed run processes', (
327331 } ) ;
328332} ) ;
329333
334+ test ( 'releaseRunPortIfNeeded skips ps-based kill when no port-scoped owner pids can be identified' , ( ) => {
335+ const calls = [ ] ;
336+ const killed = [ ] ;
337+ const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
338+ DEFAULT_WEB_PORT : 3737 ,
339+ spawnSync ( command , args ) {
340+ calls . push ( [ command , args ] ) ;
341+ if ( command === 'lsof' ) {
342+ return { error : { code : 'ENOENT' } , status : null , stdout : '' , stderr : '' } ;
343+ }
344+ if ( command === 'fuser' ) {
345+ return { error : { code : 'ENOENT' } , status : null , stdout : '' , stderr : '' } ;
346+ }
347+ if ( command === 'ps' ) {
348+ throw new Error ( 'ps should not run without port-scoped pid candidates' ) ;
349+ }
350+ throw new Error ( `unexpected command: ${ command } ` ) ;
351+ } ,
352+ process : {
353+ platform : 'linux' ,
354+ kill ( pid , signal ) {
355+ killed . push ( [ pid , signal ] ) ;
356+ }
357+ } ,
358+ console : { log ( ) { } }
359+ } ) ;
360+
361+ const result = releaseRunPortIfNeeded ( 3737 , '0.0.0.0' ) ;
362+ assert . deepStrictEqual ( calls , [
363+ [ 'lsof' , [ '-ti' , 'tcp:3737' ] ] ,
364+ [ 'fuser' , [ '3737/tcp' ] ]
365+ ] ) ;
366+ assert . deepStrictEqual ( killed , [ ] ) ;
367+ assert . deepStrictEqual ( result , {
368+ attempted : true ,
369+ released : false ,
370+ pids : [ ]
371+ } ) ;
372+ } ) ;
373+
374+ test ( 'releaseRunPortIfNeeded only taskkills Windows listeners that match host and managed command line' , ( ) => {
375+ const calls = [ ] ;
376+ const logs = [ ] ;
377+ const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
378+ DEFAULT_WEB_PORT : 3737 ,
379+ spawnSync ( command , args ) {
380+ calls . push ( [ command , args ] ) ;
381+ if ( command === 'netstat' ) {
382+ return {
383+ status : 0 ,
384+ stdout : [
385+ ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 1111' ,
386+ ' TCP 127.0.0.1:3737 0.0.0.0:0 LISTENING 2222' ,
387+ ' TCP 0.0.0.0:3737 0.0.0.0:0 LISTENING 3333'
388+ ] . join ( '\r\n' ) ,
389+ stderr : ''
390+ } ;
391+ }
392+ if ( command === 'powershell' ) {
393+ if ( String ( args [ 2 ] ) . includes ( '1111' ) ) {
394+ return { status : 0 , stdout : 'node C:\\repo\\cli.js run --no-browser\r\n' , stderr : '' } ;
395+ }
396+ if ( String ( args [ 2 ] ) . includes ( '3333' ) ) {
397+ return { status : 0 , stdout : 'node C:\\repo\\other-server.js\r\n' , stderr : '' } ;
398+ }
399+ throw new Error ( `unexpected powershell args: ${ args . join ( ' ' ) } ` ) ;
400+ }
401+ if ( command === 'taskkill' ) {
402+ return { status : 0 , stdout : '' , stderr : '' } ;
403+ }
404+ throw new Error ( `unexpected command: ${ command } ` ) ;
405+ } ,
406+ process : { platform : 'win32' , pid : 9999 } ,
407+ console : { log ( message ) { logs . push ( message ) ; } }
408+ } ) ;
409+
410+ const result = releaseRunPortIfNeeded ( 3737 , '0.0.0.0' ) ;
411+ assert . deepStrictEqual ( calls , [
412+ [ 'netstat' , [ '-ano' , '-p' , 'tcp' ] ] ,
413+ [ 'powershell' , [ '-NoProfile' , '-Command' , '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 1111"; if ($p) { $p.CommandLine }' ] ] ,
414+ [ 'taskkill' , [ '/PID' , '1111' , '/F' ] ] ,
415+ [ 'powershell' , [ '-NoProfile' , '-Command' , '$p = Get-CimInstance Win32_Process -Filter "ProcessId = 3333"; if ($p) { $p.CommandLine }' ] ]
416+ ] ) ;
417+ assert . deepStrictEqual ( result , {
418+ attempted : true ,
419+ released : true ,
420+ pids : [ 1111 ]
421+ } ) ;
422+ assert . deepStrictEqual ( logs , [ '~ 已释放端口 3737 占用' ] ) ;
423+ } ) ;
424+
330425test ( 'cmdStart releases the resolved port before creating the web server' , ( ) => {
331426 const resolveIndex = cmdStartSource . indexOf ( 'resolveWebPort(' ) ;
332- const releaseIndex = cmdStartSource . indexOf ( 'releaseRunPortIfNeeded(' ) ;
427+ const releaseIndex = cmdStartSource . indexOf ( 'releaseRunPortIfNeeded(port, host) ' ) ;
333428 const createIndex = cmdStartSource . indexOf ( 'createWebServer(' ) ;
334429
335430 assert ( resolveIndex >= 0 , 'cmdStart should resolve the web port' ) ;
336- assert ( releaseIndex >= 0 , 'cmdStart should release the run port before startup' ) ;
431+ assert ( releaseIndex >= 0 , 'cmdStart should release the run port with host before startup' ) ;
337432 assert ( createIndex >= 0 , 'cmdStart should create the web server' ) ;
338433 assert ( resolveIndex < releaseIndex , 'cmdStart should resolve the port before releasing it' ) ;
339434 assert ( releaseIndex < createIndex , 'cmdStart should release the port before creating the web server' ) ;
0 commit comments