feat: the browser terminal on a WebSocket too, long polls when it does not connect or drops

This commit is contained in:
redtea committed 2026-10-14 21:06:32 +03:00
1 parent 73bb720ec6
commit 47f1b2e55b
4 files changed
+127 -12

No files matched your search

+42 -10
View File
@@ -76,13 +76,47 @@
this.sid = (await r.json()).session_id;
this.setState('wait', 'ожидание агента…');
this.fit();
// A WebSocket if it connects; the long polls otherwise, and from a
// dropped WebSocket on for the rest of this session.
if (!(await this.openWS())) this.poll();
this.queueIn({ t: 'r', cols: this.term.cols, rows: this.term.rows });
this.poll();
} catch (e) {
this.setState('err', 'ошибка сети');
this.term.write('\r\n\x1b[31m[ошибка сети: ' + (e && e.message ? e.message : e) + ']\x1b[0m\r\n');
}
}
openWS() {
return new Promise(resolve => {
if (!window.WebSocket) { resolve(false); return; }
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + base + this.sid + '/ws');
let opened = false;
const timer = setTimeout(() => { if (!opened) { ws.close(); resolve(false); } }, 5000);
ws.onopen = () => { opened = true; clearTimeout(timer); this.ws = ws; resolve(true); };
ws.onmessage = ev => {
let j;
try { j = JSON.parse(ev.data); } catch (e) { return; }
this.take(j);
};
ws.onclose = () => {
clearTimeout(timer);
if (!opened) { resolve(false); return; }
this.ws = null;
if (!this.closed && !this.ended) this.poll();
};
});
}
// take applies one answer of the output channel, either transport's.
take(j) {
if (j.closed) { this.finish('[сессия завершена]'); return false; }
for (const f of j.frames || []) {
if (f.exit) { this.finish('[сессия завершена]'); return false; }
if (f.data) {
if (!this.connected) { this.connected = true; this.setState('ok', 'подключено'); }
this.term.write(b64ToBytes(f.data));
}
}
return true;
}
async poll() {
while (!this.closed && this.sid) {
let r;
@@ -90,15 +124,7 @@
catch (e) { await new Promise(res => setTimeout(res, 1000)); continue; }
if (this.closed) return;
if (!r.ok) { this.finish('[соединение закрыто]'); return; }
const j = await r.json();
if (j.closed) { this.finish('[сессия завершена]'); return; }
for (const f of j.frames || []) {
if (f.exit) { this.finish('[сессия завершена]'); return; }
if (f.data) {
if (!this.connected) { this.connected = true; this.setState('ok', 'подключено'); }
this.term.write(b64ToBytes(f.data));
}
}
if (!this.take(await r.json())) return;
}
}
finish(msg) {
@@ -126,6 +152,11 @@
const body = m.t === 'i'
? { t: 'i', d: bytesToB64(new TextEncoder().encode(m.text)), seq: ++this.inSeq }
: { t: 'r', cols: m.cols, rows: m.rows, seq: ++this.inSeq };
// On the WebSocket a message goes out in order with no answer to wait
// for; numbered as well, so a repeat by the long polls is dropped.
if (this.ws && this.ws.readyState === 1) {
try { this.ws.send(JSON.stringify(body)); continue; } catch (e) { /* by request below */ }
}
for (let wait = 300; !this.closed; wait = Math.min(wait * 2, 5000)) {
let r = null;
try {
@@ -145,6 +176,7 @@
close() {
if (this.closed) return;
this.closed = true;
if (this.ws) { try { this.ws.close(); } catch (e) { /* closed already */ } }
if (this.sid) fetch(base + this.sid + '/close', { method: 'POST', keepalive: true }).catch(() => {});
this.term.dispose();
this.el.remove();