fix: confirm proxy exit before disconnect
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user