refactor(process): allow awaiting physical output closure

Allow the existing supervised output wait to omit its deadline while preserving bounded waits and abort-listener cleanup for current consumers. Updater package-manager commands can await physical stream completion without an invented install timeout.

Refs #4888.
This commit is contained in:
morluto
2026-10-18 05:57:18 +08:00
parent 4f7b5fb602
commit 182e9a268c
2 changed files with 49 additions and 4 deletions
+7 -4
View File
@@ -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<boolean> {
return waitForProcessEvent(
@@ -839,7 +841,7 @@ const waitForSpawn = (child: ChildProcess): Promise<void> =>
const waitForProcessEvent = async (
observed: boolean,
event: Promise<unknown>,
timeoutMs: number,
timeoutMs: number | undefined,
signal?: AbortSignal,
): Promise<boolean> => {
if (observed) return true;
@@ -850,7 +852,8 @@ const waitForProcessEvent = async (
return await Promise.race([
event.then(() => true),
new Promise<boolean>((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 });
+42
View File
@@ -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();
}
},
);