VNC-275 implement explicit keepalive (#172)

* VNC-275 implement explicit keepalive

* VNC-275 graceful disconnect

---------

Co-authored-by: Matt McClaskey <matt@kasmweb.com>
This commit is contained in:
Matt McClaskey 2025-11-03 13:03:43 -05:00 committed by GitHub
parent 0baf17a263
commit 887662b0ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 103 additions and 4 deletions

View File

@ -1579,7 +1579,7 @@ const UI = {
}, 10000);
} else {
//send keep-alive
UI.rfb.sendKey(1, null, false);
UI.rfb.sendKeepAlive();
}
}
}, 5000);
@ -1856,7 +1856,11 @@ const UI = {
},
disconnectedRx(event) {
parent.postMessage({ action: 'disconnectrx', value: event.detail.reason}, '*' );
const detail = event.detail || {};
if (detail.serverNotice && detail.serverNotice.graceful) {
return;
}
parent.postMessage({ action: 'disconnectrx', value: detail.reason}, '*' );
},
toggleNav(){

View File

@ -54,6 +54,7 @@ export const encodings = {
pseudoEncodingVideoOutTimeLevel1: -1986,
pseudoEncodingVideoOutTimeLevel100: -1887,
pseudoEncodingQOI: -1886,
pseudoEncodingKasmDisconnectNotify: -1885,
pseudoEncodingVMwareCursor: 0x574d5664,
pseudoEncodingVMwareCursorPosition: 0x574d5666,

View File

@ -43,6 +43,8 @@ import { toSignedRelative16bit } from './util/int.js';
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
const CLIENT_MSG_TYPE_KEEPALIVE = 184;
const SERVER_MSG_TYPE_DISCONNECT_NOTIFY = 185;
// Minimum wait (ms) between two mouse moves
const MOUSE_MOVE_DELAY = 17;
@ -105,6 +107,10 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = '';
this._rfbAuthScheme = -1;
this._rfbCleanDisconnect = true;
this._disconnectReason = null;
this._disconnectCode = null;
this._serverDisconnectNotice = null;
this._lastServerDisconnectNotice = null;
// Server capabilities
this._rfbVersion = 0;
@ -996,6 +1002,16 @@ export default class RFB extends EventTargetMixin {
}
}
sendKeepAlive() {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._isPrimaryDisplay) {
RFB.messages.keepAlive(this._sock);
} else {
this._proxyRFBMessage('keepAlive', []);
}
}
focus() {
this._keyboard.focus();
}
@ -1178,7 +1194,26 @@ export default class RFB extends EventTargetMixin {
}
msg += ")";
}
switch (this._rfbConnectionState) {
if (typeof e.code === 'number') {
this._disconnectCode = e.code;
}
if (e.reason) {
this._disconnectReason = e.reason;
}
if (this._serverDisconnectNotice) {
const notice = this._serverDisconnectNotice;
if (notice.reason && !this._disconnectReason) {
this._disconnectReason = notice.reason;
}
this._rfbCleanDisconnect = !!notice.graceful;
this._lastServerDisconnectNotice = notice;
this._serverDisconnectNotice = null;
} else if (e.wasClean === false || e.code === 1006) {
this._rfbCleanDisconnect = false;
}
switch (this._rfbConnectionState) {
case 'connecting':
this._fail("Connection closed " + msg);
break;
@ -1723,6 +1758,14 @@ export default class RFB extends EventTargetMixin {
Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
if (state === 'connecting') {
this._disconnectReason = null;
this._disconnectCode = null;
this._serverDisconnectNotice = null;
this._lastServerDisconnectNotice = null;
this._rfbCleanDisconnect = true;
}
if (this._disconnTimer && state !== 'disconnecting') {
Log.Debug("Clearing disconnect timer");
clearTimeout(this._disconnTimer);
@ -1756,7 +1799,13 @@ export default class RFB extends EventTargetMixin {
case 'disconnected':
this.dispatchEvent(new CustomEvent(
"disconnect", { detail:
{ clean: this._rfbCleanDisconnect } }));
{ clean: this._rfbCleanDisconnect,
reason: this._disconnectReason,
code: this._disconnectCode,
serverNotice: this._lastServerDisconnectNotice } }));
this._disconnectReason = null;
this._disconnectCode = null;
this._lastServerDisconnectNotice = null;
break;
}
}
@ -1781,6 +1830,9 @@ export default class RFB extends EventTargetMixin {
Log.Error("RFB failure: " + details);
break;
}
this._disconnectReason = details;
this._disconnectCode = null;
this._serverDisconnectNotice = null;
this._rfbCleanDisconnect = false; //This is sent to the UI
// Transition to disconnected without waiting for socket to close
@ -1890,6 +1942,9 @@ export default class RFB extends EventTargetMixin {
RFB.messages.pointerEvent(this._sock, this._mousePos.x, this._mousePos.y, 0, event.data.args[2], event.data.args[3]);
this._setLastActive();
break;
case 'keepAlive':
RFB.messages.keepAlive(this._sock);
break;
case 'keyEvent':
RFB.messages.keyEvent(this._sock, ...event.data.args);
this._setLastActive();
@ -3163,6 +3218,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingContinuousUpdates);
encs.push(encodings.pseudoEncodingDesktopName);
encs.push(encodings.pseudoEncodingExtendedClipboard);
encs.push(encodings.pseudoEncodingKasmDisconnectNotify);
if (this._hasWebp())
encs.push(encodings.pseudoEncodingWEBP);
if (this._enableQOI)
@ -3692,6 +3748,8 @@ export default class RFB extends EventTargetMixin {
case 183: // KASM unix relay data
return this._handleUnixRelay();
case SERVER_MSG_TYPE_DISCONNECT_NOTIFY: // KASM disconnect notice
return this._handleDisconnectNotify();
case 248: // ServerFence
return this._handleServerFenceMsg();
@ -3857,6 +3915,32 @@ export default class RFB extends EventTargetMixin {
processRelay && processRelay(payload);
}
_handleDisconnectNotify() {
if (this._sock.rQwait("DisconnectNotify header", 8, 1)) { return false; }
const flags = this._sock.rQshift8();
this._sock.rQskipBytes(3);
const reasonLength = this._sock.rQshift32();
if (reasonLength > 0 && this._sock.rQwait("DisconnectNotify reason", reasonLength, 8)) { return false; }
let reason = null;
if (reasonLength > 0) {
reason = this._sock.rQshiftStr(reasonLength);
}
const graceful = (flags & 0x1) !== 0;
this._serverDisconnectNotice = { flags, reason, graceful };
if (reason !== null) {
this._disconnectReason = reason;
}
if (graceful) {
this._rfbCleanDisconnect = true;
}
return true;
}
_framebufferUpdate() {
if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
@ -4425,6 +4509,16 @@ RFB.messages = {
sock.flush();
},
keepAlive(sock) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = CLIENT_MSG_TYPE_KEEPALIVE;
sock._sQlen += 1;
sock.flush();
},
// Used to build Notify and Request data.
_buildExtendedClipboardFlags(actions, formats) {
let data = new Uint8Array(4);