=== modified file 'src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java' --- src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java 2016-02-23 16:33:44 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/WebClientMessageTypes.java 2016-02-24 22:37:36 +0000 @@ -19,7 +19,7 @@ ** and z-order operations. ** 005 CA 20151024 Added support for WINDOW:SENSITIVE attribute. ** 006 CA 20160203 Added hash management operations. -** SBI 20160220 Added MSG_PARTIAL, MSG_FILE_UPLOAD +** SBI 20160225 Added MSG_PARTIAL, MSG_PING_PONG */ package com.goldencode.p2j.ui.client.driver.web; @@ -187,5 +187,8 @@ /** Wraps the large message from the client to the sequence of this partial type messages. */ public static final byte MSG_PARTIAL = (byte) 0xFE; - + + /** Ping/Pong messages. */ + public static final byte MSG_PING_PONG = (byte) 0xFC; + } === modified file 'src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java' --- src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java 2016-02-24 06:12:51 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/WebClientProtocol.java 2016-02-24 22:38:20 +0000 @@ -19,7 +19,7 @@ ** message result. ** 005 CA 20160203 Fixed deadlock for receivedMessages - it must lock on the "lock" field, as ** this object is used for all sock operations (including key reading). -** SBI 20160220 Added MSG_PARTIAL and MSG_FILE_UPLOAD. +** SBI 20160225 Added MSG_PARTIAL and MSG_PING_PONG. */ package com.goldencode.p2j.ui.client.driver.web; @@ -374,6 +374,11 @@ getMessagesCollector().processPartialMesssage(message, offset); handled = true; } + else if (message[offset] == MSG_PING_PONG && length == 1) + { + sendBinaryMessage(MSG_PING_PONG); + handled = true; + } return handled; } @@ -413,7 +418,11 @@ { synchronized (lock) { - session.close(); + if (error != null) + { + error.printStackTrace(); + } + session.close(StatusCode.ABNORMAL, ""); } } === modified file 'src/com/goldencode/p2j/ui/client/driver/web/WebPageHandler.java' --- src/com/goldencode/p2j/ui/client/driver/web/WebPageHandler.java 2016-02-20 18:15:08 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/WebPageHandler.java 2016-02-24 22:40:56 +0000 @@ -12,8 +12,8 @@ ** 001 GES 20150312 Created initial version by moving code from the ChuiWebPageHandler and making ** it more generic (to handle GUI too). ** 002 GES 20150717 Moved clipboard processing to ChUI-specific location. -** 003 SBI 20160220 Added maxBinaryMessage parameter to the template file in order to deliver -** its value to the JS client. +** 003 SBI 20160225 Added "maxBinaryMessage", "watchdogTimeout" and "maxIdleTime" parameters to +** the template file in order to deliver its value to the JS client. */ package com.goldencode.p2j.ui.client.driver.web; @@ -158,6 +158,31 @@ text = text.replace("${maxBinaryMessage}", String.valueOf(maxBinaryMessage)); } + if (text.contains("${maxIdleTime}")) + { + int socketTimeout = config.getInt("client", "web", "socketTimeout", -1); + int maxIdleTime; + // if socketTimeout > 0, then it overrides maxIdleTime + // at the start of the websocket session. + if (socketTimeout > 0) + { + maxIdleTime = socketTimeout; + } + else + { + // configuration must provide the valid value of client:web:maxIdleTime + maxIdleTime = config.getInt("client", "web", "maxIdleTime", -1); + } + text = text.replace("${maxIdleTime}", String.valueOf(maxIdleTime)); + } + + if (text.contains("${watchdogTimeout}")) + { + // a watch dog checks the elapsed time from its start one per a minute + int watchdogTimeout = config.getInt("client", "web", "watchdogTimeout", 60000); + text = text.replace("${watchdogTimeout}", String.valueOf(watchdogTimeout)); + } + return text; } === modified file 'src/com/goldencode/p2j/ui/client/driver/web/index.html' --- src/com/goldencode/p2j/ui/client/driver/web/index.html 2016-02-20 18:15:08 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/index.html 2016-02-24 13:43:13 +0000 @@ -33,16 +33,36 @@ "use strict"; p2j.init({ - 'isGui' : ${isGui}, - 'canvas' : {'size' : {'${height_name}' : ${height_val}, '${width_name}' : ${width_val}}, 'mouse' : false}, - 'font' : {'name' : '${font.name}', 'size' : ${font.size}, 'color' : {'f' : '${font.color}', 'b' : '${font.background}'}}, - 'cursor' : {'type' : 'solid', 'visible' : true, 'blinking' : false}, - 'sound' : {'id' : 'beep', 'enabled' : true}, - 'socket' : {'url' : 'wss://' + window.location.host + '${context}/ajax', 'maxBinaryMessage' : ${maxBinaryMessage}}, - 'page' : 'index.html', - 'clipboard' : {'enabled' : ${clipboard.enabled}, 'id' : 'clipboard', 'stroke' : true, 'input' : 'copy'}, - 'taskbar' : {'enabled' : ${taskbar.enabled}}, - 'referrer' : '${referrer}', + 'isGui' : ${isGui}, + 'canvas' : { + 'size' : { + '${height_name}' : ${height_val}, + '${width_name}' : ${width_val} + }, + 'mouse' : false + }, + 'font' : { + 'name' : '${font.name}', + 'size' : ${font.size}, + 'color' : {'f' : '${font.color}', 'b' : '${font.background}'} + }, + 'cursor' : {'type' : 'solid', 'visible' : true, 'blinking' : false}, + 'sound' : {'id' : 'beep', 'enabled' : true}, + 'socket' : { + 'url' : 'wss://' + window.location.host + '${context}/ajax', + 'maxBinaryMessage' : ${maxBinaryMessage}, + 'maxIdleTime' : ${maxIdleTime}, + 'watchdogTimeout' : ${watchdogTimeout} + }, + 'page' : 'index.html', + 'clipboard' : { + 'enabled' : ${clipboard.enabled}, + 'id' : 'clipboard', + 'stroke' : true, + 'input' : 'copy' + }, + 'taskbar' : {'enabled' : ${taskbar.enabled}}, + 'referrer' : '${referrer}', 'container' : 'cont' }); }); === modified file 'src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js' --- src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js 2016-02-24 06:12:51 +0000 +++ src/com/goldencode/p2j/ui/client/driver/web/res/p2j.socket.js 2016-02-24 22:36:46 +0000 @@ -42,7 +42,7 @@ ** available for the browser. ** CA 20160629 Added a drawing cache (and cache management, when the hash was expired by the ** client-side). -** SBI 20160202 Implemented MSG_PARTIAL. +** SBI 20160225 Implemented MSG_PARTIAL, added MSG_PING_PONG and idle/connectivity timers. */ "use strict"; @@ -70,6 +70,27 @@ /** The maximal size for binary messages that can be accepted by the websocket server. */ var maxBinaryMessage; + /** The maximal idle time for this websocket connection */ + var maxIdleTime; + + /** + * The maximal period within which to establish new connections with the same websocket server + * instance is possible. + */ + var watchdogTimeout; + + /** The websocket connection url */ + var socketUrl; + + /** The Idle Timer */ + var idleTimer; + + /** The Connectivity Timer */ + var connectivityTimer; + + /** Watch Ping/Pong */ + var pingPongWatcher; + /** * The next unique message id. */ @@ -610,6 +631,773 @@ return num; }; + + /** Web socket message handler */ + var messageHandler = function(message) + { + var t1 = (new Date()).getTime(); + callNo = callNo + 1; + + // reset the last activity time + idleTimer.reset(t1); + + switch (message[0]) + { + case 0x80: + // clear screen + p2j.screen.clear(); + break; + case 0x81: + p2j.screen.drawRectangles(message); + var t2 = (new Date()).getTime(); + console.log(callNo + ":" + drawNo + " draw: " + message.length + " done in " + (t2 - t1)); + return; + case 0x82: + // set cursor position + p2j.screen.setCursorPosition(message[1], message[2]); + break; + case 0x83: + // show cursor + p2j.screen.setCursorStatus(message[1]); + break; + case 0x84: + // message beep + p2j.sound.beep(); + break; + case 0x85: + // quit + window.location.replace(referrer); + break; + case 0x86: + // switch mode p2j/vt100 + p2j.keyboard.vt100 = (message[1] == 0) ? false : true; + break; + case 0x87: + // server-driven request for clipboard contents + p2j.clipboard.sendClipboardContents(); + break; + case 0x88: + // The clipboard is changed. + var text = me.readStringBinaryMessage(message, 1); + p2j.clipboard.writeClipboard(text); + break; + case 0x89: + // create a top-level window with the given id + var id = me.readInt32BinaryMessage(message, 1); + p2j.screen.createWindow(id); + break; + case 0x8A: + // create a child window with the given id, owner and title + var id = me.readInt32BinaryMessage(message, 1); + var owner = me.readInt32BinaryMessage(message, 5); + var title = me.readStringBinaryMessage(message, 9); + p2j.screen.createChildWindow(id, owner); + break; + case 0x8B: + // destroy top-level or child window + var id = me.readInt32BinaryMessage(message, 1); + var numberImages = me.readInt32BinaryMessage(message, 5); + var images = []; + for (var i = 0; i < numberImages; i++) + { + images[i] = me.readInt32BinaryMessage(message, 9 + (i * 4)); + } + p2j.screen.destroyWindow(id, images); + break; + case 0x8C: + // change visibility for top-level or child window + var id = me.readInt32BinaryMessage(message, 1); + var visible = message[5] === 0 ? false : true; + p2j.screen.setWindowVisible(id, visible); + break; + + // font and metrics related requests + case 0x8D: + // paragraph height + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var textLength = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var text = me.readStringBinaryMessageByLength(message, offset, textLength); + offset = offset + textLength * 2; + + var font = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var maxWidth = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + var pheight = p2j.screen.layoutParagraphWorker(null, + text, + font, + 0, + 0, + maxWidth); + + me.sendInt16BinaryMessage(0x06, msgId, pheight); + break; + case 0x8E: + // text height + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var textLength = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var text = me.readStringBinaryMessageByLength(message, offset, textLength); + offset = offset + textLength * 2; + + var font = me.readInt32BinaryMessage(message, offset); + + var theight = p2j.fonts.getTextHeight(font, text); + + me.sendInt8BinaryMessage(0x07, msgId, theight); + break; + case 0x8F: + // text width + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var textLength = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var text = me.readStringBinaryMessageByLength(message, offset, textLength); + offset = offset + textLength * 2; + + var font = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var twidth = p2j.fonts.getTextWidth(font, text); + + me.sendInt16BinaryMessage(0x08, msgId, twidth); + break; + case 0x90: + // font height + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var font = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var fheight = p2j.fonts.getFontHeight(font); + + me.sendInt8BinaryMessage(0x09, msgId, fheight); + break; + case 0x91: + // font widths + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var font = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var fwidths = p2j.fonts.getFontWidths(font); + + me.sendByteArrayBinaryMessage(0x0A, msgId, fwidths); + break; + case 0x92: + // create font + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var font = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var nameLength = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + var name = me.readStringBinaryMessageByLength(message, offset, nameLength); + offset = offset + nameLength * 2; + + var size = message[offset]; + offset = offset + 1; + + var style = message[offset]; + offset = offset + 1; + + var defLength = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var b64font = ""; + if (defLength > 0) + { + var binFont = ''; + for (var i = 0; i < defLength; i++) + { + binFont += String.fromCharCode(message[offset]); + offset = offset + 1; + } + b64font = window.btoa(binFont); + } + + var fontId = p2j.fonts.createFont(font, name, size, style, b64font); + + me.sendInt32BinaryMessage(0x0B, msgId, fontId); + break; + case 0x93: + // derive font + + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var font = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + p2j.fonts.deriveFont(font); + + me.sendInt8BinaryMessage(0x0C, msgId, 1); + break; + case 0x94: + // set cursor style + var styleId = me.readInt32BinaryMessage(message, 1); + var wid = me.readInt32BinaryMessage(message, 5); + p2j.screen.setCursorStyle(styleId, wid); + break; + case 0x95: + // restack windows + var num = me.readInt32BinaryMessage(message, 1); + var winids = []; + for (var i = 0; i < num; i++) + { + winids.push(me.readInt32BinaryMessage(message, 5 + (i * 4))); + } + p2j.screen.restackZOrderEntries(winids); + break; + case 0x96: + // register/deregister widgets for mouse actions + + var offset = 1; + + // the number of windows with new widgets + var windowNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var j = 0; j < windowNo; j++) + { + // the window ID + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + if (!p2j.screen.isExistingWindowId(windowID)) + { + console.log("undefined window " + windowID); + continue; + } + + var theWindow = p2j.screen.getWindow(windowID); + + // the number of new widgets in this window + var widgetNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var k = 0; k < widgetNo; k++) + { + // the widget ID + var wid = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + // the coordinates + var x = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + var y = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + // the size + var width = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + var height = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + // bit-encoded mouse ops: each bit from 1 to 11, if set, represents a mouse + // operation as defined in p2j.screen.mouseOps + var actions = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + theWindow.deregisterMouseWidget(wid); + theWindow.registerMouseWidget(wid, x, y, width, height, actions); + } + + var allWNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + var zOrder = []; + + for (var k = 0; k < allWNo; k++) + { + zOrder[k] = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + } + + theWindow.setWidgetZOrder(zOrder); + } + + // the number of windows with dead widgets + windowNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var j = 0; j < windowNo; j++) + { + // the window ID + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var theWindow = p2j.screen.getWindow(windowID); + + if (theWindow == undefined) + { + console.log("undefined window" + windowID); + continue; + } + + // the number of dead widgets in this window + var widgetNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var k = 0; k < widgetNo; k++) + { + // the widget ID + var wid = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + theWindow.deregisterMouseWidget(wid); + } + } + + // the number of windows with new "any widgets" + windowNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var j = 0; j < windowNo; j++) + { + // the window ID + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + if (!p2j.screen.isExistingWindowId(windowID)) + { + console.log("undefined window " + windowID); + continue; + } + + var theWindow = p2j.screen.getWindow(windowID); + + // the number of new "any widgets" in this window + var widgetNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var k = 0; k < widgetNo; k++) + { + // the widget ID + var wid = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + theWindow.deregisterAnyMouseWidget(wid); + theWindow.registerAnyMouseWidget(wid); + } + } + + // the number of windows with dead "any widgets" + windowNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var j = 0; j < windowNo; j++) + { + // the window ID + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var theWindow = p2j.screen.getWindow(windowID); + + if (theWindow == undefined) + { + console.log("undefined window" + windowID); + continue; + } + + // the number of dead "any widgets" in this window + var widgetNo = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + for (var k = 0; k < widgetNo; k++) + { + // the widget ID + var wid = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + theWindow.deregisterAnyMouseWidget(wid); + } + } + + break; + case 0x97: + // enable/disable mouse events + p2j.screen.captureMouseEvents(message[1] == 1); + break; + case 0x98: + // enable/disable OS events + var wid = me.readInt32BinaryMessage(message, 1); + p2j.screen.enableOsEvents(wid, message[5] == 1); + break; + case 0x99: + // set window iconification state + var offset = 1; + + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var iconified = (message[offset] == 1); + offset = offset + 1; + + var theWindow = p2j.screen.getWindow(windowID); + //p2j.logger.log("recieved 0x99: iconified = " + iconified + " windowId=" + windowID); + if (iconified) + { + theWindow.iconify(); + } + else + { + theWindow.deiconify(); + } + + break; + case 0x9A: + // resizeable window + var offset = 1; + + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var theWindow = p2j.screen.getWindow(windowID); + + theWindow.resizeable = (message[offset] == 1); + offset = offset + 1; + + if (theWindow.resizeable) + { + theWindow.minWidth = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + theWindow.minHeight = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + + theWindow.maxWidth = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + theWindow.maxHeight = me.readInt16BinaryMessage(message, offset); + offset = offset + 2; + } + else + { + var rect = theWindow.canvas.getBoundingClientRect(); + + var rwidth = rect.right - rect.left + 1; + var rheight = rect.bottom - rect.top + 1; + + theWindow.minWidth = rwidth; + theWindow.minHeight = rheight; + + theWindow.maxWidth = rwidth; + theWindow.maxHeight = rheight; + } + + break; + case 0x9B: + // current editors selection is changed + var text = me.readStringBinaryMessage(message, 1); + p2j.clipboard.setSelection(text); + break; + case 0x9C: + var id = me.readInt32BinaryMessage(message, 1); + p2j.screen.moveToTop(id); + break; + case 0x9D: + var id = me.readInt32BinaryMessage(message, 1); + p2j.screen.moveToBottom(id); + break; + case 0x9E: + // change sensitivity for top-level or child window + var id = me.readInt32BinaryMessage(message, 1); + var enabled = message[5] === 0 ? false : true; + p2j.screen.setWindowEnabled(id, enabled); + break; + case 0x9F: + // font is installed + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var fontNameLength = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + var fontName = me.readStringBinaryMessageByLength(message, offset, fontNameLength); + + var result = p2j.fonts.isFontInstalled(fontName); + + me.sendInt8BinaryMessage(0x9F, msgId, result ? 1 : 0); + break; + case 0xA0: + // remove hashes + var offset = 1; + + var msgId = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + // window ID + var windowID = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + // number of hashes + var hashNo = me.readInt32BinaryMessage(message, offset); + offset = offset + 4; + + if (windowID == 0) + { + for (var i = 0; i < hashNo; i++) + { + // read each hash and remove it + var hashBytes = message.slice(offset, offset + 16); + var hash = me.createHexString(hashBytes); + delete p2j.screen.globalCache[hash]; + + offset += 16; + } + } + else + { + var theWindow = p2j.screen.getWindow(windowID); + + for (var i = 0; i < hashNo; i++) + { + // read each hash and remove it + var hashBytes = message.slice(offset, offset + 16); + var hash = me.createHexString(hashBytes); + delete theWindow.removeCachedDraw(hash); + + offset += 16; + } + } + + me.sendInt8BinaryMessage(0x12, msgId, 1); + break; + case 0xfc: + // ping/pong server answer + clearTimeout(pingPongWatcher); + break; + }; + var t2 = (new Date()).getTime(); + console.log(callNo + ": " + message[0].toString(16) + " done in " + (t2 - t1)); + }; + + /** + * It is responsible for executing periodically with the given period the provided callback + * function if the elapsed time exceeds its period. The elapsed time is calulated using + * the current time and the last checkpoint time. + * + * @param {Number} maxLifeTime + * The maximal life time of the internal timer measured in milliseconds. + * @param {Number} period + * The period of the internal timer. + * @param {Function} callback + * The callback function or closure. + */ + function ControlTimer(maxLifeTime, period, callback) + { + /** The time of the last check */ + var checkpoint; + + /** + * If the elapsed time exceeds its period, then it invokes + * the callback function. If the elapsed time exceeds the maximal life time, + * then the timer is stopped. + */ + function check() + { + var test = (new Date()).getTime(); + var elapsed = test - checkpoint; + if (elapsed > maxLifeTime) + { + kill(); + } + if (elapsed > period) + { + callback(); + } + } + + /** The timer id */ + var timer; + + /** Indicates the timer is running */ + var running = false; + + /** + * Starts the timer that checks periodicaly with the given rate if the elapsed time + * exceeds its period. Sets the last checkpoint to the current time. + */ + function start() + { + reset((new Date()).getTime()); + if (running) + { + return; + } + timer = setInterval(check, period); + running = true; + } + + this.start = start; + + /** + * Resets the last check time. + * + * @param {Number} lastActivity + * The last activity time in milliseconds. + */ + function reset(lastActivity) + { + checkpoint = lastActivity; + } + + this.reset = reset; + + /** + * Stops and disposes the internal timer. It can be started again. + */ + function kill() + { + if (timer) + { + clearInterval(timer); + } + running = false; + } + + this.kill = kill; + } + + /** + * Tries to open a new websocket connection to the server. + * + * @param {String} url + * URL connection string + */ + function createWebSocket(url) + { + // check WebSocket support + if (!('WebSocket' in window)) + { + p2j.screen.error('WebSockets are NOT supported by your Browser.'); + return; + } + + ws = new WebSocket(url); + + // on web socket open + ws.onopen = function() + { + connected = true; + ws.binaryType = 'arraybuffer'; + + connectivityTimer.kill(); + idleTimer.start(); + + // notify the web socket has opened and the page is loaded + me.sendNotification(0xff); + }; + + // on web socket message + ws.onmessage = function(evt) + { + var data = evt.data; + + if (data instanceof ArrayBuffer) + { + // binary messages + var message = new Uint8Array(data); + var isDrawing = message[0] === 0x81; + if (isDrawing) + { + p2j.displayWaitCursor(); + } + setTimeout(function() { messageHandler(message); }, 0); + if (isDrawing) + { + setTimeout(function() { p2j.restoreCursor(); }, 0); + } + } + else + { + // text messages + var pay = JSON.parse(data); + setTimeout( + function() + { + switch (pay.c) + { + case 0: + // color palette + p2j.screen.palette = pay.p; + break; + }; + }, 0); + } + } + + /** + * Web socket close events handler + * + * @param {CloseEvent} evt + * The close websocket event + */ + ws.onclose = function(evt) + { + connected = false; + p2j.screen.error("Connection closed with the returned code " + evt.code); + idleTimer.kill(); + // check the normal close reason + if (evt.code === 1000) + { + setTimeout(function() { window.location.replace(referrer); }, 1000); + } + else + { + connectivityTimer.start(); + } + }; + + /** + * Web socket errors events handler + * + * @param {Event} evt + * The websocket error event + */ + ws.onerror = function(evt) + { + connected = false; + idleTimer.kill(); + connectivityTimer.start(); + }; + } /** * Initialize module. @@ -617,653 +1405,76 @@ * @param {object} cfg configuration. */ me.init = function(cfg) - { + { referrer = cfg.referrer; maxBinaryMessage = cfg.socket.maxBinaryMessage; - if ('WebSocket' in window) - { - ws = new WebSocket(cfg.socket.url); - - // on web socket open - ws.onopen = function() - { - connected = true; - ws.binaryType = 'arraybuffer'; - - // notify the web socket has opened and the page is loaded - me.sendNotification(0xff); - }; - - /** Web socket message handler */ - var messageHandler = function(message) - { - var t1 = (new Date()).getTime(); - callNo = callNo + 1; - - switch (message[0]) - { - case 0x80: - // clear screen - p2j.screen.clear(); - break; - case 0x81: - p2j.screen.drawRectangles(message); - var t2 = (new Date()).getTime(); - console.log(callNo + ":" + drawNo + " draw: " + message.length + " done in " + (t2 - t1)); - return; - case 0x82: - // set cursor position - p2j.screen.setCursorPosition(message[1], message[2]); - break; - case 0x83: - // show cursor - p2j.screen.setCursorStatus(message[1]); - break; - case 0x84: - // message beep - p2j.sound.beep(); - break; - case 0x85: - // quit - window.location.replace(referrer); - break; - case 0x86: - // switch mode p2j/vt100 - p2j.keyboard.vt100 = (message[1] == 0) ? false : true; - break; - case 0x87: - // server-driven request for clipboard contents - p2j.clipboard.sendClipboardContents(); - break; - case 0x88: - // The clipboard is changed. - var text = me.readStringBinaryMessage(message, 1); - p2j.clipboard.writeClipboard(text); - break; - case 0x89: - // create a top-level window with the given id - var id = me.readInt32BinaryMessage(message, 1); - p2j.screen.createWindow(id); - break; - case 0x8A: - // create a child window with the given id, owner and title - var id = me.readInt32BinaryMessage(message, 1); - var owner = me.readInt32BinaryMessage(message, 5); - var title = me.readStringBinaryMessage(message, 9); - p2j.screen.createChildWindow(id, owner); - break; - case 0x8B: - // destroy top-level or child window - var id = me.readInt32BinaryMessage(message, 1); - var numberImages = me.readInt32BinaryMessage(message, 5); - var images = []; - for (var i = 0; i < numberImages; i++) - { - images[i] = me.readInt32BinaryMessage(message, 9 + (i * 4)); - } - p2j.screen.destroyWindow(id, images); - break; - case 0x8C: - // change visibility for top-level or child window - var id = me.readInt32BinaryMessage(message, 1); - var visible = message[5] === 0 ? false : true; - p2j.screen.setWindowVisible(id, visible); - break; - - // font and metrics related requests - case 0x8D: - // paragraph height - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var textLength = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var text = me.readStringBinaryMessageByLength(message, offset, textLength); - offset = offset + textLength * 2; - - var font = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var maxWidth = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - var pheight = p2j.screen.layoutParagraphWorker(null, - text, - font, - 0, - 0, - maxWidth); - - me.sendInt16BinaryMessage(0x06, msgId, pheight); - break; - case 0x8E: - // text height - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var textLength = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var text = me.readStringBinaryMessageByLength(message, offset, textLength); - offset = offset + textLength * 2; - - var font = me.readInt32BinaryMessage(message, offset); - - var theight = p2j.fonts.getTextHeight(font, text); - - me.sendInt8BinaryMessage(0x07, msgId, theight); - break; - case 0x8F: - // text width - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var textLength = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var text = me.readStringBinaryMessageByLength(message, offset, textLength); - offset = offset + textLength * 2; - - var font = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var twidth = p2j.fonts.getTextWidth(font, text); - - me.sendInt16BinaryMessage(0x08, msgId, twidth); - break; - case 0x90: - // font height - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var font = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var fheight = p2j.fonts.getFontHeight(font); - - me.sendInt8BinaryMessage(0x09, msgId, fheight); - break; - case 0x91: - // font widths - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var font = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var fwidths = p2j.fonts.getFontWidths(font); - - me.sendByteArrayBinaryMessage(0x0A, msgId, fwidths); - break; - case 0x92: - // create font - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var font = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var nameLength = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - var name = me.readStringBinaryMessageByLength(message, offset, nameLength); - offset = offset + nameLength * 2; - - var size = message[offset]; - offset = offset + 1; - - var style = message[offset]; - offset = offset + 1; - - var defLength = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var b64font = ""; - if (defLength > 0) - { - var binFont = ''; - for (var i = 0; i < defLength; i++) - { - binFont += String.fromCharCode(message[offset]); - offset = offset + 1; - } - b64font = window.btoa(binFont); - } - - var fontId = p2j.fonts.createFont(font, name, size, style, b64font); - - me.sendInt32BinaryMessage(0x0B, msgId, fontId); - break; - case 0x93: - // derive font - - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var font = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - p2j.fonts.deriveFont(font); - - me.sendInt8BinaryMessage(0x0C, msgId, 1); - break; - case 0x94: - // set cursor style - var styleId = me.readInt32BinaryMessage(message, 1); - var wid = me.readInt32BinaryMessage(message, 5); - p2j.screen.setCursorStyle(styleId, wid); - break; - case 0x95: - // restack windows - var num = me.readInt32BinaryMessage(message, 1); - var winids = []; - for (var i = 0; i < num; i++) - { - winids.push(me.readInt32BinaryMessage(message, 5 + (i * 4))); - } - p2j.screen.restackZOrderEntries(winids); - break; - case 0x96: - // register/deregister widgets for mouse actions - - var offset = 1; - - // the number of windows with new widgets - var windowNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var j = 0; j < windowNo; j++) - { - // the window ID - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - if (!p2j.screen.isExistingWindowId(windowID)) - { - console.log("undefined window " + windowID); - continue; - } - - var theWindow = p2j.screen.getWindow(windowID); - - // the number of new widgets in this window - var widgetNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var k = 0; k < widgetNo; k++) - { - // the widget ID - var wid = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - // the coordinates - var x = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - var y = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - // the size - var width = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - var height = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - // bit-encoded mouse ops: each bit from 1 to 11, if set, represents a mouse - // operation as defined in p2j.screen.mouseOps - var actions = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - theWindow.deregisterMouseWidget(wid); - theWindow.registerMouseWidget(wid, x, y, width, height, actions); - } - - var allWNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - var zOrder = []; - - for (var k = 0; k < allWNo; k++) - { - zOrder[k] = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - } - - theWindow.setWidgetZOrder(zOrder); - } - - // the number of windows with dead widgets - windowNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var j = 0; j < windowNo; j++) - { - // the window ID - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var theWindow = p2j.screen.getWindow(windowID); - - if (theWindow == undefined) - { - console.log("undefined window" + windowID); - continue; - } - - // the number of dead widgets in this window - var widgetNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var k = 0; k < widgetNo; k++) - { - // the widget ID - var wid = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - theWindow.deregisterMouseWidget(wid); - } - } - - // the number of windows with new "any widgets" - windowNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var j = 0; j < windowNo; j++) - { - // the window ID - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - if (!p2j.screen.isExistingWindowId(windowID)) - { - console.log("undefined window " + windowID); - continue; - } - - var theWindow = p2j.screen.getWindow(windowID); - - // the number of new "any widgets" in this window - var widgetNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var k = 0; k < widgetNo; k++) - { - // the widget ID - var wid = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - theWindow.deregisterAnyMouseWidget(wid); - theWindow.registerAnyMouseWidget(wid); - } - } - - // the number of windows with dead "any widgets" - windowNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var j = 0; j < windowNo; j++) - { - // the window ID - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var theWindow = p2j.screen.getWindow(windowID); - - if (theWindow == undefined) - { - console.log("undefined window" + windowID); - continue; - } - - // the number of dead "any widgets" in this window - var widgetNo = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - for (var k = 0; k < widgetNo; k++) - { - // the widget ID - var wid = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - theWindow.deregisterAnyMouseWidget(wid); - } - } - - break; - case 0x97: - // enable/disable mouse events - p2j.screen.captureMouseEvents(message[1] == 1); - break; - case 0x98: - // enable/disable OS events - var wid = me.readInt32BinaryMessage(message, 1); - p2j.screen.enableOsEvents(wid, message[5] == 1); - break; - case 0x99: - // set window iconification state - var offset = 1; - - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var iconified = (message[offset] == 1); - offset = offset + 1; - - var theWindow = p2j.screen.getWindow(windowID); - //p2j.logger.log("recieved 0x99: iconified = " + iconified + " windowId=" + windowID); - if (iconified) - { - theWindow.iconify(); - } - else - { - theWindow.deiconify(); - } - - break; - case 0x9A: - // resizeable window - var offset = 1; - - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var theWindow = p2j.screen.getWindow(windowID); - - theWindow.resizeable = (message[offset] == 1); - offset = offset + 1; - - if (theWindow.resizeable) - { - theWindow.minWidth = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - theWindow.minHeight = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - - theWindow.maxWidth = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - theWindow.maxHeight = me.readInt16BinaryMessage(message, offset); - offset = offset + 2; - } - else - { - var rect = theWindow.canvas.getBoundingClientRect(); - - var rwidth = rect.right - rect.left + 1; - var rheight = rect.bottom - rect.top + 1; - - theWindow.minWidth = rwidth; - theWindow.minHeight = rheight; - - theWindow.maxWidth = rwidth; - theWindow.maxHeight = rheight; - } - - break; - case 0x9B: - // current editors selection is changed - var text = me.readStringBinaryMessage(message, 1); - p2j.clipboard.setSelection(text); - break; - case 0x9C: - var id = me.readInt32BinaryMessage(message, 1); - p2j.screen.moveToTop(id); - break; - case 0x9D: - var id = me.readInt32BinaryMessage(message, 1); - p2j.screen.moveToBottom(id); - break; - case 0x9E: - // change sensitivity for top-level or child window - var id = me.readInt32BinaryMessage(message, 1); - var enabled = message[5] === 0 ? false : true; - p2j.screen.setWindowEnabled(id, enabled); - break; - case 0x9F: - // font is installed - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var fontNameLength = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - var fontName = me.readStringBinaryMessageByLength(message, offset, fontNameLength); - - var result = p2j.fonts.isFontInstalled(fontName); - - me.sendInt8BinaryMessage(0x9F, msgId, result ? 1 : 0); - break; - case 0xA0: - // remove hashes - var offset = 1; - - var msgId = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - // window ID - var windowID = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - // number of hashes - var hashNo = me.readInt32BinaryMessage(message, offset); - offset = offset + 4; - - if (windowID == 0) - { - for (var i = 0; i < hashNo; i++) - { - // read each hash and remove it - var hashBytes = message.slice(offset, offset + 16); - var hash = me.createHexString(hashBytes); - delete p2j.screen.globalCache[hash]; - - offset += 16; - } - } - else - { - var theWindow = p2j.screen.getWindow(windowID); - - for (var i = 0; i < hashNo; i++) - { - // read each hash and remove it - var hashBytes = message.slice(offset, offset + 16); - var hash = me.createHexString(hashBytes); - delete theWindow.removeCachedDraw(hash); - - offset += 16; - } - } - - me.sendInt8BinaryMessage(0x12, msgId, 1); - break; - }; - var t2 = (new Date()).getTime(); - console.log(callNo + ": " + message[0].toString(16) + " done in " + (t2 - t1)); - }; - // on web socket message - ws.onmessage = function(evt) - { - var data = evt.data; - - if (data instanceof ArrayBuffer) - { - // binary messages - var message = new Uint8Array(data); - var isDrawing = message[0] === 0x81; - if (isDrawing) - { - p2j.displayWaitCursor(); - } - executor.executeTask({ execute : function() { messageHandler(message); } }, false); - if (isDrawing) - { - executor.executeTask({ execute : function() { p2j.restoreCursor(); } }, false); - } - } - else - { - // text messages - var pay = JSON.parse(data); - executor.executeTask( - { execute : function() - { - switch (pay.c) - { - case 0: - // color palette - p2j.screen.palette = pay.p; - break; - }; - } - }, false); - } - } - - // on web socket close - ws.onclose = function() - { - connected = false; - p2j.screen.error('Connection closed by remote host.'); - window.setInterval(function() { window.location.replace(referrer); }, 5000); - }; - - // refresh - window.onpagehide = function() - { - if (connected) - { - ws.close(); - } - }; - } - else - { - p2j.screen.error('WebSockets are NOT supported by your Browser.'); - } + maxIdleTime = cfg.socket.maxIdleTime; + + watchdogTimeout = cfg.socket.watchdogTimeout; + + socketUrl = cfg.socket.url; + + /** + * Sends ping and pong notifications to the server and starts the ping/pong watcher timer. + */ + function ping() + { + //send Ping/Pong message + me.sendNotification(0xFC); + // start ping/pong watcher + pingPongWatcher = setTimeout( + function() + { + // 5000 ms has been elapsed, but the server hasn't replied yet on the ping sent + // by the client. If ws is in OPEN state, then send new ping/pong + if (ws && ws.readyState == 1) + { + ping(); + } + }, 5000); + console.debug("Ping/Pong " + (new Date()).getTime()); + }; + + idleTimer = new ControlTimer(maxIdleTime, maxIdleTime / 3, ping); + + /** + * Connect to the websocket server via the given connection url. + */ + function connect() + { + // don't reconnect if the websocket state is CONNECTING(0) or OPEN(1) or CLOSING(2) + if (ws && ws.readyState !== 3) + { + console.debug("Connection in process " + (new Date()).getTime()); + return; + } + createWebSocket(socketUrl); + console.debug("Try to connect the server " + (new Date()).getTime()); + }; + + connectivityTimer = new ControlTimer(Math.max(watchdogTimeout, maxIdleTime), 5000, connect); + + // TODO: offline mode + window.addEventListener( + "offline", + function(e) + { + console.debug("offline"); + connectivityTimer.start(); + }, false); + + // TODO: refresh + window.onpagehide = function() + { + if (connected) + { + ws.close(); + } + }; + + createWebSocket(socketUrl); }; /**