mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user