mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
test: wait for the parent's ack before utility children exit (#55082)
process.exit() does not wait for pending postMessage() writes, so a utility child that exits right after posting can lose its message. Restore the acknowledgement handshake that #54924 removed from the api-net fixture and helper, and add it to the env-app and js2c-code-cache fixtures, so those children only exit once the parent has acknowledged their message. The #54924 test no longer posts right before exiting. The child posts its parentPort messages, then posts on a transferred MessagePortMain and exits only when the parent replies there. parentPort and the port share the same ordered Mojo channel, so all of the parentPort messages have reached the parent when the port message does. Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
554d6f5c38
commit
23c730b8b8
9 files changed
+68
-25
No files matched your search
@@ -106,6 +106,14 @@ describe('net module', () => {
|
||||
expect(await result).toEqual({ data: { ok: true }, code: 0 });
|
||||
});
|
||||
|
||||
it('acknowledges the result so that the child can exit', async () => {
|
||||
const child = new EventEmitter() as EventEmitter & { postMessage(message: unknown): void };
|
||||
child.postMessage = () => child.emit('exit', 0);
|
||||
const result = waitForUtilityResult(child);
|
||||
child.emit('message', { ok: true });
|
||||
expect(await result).toEqual({ data: { ok: true }, code: 0 });
|
||||
});
|
||||
|
||||
it('fails without waiting for the timeout when the child exits before posting a result', async () => {
|
||||
const child = new EventEmitter();
|
||||
const result = waitForUtilityResult(child);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { setImmediate } from 'node:timers/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { respondOnce, randomString, kOneKiloByte } from './lib/net-helpers.ts';
|
||||
import { defer, deferKillUtilityProcess, ifit, listen, sleepSync, startRemoteControlApp } from './lib/spec-helpers.ts';
|
||||
import { defer, deferKillUtilityProcess, ifit, listen, startRemoteControlApp } from './lib/spec-helpers.ts';
|
||||
import { closeWindow } from './lib/window-helpers.ts';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -541,7 +541,7 @@ describe('utilityProcess module', () => {
|
||||
await exit;
|
||||
});
|
||||
|
||||
it('delivers messages the child posted right before exiting', async () => {
|
||||
it('delivers messages that reached this process before the child exited', async () => {
|
||||
const count = 20;
|
||||
const child = utilityProcess.fork(path.join(fixturesPath, 'post-message-then-exit.js'));
|
||||
deferKillUtilityProcess(child);
|
||||
@@ -551,11 +551,15 @@ describe('utilityProcess module', () => {
|
||||
child.once('exit', () => {
|
||||
receivedBeforeExit = [...received];
|
||||
});
|
||||
// The child posts on this port after its parentPort messages, which share
|
||||
// the same ordered Mojo channel, so they have all reached this process
|
||||
// when the port message does. Only then is the child told to exit.
|
||||
const { port1, port2 } = new MessageChannelMain();
|
||||
defer(() => port1.close());
|
||||
port1.once('message', () => port1.postMessage('exit'));
|
||||
port1.start();
|
||||
await once(child, 'spawn');
|
||||
child.postMessage(count);
|
||||
// Block this thread while the child posts and exits, so that its
|
||||
// messages and its exit are both waiting here when it gets back to them.
|
||||
sleepSync(1000);
|
||||
child.postMessage(count, [port2]);
|
||||
const [code] = await once(child, 'exit');
|
||||
expect(code).toBe(0);
|
||||
expect(receivedBeforeExit).toEqual(Array.from({ length: count }, (_, index) => index));
|
||||
|
||||
+4
-1
@@ -65,7 +65,10 @@ app.whenReady().then(async () => {
|
||||
const utility = await withTimeout(
|
||||
new Promise((resolve) => {
|
||||
const child = utilityProcess.fork(UTIL_CHILD);
|
||||
child.once('message', (m) => resolve(m));
|
||||
child.once('message', (m) => {
|
||||
child.postMessage('ack');
|
||||
resolve(m);
|
||||
});
|
||||
}),
|
||||
'utility'
|
||||
);
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
// Reports the utility process's internal/electron/js2c/* build-time code-cache
|
||||
// status back to the parent for the js2c-code-cache spec.
|
||||
const v8Util = process._linkedBinding('electron_common_v8_util');
|
||||
// Exits once the parent has acknowledged the status: process.exit() does not
|
||||
// wait for the asynchronous postMessage() write to reach the parent.
|
||||
process.parentPort.once('message', () => process.exit(0));
|
||||
process.parentPort.postMessage(v8Util.getJs2cCodeCacheStatus());
|
||||
setTimeout(() => process.exit(0), 50);
|
||||
+24
-10
@@ -32,11 +32,21 @@ const electronNet = net;
|
||||
|
||||
v8.setFlagsFromString('--expose_gc');
|
||||
|
||||
function fail(message) {
|
||||
process.parentPort.postMessage({ ok: false, message });
|
||||
// Posts |result| and exits with |code| once the parent has acknowledged it
|
||||
// (see waitForUtilityResult in spec/lib/utility-process-helpers.ts).
|
||||
// postMessage() is an asynchronous write and process.exit() does not wait for
|
||||
// pending writes, so exiting right after posting can lose the result.
|
||||
function finish(result, code) {
|
||||
// Leftover async work from the test body can still fail while this waits for the ack.
|
||||
// Log it rather than let it change the exit code of a test that has already reported its result.
|
||||
const logError = (err) => console.error('error after the test result was posted:', err);
|
||||
process.on('uncaughtException', logError);
|
||||
process.on('unhandledRejection', logError);
|
||||
process.parentPort.once('message', () => process.exit(code));
|
||||
process.parentPort.postMessage(result);
|
||||
}
|
||||
|
||||
process.parentPort.on('message', async (e) => {
|
||||
process.parentPort.once('message', async (e) => {
|
||||
// Equivalent of beforeEach in spec/api-net.spec.ts
|
||||
respondNTimes.routeFailure = false;
|
||||
|
||||
@@ -50,19 +60,23 @@ process.parentPort.on('message', async (e) => {
|
||||
// oxlint-disable-next-line no-eval
|
||||
await eval(e.data.fn);
|
||||
} catch (err) {
|
||||
fail(`${err}`);
|
||||
process.exit(1);
|
||||
finish({ ok: false, message: `${err}` }, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Equivalent of afterEach in spec/api-net.spec.ts
|
||||
if (respondNTimes.routeFailure) {
|
||||
fail(
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
finish(
|
||||
{
|
||||
ok: false,
|
||||
message:
|
||||
'Failing this test due an unhandled error in the respondOnce route handler, check the logs above for the actual error'
|
||||
},
|
||||
1
|
||||
);
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test passed
|
||||
process.parentPort.postMessage({ ok: true });
|
||||
process.exit(0);
|
||||
finish({ ok: true }, 0);
|
||||
});
|
||||
@@ -16,6 +16,7 @@ app.whenReady().then(() => {
|
||||
child.on('message', (data) => {
|
||||
process.stdout.write(data);
|
||||
process.stdout.end();
|
||||
child.postMessage('ack');
|
||||
});
|
||||
child.on('exit', () => {
|
||||
app.quit();
|
||||
|
||||
+3
-1
@@ -1,2 +1,4 @@
|
||||
// Exits once the parent has acknowledged the message: process.exit() does not
|
||||
// wait for the asynchronous postMessage() write to reach the parent.
|
||||
process.parentPort.once('message', () => process.exit(0));
|
||||
process.parentPort.postMessage(process.env.FROM);
|
||||
process.exit(0);
|
||||
+10
-4
@@ -1,8 +1,14 @@
|
||||
// Once told how many messages to post, posts them and exits straight away,
|
||||
// without waiting for the parent to read them.
|
||||
process.parentPort.once('message', ({ data: count }) => {
|
||||
// Once given a count and a MessagePortMain, posts that many messages on
|
||||
// parentPort, then posts on the port and exits when the parent replies there.
|
||||
// process.exit() does not wait for pending postMessage() writes, so exiting
|
||||
// straight away could lose messages. parentPort and the port share the same
|
||||
// ordered Mojo channel to the parent, so by the time the parent sees the
|
||||
// message on the port, every parentPort message posted before it has arrived.
|
||||
process.parentPort.once('message', ({ data: count, ports: [port] }) => {
|
||||
for (let index = 0; index < count; index++) {
|
||||
process.parentPort.postMessage(index);
|
||||
}
|
||||
process.exit(0);
|
||||
port.once('message', () => process.exit(0));
|
||||
port.start();
|
||||
port.postMessage('posted');
|
||||
});
|
||||
@@ -9,11 +9,13 @@ import { deferKillUtilityProcess } from './spec-helpers.ts';
|
||||
|
||||
const fixturePath = path.resolve(import.meta.dirname, '..', 'fixtures', 'api', 'utility-process', 'api-net-spec.js');
|
||||
|
||||
// Waits for the result the api-net-spec.js fixture posts, then for the child to exit.
|
||||
// Waits for the result the api-net-spec.js fixture posts, acknowledges it, then
|
||||
// waits for the child to exit. The fixture only exits once it has the
|
||||
// acknowledgement, so its result can't be overtaken by (or lost to) its exit.
|
||||
// Both listeners are attached before this returns, so call it before posting work.
|
||||
// Rejects as soon as the child exits without having posted a result,
|
||||
// instead of leaving the test to wait for its timeout.
|
||||
export async function waitForUtilityResult(child: NodeJS.EventEmitter) {
|
||||
export async function waitForUtilityResult(child: NodeJS.EventEmitter & { postMessage?(message: unknown): void }) {
|
||||
const exit = once(child, 'exit');
|
||||
const [data] = await Promise.race([
|
||||
once(child, 'message'),
|
||||
@@ -21,6 +23,7 @@ export async function waitForUtilityResult(child: NodeJS.EventEmitter) {
|
||||
throw new Error(`utility process exited (${code}) before posting a result`);
|
||||
})
|
||||
]);
|
||||
child.postMessage?.('ack');
|
||||
const [code] = await exit;
|
||||
return { data, code };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user