diff --git a/src/process/ProviderProcess.ts b/src/process/ProviderProcess.ts index b85ceacb2c..948a5a197b 100644 --- a/src/process/ProviderProcess.ts +++ b/src/process/ProviderProcess.ts @@ -484,9 +484,11 @@ export class ProviderProcessSupervisor { ); } - /** Wait until output streams close; WindowsOwnedProcess closes only after its whole Job Object settles. */ + /** Wait until output streams close; omit the timeout to await physical completion. + * WindowsOwnedProcess closes only after its whole Job Object settles. + */ async waitForOutputClose( - timeoutMs: number, + timeoutMs?: number, signal?: AbortSignal, ): Promise { return waitForProcessEvent( @@ -839,7 +841,7 @@ const waitForSpawn = (child: ChildProcess): Promise => const waitForProcessEvent = async ( observed: boolean, event: Promise, - timeoutMs: number, + timeoutMs: number | undefined, signal?: AbortSignal, ): Promise => { if (observed) return true; @@ -850,7 +852,8 @@ const waitForProcessEvent = async ( return await Promise.race([ event.then(() => true), new Promise((resolve) => { - timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs)); + if (timeoutMs !== undefined) + timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs)); if (signal !== undefined) { onAbort = () => resolve(false); signal.addEventListener("abort", onAbort, { once: true }); diff --git a/src/process/ProviderProcessOutput.test.ts b/src/process/ProviderProcessOutput.test.ts index a7bb6eb19c..30a7c553d5 100644 --- a/src/process/ProviderProcessOutput.test.ts +++ b/src/process/ProviderProcessOutput.test.ts @@ -490,3 +490,45 @@ it("unreferences process and stream handles when stop reports incomplete cleanup supervisor.dispose(); } }); + +it.each(["close", "abort"] as const)( + "awaits output without a deadline until %s", + async (outcome) => { + const producer = Object.assign(new EventEmitter(), { + stdout: new PassThrough(), + stderr: new PassThrough(), + exitCode: null, + signalCode: null, + kill: () => false, + }); + const owner = new ProviderProcessSupervisor({ + process: producer, + ownsProcessLifetime: true, + }); + const controller = new AbortController(); + const removeListener = vi.spyOn(controller.signal, "removeEventListener"); + let settled = false; + const pending = owner + .waitForOutputClose(undefined, controller.signal) + .then((result) => { + settled = true; + return result; + }); + try { + await Promise.resolve(); + expect(settled).toBe(false); + if (outcome === "close") producer.emit("close", 0, null); + else controller.abort(); + expect(await pending).toBe(outcome === "close"); + expect(removeListener).toHaveBeenCalledWith( + "abort", + expect.any(Function), + ); + } finally { + removeListener.mockRestore(); + owner.dispose(); + producer.stdout.destroy(); + producer.stderr.destroy(); + } + }, +);