Files
iptvnator/apps/electron-backend-e2e/src/performance/electron-process-termination.spec.ts
T
4gray 0ecfc06494 test(electron): fix Windows cleanup and retain packaged smoke diagnostics (#1665)
* test(electron): reap Windows process trees and retain smoke diagnostics

* test(electron): require confirmed process exit before relaunch

* test(electron): retain process handle after application exit

* test(electron): bind captured processes to application instances
2026-09-23 22:55:48 +02:00

113 lines
3.9 KiB
TypeScript

import assert from 'node:assert/strict';
import { spawn, type execFileSync } from 'node:child_process';
import { once } from 'node:events';
import { describe, it } from 'node:test';
import { terminateElectronProcess } from '../electron-process-termination';
describe('Electron forced termination', () => {
it('terminates the Windows shell and its Electron descendants together', () => {
let shellAlive = true;
let electronAlive = true;
const child = {
pid: 1234,
kill: () => {
shellAlive = false;
return true;
},
};
const run = ((file, args, options) => {
assert.equal(file, 'taskkill.exe');
assert.deepEqual(args, ['/pid', '1234', '/T', '/F']);
assert.equal(options?.timeout, 5000);
shellAlive = false;
electronAlive = false;
return Buffer.alloc(0);
}) as typeof execFileSync;
terminateElectronProcess(child, 'SIGTERM', 'win32', run);
assert.equal(shellAlive, false);
assert.equal(
electronAlive,
false,
'Electron must release the profile before relaunch'
);
});
it('preserves signal escalation on Unix', () => {
const signals: NodeJS.Signals[] = [];
const child = {
pid: 1234,
kill: (signal: NodeJS.Signals) => {
signals.push(signal);
return true;
},
};
terminateElectronProcess(child, 'SIGTERM', 'linux');
terminateElectronProcess(child, 'SIGKILL', 'darwin');
assert.deepEqual(signals, ['SIGTERM', 'SIGKILL']);
});
it('does not fall back to killing only the shell when tree termination fails', () => {
const failure = new Error('taskkill failed');
const child = {
pid: 1234,
kill: () => assert.fail('would orphan the Electron descendant'),
};
const run = (() => {
throw failure;
}) as typeof execFileSync;
assert.throws(
() => terminateElectronProcess(child, 'SIGTERM', 'win32', run),
(error) => error === failure
);
});
it(
'reaps a real Windows shell child instead of orphaning its descendant',
{
skip: process.platform !== 'win32',
timeout: 15000,
},
async () => {
// Match Playwright's Windows launch topology: cmd.exe -> application.
const shell = spawn(
`"${process.execPath}"`,
[
'-e',
'"setInterval(() => {}, 1000); console.log(process.pid)"',
],
{ shell: true, stdio: ['ignore', 'pipe', 'pipe'] }
);
try {
const [output] = await once(shell.stdout, 'data', {
signal: AbortSignal.timeout(5000),
});
const descendantPid = Number(String(output).trim());
assert.ok(
Number.isSafeInteger(descendantPid) && descendantPid > 0
);
assert.notEqual(descendantPid, shell.pid);
process.kill(descendantPid, 0);
terminateElectronProcess(shell);
await assert.rejects(
async () => process.kill(descendantPid, 0),
{
code: 'ESRCH',
}
);
} finally {
if (shell.exitCode === null && shell.signalCode === null) {
// The successful taskkill can precede Node's exit event.
try {
terminateElectronProcess(shell);
} catch {
/* already exited */
}
}
}
}
);
});