mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-27 11:35:23 -07:00
fix(plugins): SIGKILL exec children that ignore SIGTERM
- Escalate timed-out or aborted plugin exec children to SIGKILL after a grace period - Move ai-explain example to assets/plugins and convert it to CommonJS - Update plugin docs for the new path and the .js/.mjs module formats
This commit is contained in:
6 files changed
+76
-11
No files matched your search
@@ -1,5 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
import { execPluginCommand } from './exec';
|
||||
|
||||
@@ -53,6 +57,50 @@ test('aborting kills the child and rejects with AbortError', async () => {
|
||||
await assert.rejects(pending, { name: 'AbortError' });
|
||||
});
|
||||
|
||||
test('force-kills a child that ignores SIGTERM after the grace period', async (t) => {
|
||||
if (process.platform === 'win32') return t.skip('POSIX signals only');
|
||||
const pidFile = path.join(await mkdtemp(path.join(os.tmpdir(), 'plugin-exec-')), 'pid');
|
||||
const controller = new AbortController();
|
||||
const pending = execPluginCommand(
|
||||
runtime,
|
||||
[
|
||||
'-e',
|
||||
`process.on("SIGTERM", () => {}); require("node:fs").writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)); setInterval(() => {}, 1000);`,
|
||||
],
|
||||
{ signal: controller.signal },
|
||||
100,
|
||||
);
|
||||
|
||||
let pid = 0;
|
||||
for (let i = 0; i < 100 && !pid; i += 1) {
|
||||
pid = Number(await readFile(pidFile, 'utf8').catch(() => '0'));
|
||||
if (!pid) await sleep(20);
|
||||
}
|
||||
assert.ok(pid > 0, 'child never reported its pid');
|
||||
try {
|
||||
controller.abort();
|
||||
await assert.rejects(pending, { name: 'AbortError' });
|
||||
let alive = true;
|
||||
for (let i = 0; i < 100 && alive; i += 1) {
|
||||
await sleep(20);
|
||||
alive = isRunning(pid);
|
||||
}
|
||||
assert.equal(alive, false, 'child survived SIGTERM');
|
||||
} finally {
|
||||
if (isRunning(pid)) process.kill(pid, 'SIGKILL');
|
||||
await rm(path.dirname(pidFile), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function isRunning(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
test('rejects when the binary does not exist', async () => {
|
||||
await assert.rejects(
|
||||
execPluginCommand('subminer-definitely-missing-binary', []),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { buildSystemBinaryEnv } from '../../utils/system-binary-env';
|
||||
import type { PluginExecOptions, PluginExecResult } from './types';
|
||||
|
||||
const MAX_CAPTURED_OUTPUT_CHARS = 4 * 1024 * 1024;
|
||||
const DEFAULT_KILL_GRACE_MS = 3_000;
|
||||
|
||||
function appendCapped(current: string, chunk: string): string {
|
||||
if (current.length >= MAX_CAPTURED_OUTPUT_CHARS) return current;
|
||||
@@ -13,11 +14,13 @@ function appendCapped(current: string, chunk: string): string {
|
||||
/**
|
||||
* Runs `command` without a shell and collects its output. Resolves with the exit code on any exit,
|
||||
* including non-zero; rejects when the binary cannot start, the timeout passes, or `signal` aborts.
|
||||
* A timed-out or aborted child gets SIGTERM, then SIGKILL after `killGraceMs`.
|
||||
*/
|
||||
export function execPluginCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: PluginExecOptions = {},
|
||||
killGraceMs = DEFAULT_KILL_GRACE_MS,
|
||||
): Promise<PluginExecResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (options.signal?.aborted) {
|
||||
@@ -45,9 +48,15 @@ export function execPluginCommand(
|
||||
finish();
|
||||
};
|
||||
|
||||
// Rejects right away, then escalates to SIGKILL if the child ignores SIGTERM.
|
||||
const killWith = (error: Error): void => {
|
||||
settle(() => reject(error));
|
||||
child.kill('SIGTERM');
|
||||
const forceKill = setTimeout(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
||||
}, killGraceMs);
|
||||
forceKill.unref();
|
||||
child.once('exit', () => clearTimeout(forceKill));
|
||||
};
|
||||
|
||||
function onAbort(): void {
|
||||
|
||||
Reference in new issue
Block a user