mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
* 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
113 lines
3.9 KiB
TypeScript
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 */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
);
|
|
});
|