From f00b1e37769210dd243b722646bee1ed3d0deb0a Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Thu, 15 Jul 2010 19:38:25 -0500 Subject: [PATCH] State machine refactoring. Add new states 'loaded', 'connect' and 'fatal': - Loaded state is first page state. Pass WebSockets mode message using this state. - Connect indicates that the user has issued a "connect" but we haven't gotten an WebSockets onopen yet. - Fatal is a condition that indicates inability to continue on: right now, lack of WebSockets/Flash or non-working canvas. Move much of the actual state transition code into updateState. Handle 'password' state better in default_controls.js; instead of disconnecting, prompt for password to send. Add comments to updateState indicating possible states. --- include/default_controls.js | 16 +++ include/vnc.js | 203 +++++++++++++++++++++++++++--------- vnc_auto.html | 32 +++--- 3 files changed, 187 insertions(+), 64 deletions(-) diff --git a/include/default_controls.js b/include/default_controls.js index 55ba75f..a6c7ab0 100644 --- a/include/default_controls.js +++ b/include/default_controls.js @@ -82,6 +82,12 @@ load: function(target) { }; }, +setPassword: function() { + console.log("setPassword"); + RFB.sendPassword($('VNC_password').value); + return false; +}, + sendCtrlAltDel: function() { RFB.sendCtrlAltDel(); }, @@ -94,6 +100,7 @@ updateState: function(state, msg) { cad = $('sendCtrlAltDelButton'); switch (state) { case 'failed': + case 'fatal': c.disabled = true; cad.disabled = true; klass = "VNC_status_error"; @@ -106,6 +113,7 @@ updateState: function(state, msg) { klass = "VNC_status_normal"; break; case 'disconnected': + case 'loaded': c.value = "Connect"; c.onclick = DefaultControls.connect; @@ -113,6 +121,14 @@ updateState: function(state, msg) { cad.disabled = true; klass = "VNC_status_normal"; break; + case 'password': + c.value = "Send Password"; + c.onclick = DefaultControls.setPassword; + + c.disabled = false; + cad.disabled = true; + klass = "VNC_status_warn"; + break; default: c.disabled = true; cad.disabled = true; diff --git a/include/vnc.js b/include/vnc.js index abca2d9..9f6468f 100644 --- a/include/vnc.js +++ b/include/vnc.js @@ -124,22 +124,26 @@ load: function () { /* Load web-socket-js if no builtin WebSocket support */ if (VNC_native_ws) { Util.Info("Using native WebSockets"); - RFB.updateState('disconnected', 'Disconnected'); + RFB.updateState('loaded', 'noVNC ready (using native WebSockets)'); } else { Util.Warn("Using web-socket-js flash bridge"); if ((! Util.Flash) || (Util.Flash.version < 9)) { - RFB.updateState('failed', "WebSockets or Adobe Flash is required"); + RFB.updateState('fatal', "WebSockets or Adobe Flash is required"); } else if (document.location.href.substr(0, 7) === "file://") { - RFB.updateState('failed', + RFB.updateState('fatal', "'file://' URL is incompatible with Adobe Flash"); } else { - RFB.updateState('disconnected', 'Disconnected'); + RFB.updateState('loaded', 'noVNC ready (using Flash WebSockets emulation)'); } } // Initialize canvas/fxcanvas - Canvas.init(RFB.canvasID); + try { + Canvas.init(RFB.canvasID); + } catch (exc) { + RFB.updateState('fatal', "No working Canvas"); + } // Populate encoding lookup tables RFB.encHandlers = {}; @@ -171,36 +175,17 @@ connect: function (host, port, password, encrypt, true_color) { } if ((!RFB.host) || (!RFB.port)) { - RFB.updateState('disconnected', "Must set host and port"); + RFB.updateState('failed', "Must set host and port"); return; } - RFB.init_vars(); - - if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) { - RFB.ws.close(); - } - RFB.init_ws(); - - RFB.updateState('ProtocolVersion'); + RFB.updateState('connect'); //Util.Debug("<< connect"); }, disconnect: function () { //Util.Debug(">> disconnect"); - if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) { - RFB.ws.close(); - RFB.updateState('closed'); - RFB.ws.onmessage = function (e) { return; }; - } - if (Canvas.ctx) { - Canvas.stop(); - if (! /__debug__$/i.test(document.location.href)) { - Canvas.clear(); - } - } - RFB.updateState('disconnected', 'Disconnected'); //Util.Debug("<< disconnect"); }, @@ -327,6 +312,21 @@ init_msg: function () { RFB.version = RFB.max_version; } + RFB.sendID = setInterval(function() { + /* + * Send updates either at a rate of one update every 50ms, + * or whatever slower rate the network can handle + */ + if (RFB.ws.bufferedAmount === 0) { + if (RFB.SQ) { + RFB.ws.send(RFB.SQ); + RFB.SQ = ""; + } + } else { + Util.Debug("Delaying send"); + } + }, 50); + cversion = "00" + parseInt(RFB.version,10) + ".00" + ((RFB.version * 10) % 10); RFB.send_string("RFB " + cversion + "\n"); @@ -782,7 +782,7 @@ display_hextile: function() { if (subencoding > 30) { // Raw RFB.updateState('failed', "Disconnected: illegal hextile subencoding " + subencoding); - Util.Debug("RQ.slice(0,30):" + RQ.slice(0,30)); + //Util.Debug("RQ.slice(0,30):" + RQ.slice(0,30)); return; } subrects = 0; @@ -1314,27 +1314,134 @@ externalUpdateState: function(state, msg) { // Stub }, +/* + * Running states: + * disconnected - idle state + * normal - connected + * + * Page states: + * loaded - page load, equivalent to disconnected + * connect - starting initialization + * password - waiting for password + * failed - abnormal transition to disconnected + * fatal - failed to load page, or fatal error + * + * VNC initialization states: + * ProtocolVersion + * Security + * Authentication + * SecurityResult + * ServerInitialization + */ updateState: function(state, statusMsg) { - var func, cmsg; - if (state === 'failed') { + var func, cmsg, oldstate = RFB.state; + if (state === oldstate) { + /* Already here, ignore */ + Util.Debug("Already in state '" + state + "', ignoring."); + return; + } + + if (oldstate === 'fatal') { + Util.Error("Fatal error, cannot continue"); + } + + if ((state === 'failed') || (state === 'fatal')) { func = Util.Error; } else { func = Util.Warn; } cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; - func("New state '" + state + "'." + cmsg); + func("New state '" + state + "', was '" + oldstate + "'." + cmsg); + + if ((oldstate === 'failed') && (state === 'disconnected')) { + // Do disconnect action, but stay in failed state. + RFB.state = 'failed'; + } else { + RFB.state = state; + } + + switch (state) { + case 'loaded': + case 'disconnected': + + if (RFB.sendID) { + clearInterval(RFB.sendID); + RFB.sendID = null; + } + + if (RFB.ws) { + if (RFB.ws.readyState === WebSocket.OPEN) { + RFB.ws.close(); + } + RFB.ws.onmessage = function (e) { return; }; + } + + if (Canvas.ctx) { + Canvas.stop(); + if (! /__debug__$/i.test(document.location.href)) { + Canvas.clear(); + } + } - if ((state === 'disconnected') && (RFB.state !== 'disconnected')) { RFB.show_timings(); + + break; + + + case 'connect': + RFB.init_vars(); + + if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) { + RFB.ws.close(); + } + RFB.init_ws(); // onopen transitions to 'ProtocolVersion' + + break; + + + case 'password': + // Ignore password state by default + break; + + + case 'normal': + if ((oldstate === 'disconnected') || (oldstate === 'failed')) { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } + + break; + + + case 'failed': + if (oldstate === 'disconnected') { + Util.Error("Invalid transition from 'disconnected' to 'failed'"); + } + if (oldstate === 'normal') { + Util.Error("Error while connected."); + } + if (oldstate === 'init') { + Util.Error("Error while initializing."); + } + + if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) { + RFB.ws.close(); + } + // Make sure we transition to disconnected + setTimeout(function() { RFB.updateState('disconnected'); }, 50); + + break; + + + default: + // Invalid state transition + } - if ((RFB.state === 'failed') && - ((state === 'disconnected') || (state === 'closed'))) { + if ((oldstate === 'failed') && (state === 'disconnected')) { // Leave the failed message RFB.externalUpdateState(state); } else { - RFB.state = state; RFB.externalUpdateState(state, statusMsg); } }, @@ -1418,27 +1525,20 @@ init_ws: function () { RFB.ws.onmessage = RFB.recv_message; RFB.ws.onopen = function(e) { Util.Debug(">> WebSocket.onopen"); - RFB.updateState('ProtocolVersion', "Starting VNC handshake"); - RFB.sendID = setInterval(function() { - /* - * Send updates either at a rate of one update every 50ms, - * or whatever slower rate the network can handle - */ - if (RFB.ws.bufferedAmount === 0) { - if (RFB.SQ) { - RFB.ws.send(RFB.SQ); - RFB.SQ = ""; - } - } else { - Util.Debug("Delaying send"); - } - }, 50); + if (RFB.state === "connect") { + RFB.updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + RFB.updateState('failed', "Got unexpected WebSockets connection"); + } Util.Debug("<< WebSocket.onopen"); }; RFB.ws.onclose = function(e) { Util.Debug(">> WebSocket.onclose"); - clearInterval(RFB.sendID); - RFB.updateState('disconnected', 'VNC disconnected'); + if (RFB.state === 'normal') { + RFB.updateState('failed', 'Server disconnected'); + } else { + RFB.updateState('disconnected', 'VNC disconnected'); + } Util.Debug("<< WebSocket.onclose"); }; RFB.ws.onerror = function(e) { @@ -1450,7 +1550,6 @@ init_ws: function () { setTimeout(function () { if (RFB.ws.readyState === WebSocket.CONNECTING) { RFB.updateState('failed', "Connect timeout"); - RFB.ws.close(); } }, RFB.connectTimeout); diff --git a/vnc_auto.html b/vnc_auto.html index c470a6e..a1a2406 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -46,10 +46,26 @@ Connect parameters are provided in query string: sb = $('VNC_status_bar'); cad = $('sendCtrlAltDelButton'); switch (state) { - case 'failed': klass = "VNC_status_error"; break; - case 'normal': klass = "VNC_status_normal"; break; - case 'disconnected': klass = "VNC_status_normal"; break; - default: klass = "VNC_status_warn"; break; + case 'failed': + case 'fatal': + klass = "VNC_status_error"; + break; + case 'normal': + klass = "VNC_status_normal"; + break; + case 'disconnected': + case 'loaded': + klass = "VNC_status_normal"; + break; + case 'password': + msg = '