UNPKG

jsroot

Version:
1,007 lines (838 loc) 34.9 kB
import { settings, httpRequest, createHttpRequest, loadScript, decodeUrl, browser, setBatchMode, isBatchMode, isObject, isFunc, isStr, btoa_func } from './core.mjs'; import { closeCurrentWindow, showProgress, loadOpenui5 } from './gui/utils.mjs'; import { sha256, sha256_2 } from './base/sha256.mjs'; // secret session key used for hashing connections keys // only if set, all messages from and to server signed with HMAC hash let sessionKey = ''; /** @summary HMAC implementation * @desc see https://en.wikipedia.org/wiki/HMAC for more details * @private */ function HMAC(key, m, o) { const kbis = sha256(sessionKey + key), block_size = 64, opad = 0x5c, ipad = 0x36, ko = [], ki = []; while (kbis.length < block_size) kbis.push(0); for (let i = 0; i < kbis.length; ++i) { const code = kbis[i]; ko.push(code ^ opad); ki.push(code ^ ipad); } const hash = sha256_2(ki, (o === undefined) ? m : new Uint8Array(m, o)); return sha256_2(ko, hash, true); } /** * @summary Class emulating web socket with long-poll http requests * * @private */ class LongPollSocket { constructor(addr, _raw, _handle, _counter) { this.path = addr; this.connid = null; this.req = null; this.raw = _raw; this.handle = _handle; this.counter = _counter; this.nextRequest('', 'connect'); } /** @summary Submit next request */ nextRequest(data, kind) { let url = this.path, reqmode = 'buf', post = null; if (kind === 'connect') { url += this.raw ? '?raw_connect' : '?txt_connect'; if (this.handle) url += '&' + this.handle.getConnArgs(this.counter++); this.connid = 'connect'; } else if (kind === 'close') { if ((this.connid === null) || (this.connid === 'close')) return; url += `?connection=${this.connid}&close`; if (this.handle) url += '&' + this.handle.getConnArgs(this.counter++); this.connid = 'close'; reqmode = 'text;sync'; // use sync mode to close connection before browser window closed } else if ((this.connid === null) || (typeof this.connid !== 'number')) { if (!browser.qt6) console.error('No connection'); } else { url += '?connection=' + this.connid; if (this.handle) url += '&' + this.handle.getConnArgs(this.counter++); if (kind === 'dummy') url += '&dummy'; } if (data) { if (this.raw) { // special workaround to avoid POST request, use base64 coding url += '&post=' + btoa_func(data); } else { // send data with post request - most efficient way reqmode = 'postbuf'; post = data; } } createHttpRequest(url, reqmode, function(res) { // this set to the request itself, res is response if (this.handle.req === this) this.handle.req = null; // get response for existing dummy request if (res === null) return this.handle.processRequest(null); if (this.handle.raw) { // raw mode - all kind of reply data packed into binary buffer // first 4 bytes header 'txt:' or 'bin:' // after the 'bin:' there is length of optional text argument like 'bin:14 :optional_text' // and immediately after text binary data. Server sends binary data so, that offset should be multiple of 8 const u8Arr = new Uint8Array(res); let str = '', i = 0, offset = u8Arr.length; if (offset < 4) { if (!browser.qt6) console.error(`longpoll got short message in raw mode ${offset}`); return this.handle.processRequest(null); } while (i < 4) str += String.fromCharCode(u8Arr[i++]); if (str !== 'txt:') { str = ''; while ((i < offset) && (String.fromCharCode(u8Arr[i]) !== ':')) str += String.fromCharCode(u8Arr[i++]); ++i; offset = i + parseInt(str.trim()); } str = ''; while (i < offset) str += String.fromCharCode(u8Arr[i++]); if (str) { if (str === '<<nope>>') this.handle.processRequest(-1111); else this.handle.processRequest(str); } if (offset < u8Arr.length) this.handle.processRequest(res, offset); } else if (this.getResponseHeader('Content-Type') === 'application/x-binary') { // binary reply with optional header const extra_hdr = this.getResponseHeader('LongpollHeader'); if (extra_hdr) this.handle.processRequest(extra_hdr); this.handle.processRequest(res, 0); } else { // text reply if (res && !isStr(res)) { let str = ''; const u8Arr = new Uint8Array(res); for (let i = 0; i < u8Arr.length; ++i) str += String.fromCharCode(u8Arr[i]); res = str; } if (res === '<<nope>>') this.handle.processRequest(-1111); else this.handle.processRequest(res); } }, function(/* err, status */) { this.handle.processRequest(null, 'error'); }, true).then(req => { req.handle = this; if (!this.req) this.req = req; // any request can be used for response, do not submit dummy until req is there req.send(post); }); } /** @summary Process request */ processRequest(res, _offset) { if (res === null) { if (isFunc(this.onerror)) this.onerror('receive data with connid ' + (this.connid || '---')); if ((_offset === 'error') && isFunc(this.onclose)) this.onclose('force_close'); this.connid = null; return; } else if (res === -1111) res = ''; let dummy_tmout = 5; if (this.connid === 'connect') { if (!res) { this.connid = null; if (isFunc(this.onerror)) this.onerror('connection rejected'); return; } this.connid = parseInt(res); dummy_tmout = 100; // when establishing connection, wait a bit longer to submit dummy package console.log(`Get new longpoll connection with id ${this.connid}`); if (isFunc(this.onopen)) this.onopen(); } else if (this.connid === 'close') { if (isFunc(this.onclose)) this.onclose(); return; } else if (isFunc(this.onmessage) && res) this.onmessage({ data: res, offset: _offset }); // minimal timeout to reduce load, generate dummy only if client not submit new request immediately if (!this.req) setTimeout(() => { if (!this.req) this.nextRequest('', 'dummy'); }, dummy_tmout); } /** @summary Send data */ send(str) { this.nextRequest(str); } /** @summary Close connection */ close() { this.nextRequest('', 'close'); } } // class LongPollSocket // ======================================================================================== /** * @summary Class re-playing socket data from stored protocol * * @private */ class FileDumpSocket { #wait_for_file; constructor(receiver) { this.receiver = receiver; this.protocol = []; this.cnt = 0; this.sendcnt = 0; httpRequest('protocol.json', 'text').then(res => this.getProtocol(res)); } /** @summary Get stored protocol */ getProtocol(res) { if (!res) return; this.protocol = JSON.parse(res); if (isFunc(this.onopen)) this.onopen(); this.nextOperation(); } /** @summary Emulate send - just count operation */ send(/* str */) { if (this.protocol[this.cnt] === 'send') { this.cnt++; setTimeout(() => this.nextOperation(), 10); } else this.sendcnt++; } /** @summary Emulate close */ close() {} /** @summary Read data for next operation */ nextOperation() { // when file request running - just ignore if (this.#wait_for_file) return; const fname = this.protocol[this.cnt]; if (!fname) return; if (fname === 'send') { if (this.sendcnt > 0) { this.sendcnt--; this.cnt++; this.nextOperation(); } return; } this.#wait_for_file = true; this.cnt++; httpRequest(fname, (fname.indexOf('.bin') > 0 ? 'buf' : 'text')).then(res => { this.#wait_for_file = false; if (!res) return; const p = fname.indexOf('_ch'), chid = (p > 0) ? Number.parseInt(fname.slice(p+3, fname.indexOf('.', p))) : 1; if (isFunc(this.receiver.provideData)) this.receiver.provideData(chid, res, 0); setTimeout(() => this.nextOperation(), 10); }); } } // class FileDumpSocket /** * @summary Client communication handle for RWebWindow. * * @desc Should be created with {@link connectWebWindow} function */ class WebWindowHandle { constructor(socket_kind, credits) { this.kind = socket_kind; this.state = 0; this.credits = Math.max(3, credits || 10); this.cansend = this.credits; this.ackn = this.credits; // this number will be send to server with first message this.send_seq = 1; // sequence counter of send messages this.recv_seq = 0; // sequence counter of received messages } /** @summary Returns arguments specified in the RWebWindow::SetUserArgs() method * @desc Can be any valid JSON expression. Undefined by default. * @param {string} [field] - if specified and user args is object, returns correspondent object member * @return user arguments object */ getUserArgs(field) { if (field && isStr(field)) return isObject(this.user_args) ? this.user_args[field] : undefined; return this.user_args; } /** @summary Set user args * @desc Normally set via RWebWindow::SetUserArgs() method */ setUserArgs(args) { this.user_args = args; } /** @summary Set callbacks receiver. * @param {object} obj - object with receiver functions * @param {function} obj.onWebsocketMsg - called when new data received from RWebWindow * @param {function} obj.onWebsocketOpened - called when connection established * @param {function} obj.onWebsocketClosed - called when connection closed * @param {function} obj.onWebsocketError - called when get error via the connection */ setReceiver(obj) { this.receiver = obj; } /** @summary Cleanup and close connection. */ cleanup() { delete this.receiver; this.close(true); } /** @summary Invoke method in the receiver. * @private */ invokeReceiver(brdcst, method, arg, arg2) { if (this.receiver && isFunc(this.receiver[method])) this.receiver[method](this, arg, arg2); if (brdcst && this.channels) { const ks = Object.keys(this.channels); for (let n = 0; n < ks.length; ++n) this.channels[ks[n]].invokeReceiver(false, method, arg, arg2); } } /** @summary Provide data for receiver. When no queue - do it directly. * @private */ provideData(chid, msg, len) { if (this.wait_first_recv) { // here dummy first recv like EMBED_DONE is handled delete this.wait_first_recv; this.state = 1; return this.invokeReceiver(false, 'onWebsocketOpened'); } if ((chid > 1) && this.channels) { const channel = this.channels[chid]; if (channel) return channel.provideData(1, msg, len); } const force_queue = len && (len < 0); if (!force_queue && (!this.msgqueue || !this.msgqueue.length)) return this.invokeReceiver(false, 'onWebsocketMsg', msg, len); if (!this.msgqueue) this.msgqueue = []; if (force_queue) len = undefined; this.msgqueue.push({ ready: true, msg, len }); } /** @summary Reserve entry in queue for data, which is not yet decoded. * @private */ reserveQueueItem() { if (!this.msgqueue) this.msgqueue = []; const item = { ready: false, msg: null, len: 0 }; this.msgqueue.push(item); return item; } /** @summary Provide data for item which was reserved before. * @private */ markQueueItemDone(item, _msg, _len) { item.ready = true; item.msg = _msg; item.len = _len; this.processQueue(); } /** @summary Process completed messages in the queue * @private */ processQueue() { if (this._loop_msgqueue || !this.msgqueue) return; this._loop_msgqueue = true; while ((this.msgqueue.length > 0) && this.msgqueue[0].ready) { const front = this.msgqueue.shift(); this.invokeReceiver(false, 'onWebsocketMsg', front.msg, front.len); } if (this.msgqueue.length === 0) delete this.msgqueue; delete this._loop_msgqueue; } /** @summary Close connection */ close(force) { if (this.master) { this.master.send(`CLOSECH=${this.channelid}`, 0); delete this.master.channels[this.channelid]; delete this.master; return; } if (this.timerid) { clearTimeout(this.timerid); delete this.timerid; } if (this._websocket && (this.state > 0)) { this.state = force ? -1 : 0; // -1 prevent socket from reopening this._websocket.onclose = null; // hide normal handler this._websocket.close(); delete this._websocket; } } /** @summary Checks number of credits for send operation * @param {number} [numsend = 1] - number of required send operations * @return true if one allow to send specified number of text message to server */ canSend(numsend) { return this.cansend >= (numsend || 1); } /** @summary Returns number of possible send operations relative to number of credits */ getRelCanSend() { return !this.credits ? 1 : this.cansend / this.credits; } /** @summary Send text message via the connection. * @param {string} msg - text message to send * @param {number} [chid] - channel id, 1 by default, 0 used only for internal communication */ send(msg, chid) { if (this.master) return this.master.send(msg, this.channelid); if (!this._websocket || (this.state <= 0)) return false; if (!Number.isInteger(chid)) chid = 1; // when not configured, channel 1 is used - main widget if (this.cansend === 0) console.error('No credits for send, increase "WebGui.ConnCredits" value on server'); const prefix = `${this.send_seq++}:${this.ackn}:${this.cansend}:${chid}:`, hash = this.key && sessionKey ? HMAC(this.key, `${prefix}${msg}`) : 'none'; this.ackn = 0; this.cansend--; // decrease number of allowed send packets this._websocket.send(`${hash}:${prefix}${msg}`); if ((this.kind === 'websocket') || (this.kind === 'longpoll')) { if (this.timerid) clearTimeout(this.timerid); this.timerid = setTimeout(() => this.keepAlive(), 10000); } return true; } /** @summary Send only last message of specified kind during defined time interval. * @desc Idea is to prevent sending multiple messages of similar kind and overload connection * Instead timeout is started after which only last specified message will be send * @private */ sendLast(kind, tmout, msg) { let d = this._delayed; if (!d) d = this._delayed = {}; d[kind] = msg; if (!d[`${kind}_handler`]) d[`${kind}_handler`] = setTimeout(() => { delete d[`${kind}_handler`]; this.send(d[kind]); }, tmout); } /** @summary Inject message(s) into input queue, for debug purposes only * @private */ inject(msg, chid, immediate) { // use timeout to avoid too deep call stack if (!immediate) return setTimeout(this.inject.bind(this, msg, chid, true), 0); if (chid === undefined) chid = 1; if (Array.isArray(msg)) { for (let k = 0; k < msg.length; ++k) this.provideData(chid, isStr(msg[k]) ? msg[k] : JSON.stringify(msg[k]), -1); this.processQueue(); } else if (msg) this.provideData(chid, isStr(msg) ? msg : JSON.stringify(msg)); } /** @summary Send keep-alive message. * @desc Only for internal use, only when used with web sockets * @private */ keepAlive() { delete this.timerid; this.send('KEEPALIVE', 0); } /** @summary Request server to resize window * @desc For local displays like CEF or qt5 only server can do this */ resizeWindow(w, h) { if (browser.qt6 || browser.cef3) this.send(`RESIZE=${w},${h}`, 0); else if ((typeof window !== 'undefined') && isFunc(window?.resizeTo)) window.resizeTo(w, h); } /** @summary Method open channel, which will share same connection, but can be used independently from main * If @param url is provided - creates fully independent instance and perform connection with it * @private */ createChannel(url) { if (url) return this.createNewInstance(url); if (this.master) return this.master.createChannel(); const channel = new WebWindowHandle('channel', this.credits); channel.wait_first_recv = true; // first received message via the channel is confirmation of established connection if (!this.channels) { this.channels = {}; this.freechannelid = 2; } channel.master = this; channel.channelid = this.freechannelid++; // register this.channels[channel.channelid] = channel; // now server-side entity should be initialized and init message send from server side! return channel; } /** @summary Standalone mode without server connection */ isStandalone() { return this.kind === 'file'; } /** @summary Returns true if socket connected */ isConnected() { return this.state > 0; } /** @summary Returns used channel ID, 1 by default */ getChannelId() { return this.channelid && this.master ? this.channelid : 1; } isChannel() { return this.getChannelId() > 1; } /** @summary Assign href parameter * @param {string} [path] - absolute path, when not specified window.location.url will be used * @private */ setHRef(path) { if (isStr(path) && (path.indexOf('?') > 0)) { this.href = path.slice(0, path.indexOf('?')); const d = decodeUrl(path); this.key = d.get('key'); this.token = d.get('token'); } else { this.href = path; delete this.key; delete this.token; } } /** @summary Return href part * @param {string} [relative_path] - relative path to the handle * @private */ getHRef(relative_path) { if (!relative_path || !this.kind || !this.href) return this.href; let addr = this.href; if (relative_path.indexOf('../') === 0) { const ddd = addr.lastIndexOf('/', addr.length - 2); addr = addr.slice(0, ddd) + relative_path.slice(2); } else addr += relative_path; return addr; } /** @summary provide connection args for the web socket * @private */ getConnArgs(ntry) { let args = ''; if (this.key) { const k = HMAC(this.key, `attempt_${ntry}`); args += `key=${k}&ntry=${ntry}`; } if (this.token) { if (args) args += '&'; args += `token=${this.token}`; } return args; } /** @summary Connect to the server * @param [href] - optional URL to widget, use document URL instead * @private */ connect(href) { // ignore connect if channel from master connection configured if (this.master && this.channelid) return; this.close(); if (href) { this._secondary = true; this.setHRef(href); } href = this.href; let ntry = 0; const retry_open = first_time => { if (this.state !== 0) return; if (!first_time) console.log(`try connect window again ${new Date().toString()}`); if (this._websocket) { this._websocket.close(); delete this._websocket; } if (!href) { href = window.location.href; if (href && href.indexOf('#') > 0) href = href.slice(0, href.indexOf('#')); if (href && href.lastIndexOf('/') > 0) href = href.slice(0, href.lastIndexOf('/') + 1); } this.href = href; ntry++; if (first_time) console.log(`Opening web socket at ${href}`); if (ntry > 2) showProgress(`Trying to connect ${href}`); let path = href; if (this.kind === 'file') { path += 'root.filedump'; this._websocket = new FileDumpSocket(this); console.log(`configure protocol log ${path}`); } else if ((this.kind === 'websocket') && first_time) { path = path.replace('http://', 'ws://').replace('https://', 'wss://') + 'root.websocket'; console.log(`configure websocket ${path}`); path += '?' + this.getConnArgs(ntry); this._websocket = new WebSocket(path); } else { path += 'root.longpoll'; console.log(`configure longpoll ${path}`); this._websocket = new LongPollSocket(path, (this.kind === 'rawlongpoll'), this, ntry); } if (!this._websocket) return; this._websocket.onopen = () => { if (ntry > 2) showProgress(); this.state = 1; const reply = (this._secondary ? '' : 'generate_key;') + (this.key || ''); this.send(`READY=${reply}`, 0); // need to confirm connection and request new key this.invokeReceiver(false, 'onWebsocketOpened'); }; this._websocket.onmessage = e => { let msg = e.data; if (this.next_binary) { const binchid = this.next_binary, server_hash = this.next_binary_hash; delete this.next_binary; delete this.next_binary_hash; if (msg instanceof Blob) { // convert Blob object to BufferArray const reader = new FileReader(), qitem = this.reserveQueueItem(); // The file's text will be printed here reader.onload = event => { let result = event.target.result; if (this.key && sessionKey) { const hash = HMAC(this.key, result, 0); if (hash !== server_hash) { console.log('Discard binary buffer because of HMAC mismatch'); result = new ArrayBuffer(0); } } this.markQueueItemDone(qitem, result, 0); }; reader.readAsArrayBuffer(msg, e.offset || 0); } else { // this is from CEF or LongPoll handler let result = msg; if (this.key && sessionKey) { const hash = HMAC(this.key, result, e.offset || 0); if (hash !== server_hash) { console.log('Discard binary buffer because of HMAC mismatch'); result = new ArrayBuffer(0); } } this.provideData(binchid, result, e.offset || 0); } return; } if (!isStr(msg)) return console.log(`unsupported message kind: ${typeof msg}`); const i0 = msg.indexOf(':'), server_hash = msg.slice(0, i0), i1 = msg.indexOf(':', i0 + 1), seq_id = Number.parseInt(msg.slice(i0 + 1, i1)), i2 = msg.indexOf(':', i1 + 1), credit = Number.parseInt(msg.slice(i1 + 1, i2)), i3 = msg.indexOf(':', i2 + 1), // cansend = parseInt(msg.slice(i2 + 1, i3)), // TODO: take into account when sending messages i4 = msg.indexOf(':', i3 + 1), chid = Number.parseInt(msg.slice(i3 + 1, i4)); // for authentication HMAC checksum and sequence id is important // HMAC used to authenticate server // sequence id is necessary to exclude submission of same packet again if (this.key && sessionKey) { const client_hash = HMAC(this.key, msg.slice(i0+1)); if (server_hash !== client_hash) return console.log(`Failure checking server HMAC sum ${server_hash}`); } if (seq_id <= this.recv_seq) return console.log(`Failure with packet sequence ${seq_id} <= ${this.recv_seq}`); this.recv_seq = seq_id; // sequence id of received packet this.ackn++; // count number of received packets, this.cansend += credit; // how many packets client can send msg = msg.slice(i4 + 1); if (chid === 0) { // console.log(`GET chid=0 message ${msg}`); if (msg === 'CLOSE') { this.close(true); // force closing of socket this.invokeReceiver(true, 'onWebsocketClosed'); } else if (msg.indexOf('NEW_KEY=') === 0) { this.new_key = msg.slice(8); console.log('get new key', this.new_key); this.storeKeyInUrl(); if (this._ask_reload) this.askReload(true); } } else if (msg.slice(0, 10) === '$$binary$$') { this.next_binary = chid; this.next_binary_hash = msg.slice(10); } else if (msg === '$$nullbinary$$') this.provideData(chid, new ArrayBuffer(0), 0); else this.provideData(chid, msg); if (this.ackn > Math.max(2, this.credits*0.7)) this.send('READY', 0); // send dummy message to server }; this._websocket.onclose = arg => { delete this._websocket; if ((this.state > 0) || (arg === 'force_close')) { console.log('websocket closed'); this.state = 0; this.invokeReceiver(true, 'onWebsocketClosed'); } }; this._websocket.onerror = err => { console.log(`websocket error ${err} state ${this.state}`); if (this.state > 0) { this.invokeReceiver(true, 'onWebsocketError', err); this.state = 0; } }; // only in interactive mode try to reconnect if (!isBatchMode()) setTimeout(retry_open, 3000); // after 3 seconds try again }; // retry_open retry_open(true); // call for the first time } /** @summary Ask to reload web widget * @desc If new key already exists - reload immediately * Otherwise request server to generate new key - and then reload page * WARNING - call only when knowing that you are doing * @private */ askReload(force) { if (this.new_key || force) { this.close(true); if (typeof location !== 'undefined') location.reload(true); } else { this._ask_reload = true; this.send('GENERATE_KEY', 0); } } /** @summary Instal Ctrl-R handler to reload web window * @desc Instead of default window reload invokes {@link askReload} method * WARNING - only call when you know that you are doing * @private */ addReloadKeyHandler() { if ((this.kind === 'file') || this._handling_reload) return; // this websocket will handle reload // embed widgets should not call this method this._handling_reload = true; window.addEventListener('keydown', evnt => { if (((evnt.key === 'R') || (evnt.key === 'r')) && evnt.ctrlKey) { evnt.stopPropagation(); evnt.preventDefault(); console.log('Prevent Ctrl-R propogation - ask reload RWebWindow!'); this.askReload(); } }); } /** @summary Replace widget URL with new key * @private */ storeKeyInUrl() { // do not modify document URLs by secondary widgets if (this._secondary) return; let href = (typeof document !== 'undefined') ? document.URL : null; if (this._can_modify_url && isStr(href) && (typeof window !== 'undefined')) { let prefix = '&key=', p = href.indexOf(prefix); if (p < 0) { prefix = '?key='; p = href.indexOf(prefix); } if ((p > 0) && this.new_key) { const p1 = href.indexOf('#', p+1), p2 = href.indexOf('&', p+1), pp = (p1 < 0) ? p2 : (p2 < 0 ? p1 : Math.min(p1, p2)); href = href.slice(0, p) + prefix + this.new_key + (pp < 0 ? '' : href.slice(pp)); window.history?.replaceState(window.history.state, undefined, href); } } if (typeof sessionStorage !== 'undefined') { sessionStorage.setItem('RWebWindow_SessionKey', sessionKey); sessionStorage.setItem('RWebWindow_Key', this.new_key); } } /** @summary Create new instance of same kind * @private */ createNewInstance(url) { const handle = new WebWindowHandle(this.kind); handle._secondary = true; handle.setHRef(this.getHRef(url)); return handle; } } // class WebWindowHandle /** @summary Method used to initialize connection to web window. * @param {object} arg - arguments * @param {string} [arg.socket_kind] - kind of connection longpoll|websocket, detected automatically from URL * @param {number} [arg.credits = 10] - number of packets which can be send to server without acknowledge * @param {object} arg.receiver - instance of receiver for websocket events, allows to initiate connection immediately * @param {string} [arg.first_recv] - required prefix in the first message from RWebWindow, remain part of message will be returned in handle.first_msg * @param {string} [arg.href] - URL to RWebWindow, using window.location.href by default * @return {Promise} {@link WebWindowHandle} instance */ async function connectWebWindow(arg) { // mark that jsroot used with RWebWindow browser.webwindow = true; if (isFunc(arg)) arg = { callback: arg }; else if (!isObject(arg)) arg = {}; let d_key, d_token, new_key; if (!arg.href) { let href = (typeof document !== 'undefined') ? document.URL : '', s_key; const p = href.indexOf('#'); if (p > 0) { s_key = href.slice(p + 1); href = href.slice(0, p); } const d = decodeUrl(href); d_key = d.get('key'); d_token = d.get('token'); if (d_key && s_key && (s_key.length > 20)) { sessionKey = s_key; if (typeof window !== 'undefined') window.history?.replaceState(window.history.state, undefined, href); } if (typeof sessionStorage !== 'undefined') { new_key = sessionStorage.getItem('RWebWindow_Key'); sessionStorage.removeItem('RWebWindow_Key'); if (sessionKey) sessionStorage.setItem('RWebWindow_SessionKey', sessionKey); else sessionKey = sessionStorage.getItem('RWebWindow_SessionKey') || ''; } // special holder script, prevents headless chrome browser from too early exit if (d.has('headless') && d_key && (browser.isChromeHeadless || browser.isChrome) && !arg.ignore_chrome_batch_holder) loadScript('root_batch_holder.js?key=' + (new_key || d_key)); if (!arg.platform) arg.platform = d.get('platform'); if (arg.platform === 'qt6') browser.qt6 = true; else if (arg.platform === 'cef3') browser.cef3 = true; if (arg.batch === undefined) arg.batch = d.has('headless'); if (arg.batch) setBatchMode(true); if (!arg.socket_kind) arg.socket_kind = d.get('ws'); if (!new_key && arg.winW && arg.winH && !isBatchMode() && isFunc(window?.resizeTo)) window.resizeTo(arg.winW, arg.winH); if (!new_key && arg.winX && arg.winY && !isBatchMode() && isFunc(window?.moveTo)) window.moveTo(arg.winX, arg.winY); } if (!arg.socket_kind) { if (browser.qt6) arg.socket_kind = 'rawlongpoll'; else if (browser.cef3) arg.socket_kind = 'longpoll'; else arg.socket_kind = 'websocket'; } if (arg.settings) Object.assign(settings, arg.settings); // only for debug purposes // arg.socket_kind = 'longpoll'; const main = new Promise(resolveFunc => { const handle = new WebWindowHandle(arg.socket_kind, arg.credits); handle.setUserArgs(arg.user_args); handle._can_modify_url = Boolean(d_key); // if key appears in URL, we can put there new key if (arg.href) handle.setHRef(arg.href); // apply href now while connect can be called from other place else { handle.key = new_key || d_key; handle.token = d_token; } if (typeof window !== 'undefined') { window.onbeforeunload = () => handle.close(true); if (browser.qt6) window.onqt5unload = window.onbeforeunload; } if (arg.receiver) { // when receiver exists, it handles itself callbacks handle.setReceiver(arg.receiver); handle.connect(); resolveFunc(handle); return; } if (!arg.first_recv) { resolveFunc(handle); return; } handle.setReceiver({ onWebsocketOpened() {}, // dummy function when websocket connected onWebsocketMsg(h, msg) { if (msg.indexOf(arg.first_recv) !== 0) return h.close(); h.first_msg = msg.slice(arg.first_recv.length); resolveFunc(h); }, onWebsocketClosed() { closeCurrentWindow(); } // when connection closed, close panel as well }); handle.connect(); }); if (!arg.ui5) return main; return Promise.all([main, loadOpenui5(arg)]).then(arr => arr[0]); } export { WebWindowHandle, connectWebWindow };