fix: confirm proxy exit before disconnect
APT build checks / build-checks (push) Successful in 36s
APT build checks / build-checks (pull_request) Successful in 38s

This commit is contained in:
2026-08-19 23:12:56 +00:00
parent f65273c7a9
commit 4a10e6051e
5 changed files with 308 additions and 52 deletions
+82 -10
View File
@@ -24,6 +24,8 @@ class FakeChild extends EventEmitter {
this.killResult = killResult;
this.killCalls = [];
this.killError = null;
this.exitCode = null;
this.signalCode = null;
}
kill(signal) {
@@ -33,7 +35,13 @@ class FakeChild extends EventEmitter {
}
}
function createHarness({ helperExists = true, children = [new FakeChild(4101)] } = {}) {
function createHarness({
helperExists = true,
children = [new FakeChild(4101)],
stopTimeoutMs = 5_000,
setTimeout = global.setTimeout,
clearTimeout = global.clearTimeout,
} = {}) {
const calls = [];
let childIndex = 0;
const spawn = (...args) => {
@@ -47,7 +55,10 @@ function createHarness({ helperExists = true, children = [new FakeChild(4101)] }
fs: fsStub,
spawn,
platform: 'win32',
now: () => 1_777_777_777_777
now: () => 1_777_777_777_777,
stopTimeoutMs,
setTimeout,
clearTimeout,
});
return { manager, calls, children };
}
@@ -239,32 +250,38 @@ test('tracks only safe process metadata and never exposes usernames', () => {
assert.doesNotMatch(JSON.stringify(tracked), /proxy\.operator/);
});
test('stopping one tracked process leaves the other tracked process alive', () => {
test('stopping one tracked process waits for confirmed exit and leaves the other alive', async () => {
const first = new FakeChild(4101);
const second = new FakeChild(4102);
const { manager } = createHarness({ children: [first, second] });
manager.launchProxy(validRequest());
manager.launchProxy(validRequest({ deviceId: '123e4567-e89b-42d3-a456-426614174001' }));
const result = manager.stopProxy(4101);
const stopping = manager.stopProxy(4101);
assert.deepEqual(manager.listTrackedProxies().map((item) => [item.processId, item.status]), [
[4101, 'stopping'],
[4102, 'running'],
]);
first.emit('exit', 0, 'SIGTERM');
const result = await stopping;
assert.deepEqual(result, {
success: true,
processId: 4101,
deviceId: VALID_DEVICE_ID,
status: 'stop-requested'
status: 'stopped'
});
assert.deepEqual(first.killCalls, ['SIGTERM']);
assert.deepEqual(second.killCalls, []);
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4102]);
});
test('never stops an untracked PID', () => {
test('never stops an untracked PID', async () => {
const child = new FakeChild(4101);
const { manager } = createHarness({ children: [child] });
manager.launchProxy(validRequest());
assert.deepEqual(manager.stopProxy(9999), {
assert.deepEqual(await manager.stopProxy(9999), {
success: false,
processId: 9999,
status: 'not-tracked'
@@ -272,21 +289,22 @@ test('never stops an untracked PID', () => {
assert.deepEqual(child.killCalls, []);
});
test('reports already-exited and permission-denied states honestly', () => {
test('reports already-exited and permission-denied states honestly', async () => {
const exited = new FakeChild(4101, false);
exited.exitCode = 0;
const denied = new FakeChild(4102);
denied.killError = Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
const { manager } = createHarness({ children: [exited, denied] });
manager.launchProxy(validRequest());
manager.launchProxy(validRequest({ deviceId: '123e4567-e89b-42d3-a456-426614174001' }));
assert.deepEqual(manager.stopProxy(4101), {
assert.deepEqual(await manager.stopProxy(4101), {
success: true,
processId: 4101,
deviceId: VALID_DEVICE_ID,
status: 'already-exited'
});
assert.deepEqual(manager.stopProxy(4102), {
assert.deepEqual(await manager.stopProxy(4102), {
success: false,
processId: 4102,
deviceId: '123e4567-e89b-42d3-a456-426614174001',
@@ -296,6 +314,60 @@ test('reports already-exited and permission-denied states honestly', () => {
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4102]);
});
test('kill true is only a request and times out while the child remains tracked as stopping', async () => {
let timeoutCallback;
const child = new FakeChild(4101, true);
const { manager } = createHarness({
children: [child],
stopTimeoutMs: 25,
setTimeout(callback, delay) {
assert.equal(delay, 25);
timeoutCallback = callback;
return 123;
},
clearTimeout() {},
});
manager.launchProxy(validRequest());
const stopping = manager.stopProxy(4101);
assert.deepEqual(manager.listTrackedProxies().map(({ processId, status }) => ({ processId, status })), [
{ processId: 4101, status: 'stopping' },
]);
timeoutCallback();
assert.deepEqual(await stopping, {
success: false,
processId: 4101,
deviceId: VALID_DEVICE_ID,
status: 'stop-timeout',
message: 'Timed out waiting for the tracked proxy process to exit.',
});
assert.equal(manager.listTrackedProxies()[0].status, 'stopping');
});
test('kill false does not claim success without confirmed exit', async () => {
const child = new FakeChild(4101, false);
const { manager } = createHarness({ children: [child] });
manager.launchProxy(validRequest());
assert.deepEqual(await manager.stopProxy(4101), {
success: false,
processId: 4101,
deviceId: VALID_DEVICE_ID,
status: 'stop-failed',
message: 'Unable to confirm that the tracked proxy process exited.',
});
assert.equal(manager.listTrackedProxies()[0].status, 'stopping');
});
test('generic child errors retain tracking unless process exit is confirmed', () => {
const child = new FakeChild(4101);
const { manager } = createHarness({ children: [child] });
manager.launchProxy(validRequest());
child.emit('error', new Error('transient process error'));
assert.deepEqual(manager.listTrackedProxies().map((entry) => entry.processId), [4101]);
});
test('exit events remove only the matching owned child', () => {
const original = new FakeChild(4101);
const replacement = new FakeChild(4101);