feat: improve session limit handling and add k6 load testing
This commit is contained in:
+119
-22
@@ -65,8 +65,15 @@ cat > /opt/portal/index.html <<'HTML'
|
||||
const statusEl = document.getElementById('status');
|
||||
const XK_ALT_L = 0xffe9;
|
||||
const XK_LEFT = 0xff51;
|
||||
|
||||
let rfb = null;
|
||||
let connected = false;
|
||||
let connectTimer = null;
|
||||
let reconnectTimer = null;
|
||||
let reconnectAttempts = 0;
|
||||
const MAX_RECONNECT_ATTEMPTS = 12;
|
||||
const RECONNECT_DELAYS_MS = [1000, 2000, 3000, 5000, 8000];
|
||||
let manualDisconnect = false;
|
||||
|
||||
function showStatus(text, isError = false) {
|
||||
statusEl.textContent = text;
|
||||
@@ -78,73 +85,160 @@ cat > /opt/portal/index.html <<'HTML'
|
||||
statusEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
showStatus('Подключение к слоту...');
|
||||
connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
showStatus('Нет подключения к экрану слота. Откройте сервис заново из дашборда.', true);
|
||||
function clearConnectTimer() {
|
||||
if (connectTimer) {
|
||||
clearTimeout(connectTimer);
|
||||
connectTimer = null;
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
const rfb = new RFB(document.getElementById('screen'), wsUrl);
|
||||
rfb.viewOnly = false;
|
||||
rfb.scaleViewport = true;
|
||||
rfb.resizeSession = true;
|
||||
rfb.addEventListener('connect', () => {
|
||||
connected = true;
|
||||
if (connectTimer) clearTimeout(connectTimer);
|
||||
hideStatus();
|
||||
});
|
||||
rfb.addEventListener('disconnect', () => {
|
||||
function clearReconnectTimer() {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function reconnectDelay(attemptNumber) {
|
||||
const idx = Math.min(Math.max(attemptNumber - 1, 0), RECONNECT_DELAYS_MS.length - 1);
|
||||
return RECONNECT_DELAYS_MS[idx];
|
||||
}
|
||||
|
||||
function scheduleConnectTimeout() {
|
||||
clearConnectTimer();
|
||||
connectTimer = setTimeout(() => {
|
||||
if (!connected && !manualDisconnect) {
|
||||
scheduleReconnect('Нет подключения к экрану слота. Пробуем переподключиться...');
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
function scheduleReconnect(reasonText) {
|
||||
if (manualDisconnect) return;
|
||||
clearConnectTimer();
|
||||
clearReconnectTimer();
|
||||
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
showStatus('Соединение со слотом потеряно. Переподключение не удалось. Откройте сервис заново.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextAttempt = reconnectAttempts + 1;
|
||||
const delayMs = reconnectDelay(nextAttempt);
|
||||
const delaySec = Math.ceil(delayMs / 1000);
|
||||
showStatus(`${reasonText} Повтор ${nextAttempt}/${MAX_RECONNECT_ATTEMPTS} через ${delaySec} сек.`, true);
|
||||
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectAttempts = nextAttempt;
|
||||
connectRfb('Переподключение к слоту...');
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function attachRfb() {
|
||||
if (rfb) {
|
||||
try { rfb.disconnect(); } catch (e) {}
|
||||
}
|
||||
rfb = new RFB(document.getElementById('screen'), wsUrl);
|
||||
rfb.viewOnly = false;
|
||||
rfb.scaleViewport = true;
|
||||
rfb.resizeSession = true;
|
||||
|
||||
rfb.addEventListener('connect', () => {
|
||||
connected = true;
|
||||
reconnectAttempts = 0;
|
||||
clearConnectTimer();
|
||||
clearReconnectTimer();
|
||||
hideStatus();
|
||||
});
|
||||
|
||||
rfb.addEventListener('disconnect', () => {
|
||||
connected = false;
|
||||
if (manualDisconnect) return;
|
||||
scheduleReconnect('Соединение со слотом потеряно.');
|
||||
});
|
||||
}
|
||||
|
||||
function connectRfb(statusText) {
|
||||
if (manualDisconnect) return;
|
||||
connected = false;
|
||||
showStatus('Соединение со слотом потеряно. Запустите сервис заново.', true);
|
||||
});
|
||||
showStatus(statusText || 'Подключение к слоту...');
|
||||
attachRfb();
|
||||
scheduleConnectTimeout();
|
||||
}
|
||||
|
||||
const enableHeartbeat = (new URLSearchParams(location.search).get('hb') ?? '1') !== '0';
|
||||
const sid = new URLSearchParams(location.search).get('sid');
|
||||
const SESSION_CLOSED_URL = '/?session_closed=idle';
|
||||
const SESSION_CLOSED_URL_BASE = '/?session_closed=';
|
||||
const CLOSE_PATH = sid ? `/api/sessions/${sid}/close` : '';
|
||||
function goSessionClosed() {
|
||||
|
||||
function goSessionClosed(reason = 'idle') {
|
||||
const safeReason = reason === 'limit' ? 'limit' : 'idle';
|
||||
const target = `${SESSION_CLOSED_URL_BASE}${safeReason}`;
|
||||
try {
|
||||
if (window.top && window.top !== window) {
|
||||
window.top.location.href = SESSION_CLOSED_URL;
|
||||
window.top.location.href = target;
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
window.location.href = SESSION_CLOSED_URL;
|
||||
window.location.href = target;
|
||||
}
|
||||
|
||||
async function touch() {
|
||||
if (!sid) return;
|
||||
try {
|
||||
const res = await fetch(`/api/sessions/${sid}/touch`, {method:'POST', credentials:'include'});
|
||||
if (!res.ok) {
|
||||
goSessionClosed();
|
||||
let reason = 'idle';
|
||||
try {
|
||||
const payload = await res.json();
|
||||
if (payload && typeof payload.reason === 'string') {
|
||||
reason = payload.reason;
|
||||
}
|
||||
} catch (e) {}
|
||||
goSessionClosed(reason);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
let closing = false;
|
||||
async function closeSessionNow() {
|
||||
if (!CLOSE_PATH || closing) return;
|
||||
closing = true;
|
||||
manualDisconnect = true;
|
||||
clearConnectTimer();
|
||||
clearReconnectTimer();
|
||||
try {
|
||||
if (rfb) rfb.disconnect();
|
||||
} catch (e) {}
|
||||
try {
|
||||
await fetch(CLOSE_PATH, {method: 'POST', credentials: 'include', keepalive: true});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (enableHeartbeat) {
|
||||
setInterval(touch, 15000);
|
||||
touch();
|
||||
window.addEventListener('pagehide', closeSessionNow);
|
||||
window.addEventListener('beforeunload', closeSessionNow);
|
||||
}
|
||||
|
||||
function keyTap(keysym, code) {
|
||||
if (!rfb) return;
|
||||
rfb.sendKey(keysym, code, true);
|
||||
rfb.sendKey(keysym, code, false);
|
||||
}
|
||||
|
||||
function chord(mod, key, modCode, keyCode) {
|
||||
if (!rfb) return;
|
||||
rfb.sendKey(mod, modCode, true);
|
||||
keyTap(key, keyCode);
|
||||
rfb.sendKey(mod, modCode, false);
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
manualDisconnect = true;
|
||||
clearConnectTimer();
|
||||
clearReconnectTimer();
|
||||
try {
|
||||
if (window.top && window.top !== window) {
|
||||
window.top.location.href = '/';
|
||||
@@ -153,9 +247,12 @@ cat > /opt/portal/index.html <<'HTML'
|
||||
} catch (e) {}
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
document.getElementById('btn-back').addEventListener('click', () => chord(XK_ALT_L, XK_LEFT, 'AltLeft', 'ArrowLeft'));
|
||||
document.getElementById('btn-home').addEventListener('click', goHome);
|
||||
document.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
connectRfb('Подключение к слоту...');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user