11import dedent from 'dedent'
22import fastGlob from 'fast-glob'
3- import { exec , spawn } from 'node:child_process'
3+ import { exec , execFile , spawn , type ChildProcess } from 'node:child_process'
44import fs from 'node:fs/promises'
5+ import { createServer } from 'node:net'
56import { platform , tmpdir } from 'node:os'
67import path from 'node:path'
7- import { stripVTControlCharacters } from 'node:util'
8+ import { promisify , stripVTControlCharacters } from 'node:util'
89import { RawSourceMap , SourceMapConsumer } from 'source-map-js'
910import { test as defaultTest , type ExpectStatic } from 'vitest'
1011import { createLineTable } from '../packages/tailwindcss/src/source-maps/line-table'
@@ -70,6 +71,8 @@ type SpawnActor = { predicate: (message: string) => boolean; resolve: () => void
7071
7172export const IS_WINDOWS = platform ( ) === 'win32'
7273
74+ const execFileAsync = promisify ( execFile )
75+
7376const TEST_TIMEOUT = IS_WINDOWS ? 120000 : 60000
7477const ASSERTION_TIMEOUT = IS_WINDOWS ? 10000 : 5000
7578
@@ -170,6 +173,7 @@ export function test(
170173 if ( debug ) console . log ( `>& ${ command } ` )
171174 let child = spawn ( command , {
172175 cwd,
176+ detached : ! IS_WINDOWS ,
173177 shell : true ,
174178 ...childProcessOptions ,
175179 env : {
@@ -178,19 +182,22 @@ export function test(
178182 } ,
179183 } )
180184
181- function dispose ( ) {
182- if ( ! child . kill ( ) ) {
183- child . kill ( 'SIGKILL' )
184- }
185+ let disposed = false
186+
187+ async function dispose ( ) {
188+ if ( disposed ) return disposePromise
189+ disposed = true
190+
191+ await killProcessTree ( child )
185192
186- let timer = setTimeout (
187- ( ) =>
188- rejectDisposal ?.( new Error ( `spawned process (${ command } ) did not exit in time` ) ) ,
189- ASSERTION_TIMEOUT ,
193+ let timer = setTimeout ( ( ) => {
194+ forceKillProcessTree ( child )
195+ rejectDisposal ?.( new Error ( `spawned process (${ command } ) did not exit in time` ) )
196+ } , ASSERTION_TIMEOUT )
197+ disposePromise . then (
198+ ( ) => clearTimeout ( timer ) ,
199+ ( ) => clearTimeout ( timer ) ,
190200 )
191- disposePromise . finally ( ( ) => {
192- clearTimeout ( timer )
193- } )
194201 return disposePromise
195202 }
196203 disposables . push ( dispose )
@@ -607,6 +614,65 @@ export async function fetchStyles(base: string, path = '/'): Promise<string> {
607614 } , '' )
608615}
609616
617+ export async function getRandomPort ( ) {
618+ return new Promise < number > ( ( resolve , reject ) => {
619+ let server = createServer ( )
620+ server . unref ( )
621+ server . on ( 'error' , reject )
622+ server . listen ( 0 , '127.0.0.1' , ( ) => {
623+ let address = server . address ( )
624+ server . close ( ( ) => {
625+ if ( address && typeof address === 'object' ) {
626+ resolve ( address . port )
627+ } else {
628+ reject ( new Error ( 'Unable to allocate random port' ) )
629+ }
630+ } )
631+ } )
632+ } )
633+ }
634+
635+ async function killProcessTree ( child : ChildProcess ) {
636+ if ( child . exitCode !== null || child . signalCode !== null || child . pid === undefined ) {
637+ return
638+ }
639+
640+ if ( IS_WINDOWS ) {
641+ await execFileAsync ( 'taskkill' , [ '/pid' , String ( child . pid ) , '/T' , '/F' ] , {
642+ timeout : ASSERTION_TIMEOUT ,
643+ windowsHide : true ,
644+ } ) . catch ( ( ) => { } )
645+ return
646+ }
647+
648+ try {
649+ process . kill ( - child . pid , 'SIGTERM' )
650+ } catch ( error : any ) {
651+ if ( error ?. code !== 'ESRCH' ) {
652+ child . kill ( )
653+ }
654+ }
655+ }
656+
657+ function forceKillProcessTree ( child : ChildProcess ) {
658+ if ( child . exitCode !== null || child . signalCode !== null || child . pid === undefined ) {
659+ return
660+ }
661+
662+ if ( IS_WINDOWS ) {
663+ execFile ( 'taskkill' , [ '/pid' , String ( child . pid ) , '/T' , '/F' ] , { windowsHide : true } , ( ) => { } )
664+ return
665+ }
666+
667+ try {
668+ process . kill ( - child . pid , 'SIGKILL' )
669+ } catch ( error : any ) {
670+ if ( error ?. code !== 'ESRCH' ) {
671+ child . kill ( 'SIGKILL' )
672+ }
673+ }
674+ }
675+
610676async function gracefullyRemove ( dir : string ) {
611677 // Skip removing the directory in CI because it can stall on Windows
612678 if ( ! process . env . CI ) {
0 commit comments