@@ -4,6 +4,10 @@ import net from 'node:net';
44import os from 'node:os' ;
55import path from 'node:path' ;
66
7+ const CHILD_CLOSE_GRACE_MS = 3_000 ;
8+ const CHILD_FORCE_CLOSE_GRACE_MS = 1_000 ;
9+ const TASKKILL_TIMEOUT_MS = 5_000 ;
10+
711export async function preflightOpenCodeLiveEnvironment ( input ) {
812 const repoRoot = input . repoRoot ;
913 const opencodeBin = process . env . OPENCODE_BIN ?. trim ( ) || '/opt/homebrew/bin/opencode' ;
@@ -97,6 +101,7 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
97101 cwd,
98102 env,
99103 stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
104+ windowsHide : true ,
100105 } ) ;
101106 let output = '' ;
102107 let spawnError = '' ;
@@ -138,26 +143,100 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
138143 }
139144}
140145
141- function stopChild ( child ) {
146+ async function stopChild ( child , options = { } ) {
147+ const platform = options . platform ?? process . platform ;
148+ const killProcessTree = options . killProcessTree ?? taskkillProcessTree ;
149+ const closeGraceMs = options . closeGraceMs ?? CHILD_CLOSE_GRACE_MS ;
150+ const forceCloseGraceMs = options . forceCloseGraceMs ?? CHILD_FORCE_CLOSE_GRACE_MS ;
151+
152+ if ( hasChildExited ( child ) ) {
153+ return ;
154+ }
155+
156+ if ( platform === 'win32' && child . pid ) {
157+ await killProcessTree ( child . pid ) ;
158+ } else if ( ! child . killed ) {
159+ sendChildSignal ( child , 'SIGTERM' ) ;
160+ }
161+
162+ if ( await waitForChildClose ( child , closeGraceMs ) ) {
163+ return ;
164+ }
165+
166+ if ( ! hasChildExited ( child ) ) {
167+ sendChildSignal ( child , 'SIGKILL' ) ;
168+ if ( ! ( await waitForChildClose ( child , forceCloseGraceMs ) ) ) {
169+ child . stdout ?. destroy ( ) ;
170+ child . stderr ?. destroy ( ) ;
171+ child . unref ?. ( ) ;
172+ }
173+ }
174+ }
175+
176+ function taskkillProcessTree ( pid ) {
142177 return new Promise ( ( resolve ) => {
143- if ( child . exitCode != null || child . killed ) {
178+ let done = false ;
179+ let taskkill = null ;
180+ const finish = ( ) => {
181+ if ( done ) return ;
182+ done = true ;
183+ clearTimeout ( timeout ) ;
144184 resolve ( ) ;
145- return ;
146- }
185+ } ;
147186 const timeout = setTimeout ( ( ) => {
148- if ( child . exitCode == null ) {
149- child . kill ( 'SIGKILL ') ;
187+ if ( taskkill ) {
188+ sendChildSignal ( taskkill , 'SIGTERM ') ;
150189 }
151- resolve ( ) ;
152- } , 3_000 ) ;
153- child . once ( 'close' , ( ) => {
190+ finish ( ) ;
191+ } , TASKKILL_TIMEOUT_MS ) ;
192+ try {
193+ taskkill = spawn (
194+ path . join ( process . env . SystemRoot ?? 'C:\\Windows' , 'System32' , 'taskkill.exe' ) ,
195+ [ '/T' , '/F' , '/PID' , String ( pid ) ] ,
196+ {
197+ stdio : 'ignore' ,
198+ windowsHide : true ,
199+ }
200+ ) ;
201+ taskkill . unref ?. ( ) ;
202+ taskkill . once ( 'error' , finish ) ;
203+ taskkill . once ( 'close' , finish ) ;
204+ } catch {
205+ finish ( ) ;
206+ }
207+ } ) ;
208+ }
209+
210+ function waitForChildClose ( child , timeoutMs ) {
211+ if ( hasChildExited ( child ) ) {
212+ return Promise . resolve ( true ) ;
213+ }
214+
215+ return new Promise ( ( resolve ) => {
216+ let done = false ;
217+ const finish = ( closed ) => {
218+ if ( done ) return ;
219+ done = true ;
154220 clearTimeout ( timeout ) ;
155- resolve ( ) ;
156- } ) ;
157- child . kill ( 'SIGTERM' ) ;
221+ resolve ( closed ) ;
222+ } ;
223+ const timeout = setTimeout ( ( ) => finish ( false ) , timeoutMs ) ;
224+ child . once ( 'close' , ( ) => finish ( true ) ) ;
158225 } ) ;
159226}
160227
228+ function hasChildExited ( child ) {
229+ return child . exitCode != null || child . signalCode != null ;
230+ }
231+
232+ function sendChildSignal ( child , signal ) {
233+ try {
234+ child . kill ( signal ) ;
235+ } catch {
236+ // Process may already be gone between liveness checks and the kill call.
237+ }
238+ }
239+
161240function allocateLoopbackPort ( ) {
162241 return new Promise ( ( resolve , reject ) => {
163242 const server = net . createServer ( ) ;
@@ -190,3 +269,8 @@ function skip(reason) {
190269function compactOutput ( value ) {
191270 return value . replace ( / \s + / g, ' ' ) . trim ( ) . slice ( 0 , 1_200 ) ;
192271}
272+
273+ export const __opencodeLivePreflightTestHooks = {
274+ stopChild,
275+ taskkillProcessTree,
276+ } ;
0 commit comments