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
+8 -6
View File
@@ -292,21 +292,23 @@ class AppRuntime {
}
if (!processId || !deviceId) return { success: false, message: 'Proxy process is not owned by this app.' };
const result = this.proxyManager.stopProxy(processId);
const result = await this.proxyManager.stopProxy(processId);
if (result.success) this.proxyByDevice.delete(deviceId);
return { ...result, deviceId };
}
disconnect() {
async disconnect() {
const tracked = this._reconcileProxies();
for (const proxy of tracked) {
try { this.proxyManager.stopProxy(proxy.processId); } catch {}
}
await Promise.all(tracked.map(async (proxy) => {
try {
await this.proxyManager.stopProxy(proxy.processId);
} catch {}
}));
const remaining = this._reconcileProxies();
if (remaining.length > 0) {
return {
success: false,
message: 'Could not stop every active proxy. The Alta session remains connected.',
message: 'Could not confirm every active proxy exited. The Alta session remains connected.',
...this.getConnectionState(),
};
}
+88 -21
View File
@@ -10,6 +10,7 @@ const MAX_HOST_LENGTH = 253;
const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const ALTA_SUFFIXES = ['.avasecurity.com', '.avigilon.com'];
const DEFAULT_STOP_TIMEOUT_MS = 5_000;
class ProxyLaunchError extends Error {
constructor(message, code) {
@@ -70,7 +71,10 @@ function createProxyManager({
fs = nodeFs,
spawn = nodeSpawn,
platform = process.platform,
now = Date.now
now = Date.now,
stopTimeoutMs = DEFAULT_STOP_TIMEOUT_MS,
setTimeout = global.setTimeout,
clearTimeout = global.clearTimeout
} = {}) {
if (platform !== 'win32') {
throw new ProxyLaunchError('Proxy helper is supported only on the Windows platform.', 'UNSUPPORTED_PLATFORM');
@@ -78,7 +82,9 @@ function createProxyManager({
if (typeof appDirectory !== 'string' || !nodePath.win32.isAbsolute(appDirectory)) {
throw new ProxyLaunchError('Approved application directory must be an absolute path.', 'INVALID_APP_DIRECTORY');
}
if (typeof fs.existsSync !== 'function' || typeof spawn !== 'function' || typeof now !== 'function') {
if (typeof fs.existsSync !== 'function' || typeof spawn !== 'function' || typeof now !== 'function' ||
!Number.isSafeInteger(stopTimeoutMs) || stopTimeoutMs <= 0 ||
typeof setTimeout !== 'function' || typeof clearTimeout !== 'function') {
throw new TypeError('Invalid proxy manager dependency.');
}
@@ -90,6 +96,32 @@ function createProxyManager({
if (current && current.child === child) trackedChildren.delete(processId);
}
function hasConfirmedExit(child) {
return (child.exitCode !== null && child.exitCode !== undefined) ||
(child.signalCode !== null && child.signalCode !== undefined);
}
function finishStopAttempt(entry, result) {
const attempt = entry.stopAttempt;
if (!attempt) return;
entry.stopAttempt = null;
entry.stopPromise = null;
clearTimeout(attempt.timer);
attempt.resolve(result);
}
function confirmExit(entry) {
const current = trackedChildren.get(entry.processId);
if (!current || current.child !== entry.child) return;
trackedChildren.delete(entry.processId);
finishStopAttempt(entry, {
success: true,
processId: entry.processId,
deviceId: entry.deviceId,
status: 'stopped'
});
}
function launchProxy(request) {
if (!request || typeof request !== 'object' || Array.isArray(request)) {
throw new ProxyLaunchError('Proxy launch request is invalid.', 'INVALID_REQUEST');
@@ -139,19 +171,24 @@ function createProxyManager({
processId: child.pid,
deviceId,
startedAt: now(),
status: 'running'
status: 'running',
stopAttempt: null,
stopPromise: null
};
trackedChildren.set(entry.processId, entry);
if (typeof child.once === 'function') {
child.once('exit', () => removeIfOwned(entry.processId, child));
child.once('error', () => removeIfOwned(entry.processId, child));
child.once('exit', () => confirmExit(entry));
child.once('close', () => confirmExit(entry));
child.once('error', () => {
if (hasConfirmedExit(child)) confirmExit(entry);
});
}
return { success: true, ...safeMetadata(entry) };
}
function stopProxy(processId) {
async function stopProxy(processId) {
if (!Number.isSafeInteger(processId) || processId <= 0) {
throw new ProxyLaunchError('Process identifier is invalid.', 'INVALID_PROCESS_ID');
}
@@ -159,28 +196,56 @@ function createProxyManager({
const entry = trackedChildren.get(processId);
if (!entry) return { success: false, processId, status: 'not-tracked' };
try {
const requested = entry.child.kill('SIGTERM');
if (!requested) {
removeIfOwned(processId, entry.child);
return {
success: true,
processId,
deviceId: entry.deviceId,
status: 'already-exited'
};
}
if (hasConfirmedExit(entry.child)) {
removeIfOwned(processId, entry.child);
return {
success: true,
processId,
deviceId: entry.deviceId,
status: 'stop-requested'
status: 'already-exited'
};
}
if (entry.stopPromise) return entry.stopPromise;
entry.status = 'stopping';
entry.stopPromise = new Promise((resolve) => {
const timer = setTimeout(() => {
finishStopAttempt(entry, {
success: false,
processId,
deviceId: entry.deviceId,
status: 'stop-timeout',
message: 'Timed out waiting for the tracked proxy process to exit.'
});
}, stopTimeoutMs);
entry.stopAttempt = { resolve, timer };
});
const stopPromise = entry.stopPromise;
try {
const requested = entry.child.kill('SIGTERM');
if (!requested) {
if (hasConfirmedExit(entry.child)) {
removeIfOwned(processId, entry.child);
finishStopAttempt(entry, {
success: true,
processId,
deviceId: entry.deviceId,
status: 'already-exited'
});
} else {
finishStopAttempt(entry, {
success: false,
processId,
deviceId: entry.deviceId,
status: 'stop-failed',
message: 'Unable to confirm that the tracked proxy process exited.'
});
}
}
} catch (error) {
const permissionDenied = error && (error.code === 'EPERM' || error.code === 'EACCES');
return {
finishStopAttempt(entry, {
success: false,
processId,
deviceId: entry.deviceId,
@@ -188,8 +253,9 @@ function createProxyManager({
message: permissionDenied
? 'Unable to stop the tracked proxy process: permission denied.'
: 'Unable to stop the tracked proxy process.'
};
});
}
return stopPromise;
}
function listTrackedProxies() {
@@ -200,6 +266,7 @@ function createProxyManager({
}
module.exports = {
DEFAULT_STOP_TIMEOUT_MS,
HELPER_FILENAME,
MAX_USERNAME_LENGTH,
ProxyLaunchError,