1+ import { spawn as nodeSpawn } from "node:child_process" ;
2+ import type { ChildProcessWithoutNullStreams } from "node:child_process" ;
13import path from "node:path" ;
24
35const cachedProbes = new Map < string , Promise < boolean > > ( ) ;
@@ -10,6 +12,20 @@ function isBunExecutable(executablePath: string): boolean {
1012}
1113
1214type RuntimeVersions = NodeJS . ProcessVersions & { bun ?: string } ;
15+ type SpawnFn = (
16+ command : string ,
17+ args : string [ ] ,
18+ options : {
19+ stdin : "pipe" ;
20+ stdout : "pipe" ;
21+ stderr : "ignore" ;
22+ env : NodeJS . ProcessEnv ;
23+ } ,
24+ ) => ChildProcessWithoutNullStreams ;
25+
26+ export function resetChildBunStdinPipeProbeCache ( ) : void {
27+ cachedProbes . clear ( ) ;
28+ }
1329
1430function isBunScriptEntryPoint ( childEntryPoint : string | undefined ) : boolean {
1531 if ( ! childEntryPoint ) return false ;
@@ -56,6 +72,7 @@ export async function supportsChildBunStdinPipe(
5672 childEntryPoint ?: string ,
5773 executablePath : string = process . execPath ,
5874 versions : RuntimeVersions = process . versions ,
75+ spawnFn : SpawnFn = nodeSpawn as unknown as SpawnFn ,
5976) : Promise < boolean > {
6077 if ( ! usesBunChildRuntime ( childEntryPoint , executablePath , versions ) ) {
6178 // The known stdin pipe issue is Bun-runtime specific.
@@ -79,23 +96,20 @@ export async function supportsChildBunStdinPipe(
7996 "process.stdin.resume();" +
8097 "setTimeout(() => process.exit(2), 1200);" ;
8198
82- // IMPORTANT: use Bun.spawn here to match the actual supervised runner,
83- // which spawns the Core process via Bun.spawn (ProcessManager).
84- // The stdin-pipe issue can differ between node:child_process.spawn and Bun.spawn.
85- const bunAny = ( globalThis as unknown as { Bun ?: typeof Bun } ) . Bun ;
86- if ( ! bunAny ) {
87- resolve ( false ) ;
88- return ;
89- }
90-
91- const child = bunAny . spawn ( {
92- cmd : [ ...probeCommand , script ] ,
99+ // Match the actual supervised stdio path in Supervisor, which uses
100+ // node:child_process.spawn for Bun child entrypoints.
101+ const child = spawnFn ( probeCommand [ 0 ] ! , [ ...probeCommand . slice ( 1 ) , script ] , {
93102 stdin : "pipe" ,
94103 stdout : "pipe" ,
95104 stderr : "ignore" ,
96105 env : { ...process . env } ,
97106 } ) ;
98107
108+ if ( ! child . stdin || ! child . stdout ) {
109+ resolve ( false ) ;
110+ return ;
111+ }
112+
99113 let stdout = "" ;
100114 let settled = false ;
101115 const finish = ( value : boolean ) => {
@@ -109,35 +123,25 @@ export async function supportsChildBunStdinPipe(
109123 resolve ( value ) ;
110124 } ;
111125
112- // Read stdout until token observed or timeout.
113- const decoder = new TextDecoder ( ) ;
114- const reader = child . stdout . getReader ( ) ;
115- void ( async ( ) => {
116- try {
117- while ( true ) {
118- const { done, value } = await reader . read ( ) ;
119- if ( done ) break ;
120- stdout += decoder . decode ( value , { stream : true } ) ;
121- if ( stdout . includes ( token ) ) {
122- finish ( true ) ;
123- return ;
124- }
125- }
126- } catch {
127- finish ( false ) ;
128- } finally {
129- try {
130- reader . releaseLock ( ) ;
131- } catch {
132- // ignore
133- }
126+ child . stdout . setEncoding ( "utf8" ) ;
127+ child . stdout . on ( "data" , ( chunk : string ) => {
128+ stdout += chunk ;
129+ if ( stdout . includes ( token ) ) {
130+ finish ( true ) ;
134131 }
132+ } ) ;
133+ child . stdout . on ( "error" , ( ) => {
134+ finish ( false ) ;
135+ } ) ;
136+ child . once ( "error" , ( ) => {
137+ finish ( false ) ;
138+ } ) ;
139+ child . once ( "exit" , ( ) => {
135140 finish ( stdout . includes ( token ) ) ;
136- } ) ( ) ;
141+ } ) ;
137142
138143 try {
139- child . stdin . write ( new TextEncoder ( ) . encode ( `${ token } \n` ) ) ;
140- child . stdin . flush ( ) ;
144+ child . stdin . write ( `${ token } \n` ) ;
141145 child . stdin . end ( ) ;
142146 } catch {
143147 finish ( false ) ;
0 commit comments