qwebchannel.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /****************************************************************************
  2. **
  3. ** Copyright (C) 2016 The Qt Company Ltd.
  4. ** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
  5. ** Contact: https://www.qt.io/licensing/
  6. **
  7. ** This file is part of the QtWebChannel module of the Qt Toolkit.
  8. **
  9. ** $QT_BEGIN_LICENSE:LGPL$
  10. ** Commercial License Usage
  11. ** Licensees holding valid commercial Qt licenses may use this file in
  12. ** accordance with the commercial license agreement provided with the
  13. ** Software or, alternatively, in accordance with the terms contained in
  14. ** a written agreement between you and The Qt Company. For licensing terms
  15. ** and conditions see https://www.qt.io/terms-conditions. For further
  16. ** information use the contact form at https://www.qt.io/contact-us.
  17. **
  18. ** GNU Lesser General Public License Usage
  19. ** Alternatively, this file may be used under the terms of the GNU Lesser
  20. ** General Public License version 3 as published by the Free Software
  21. ** Foundation and appearing in the file LICENSE.LGPL3 included in the
  22. ** packaging of this file. Please review the following information to
  23. ** ensure the GNU Lesser General Public License version 3 requirements
  24. ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
  25. **
  26. ** GNU General Public License Usage
  27. ** Alternatively, this file may be used under the terms of the GNU
  28. ** General Public License version 2.0 or (at your option) the GNU General
  29. ** Public license version 3 or any later version approved by the KDE Free
  30. ** Qt Foundation. The licenses are as published by the Free Software
  31. ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
  32. ** included in the packaging of this file. Please review the following
  33. ** information to ensure the GNU General Public License requirements will
  34. ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
  35. ** https://www.gnu.org/licenses/gpl-3.0.html.
  36. **
  37. ** $QT_END_LICENSE$
  38. **
  39. ****************************************************************************/
  40. "use strict";
  41. var QWebChannelMessageTypes = {
  42. signal: 1,
  43. propertyUpdate: 2,
  44. init: 3,
  45. idle: 4,
  46. debug: 5,
  47. invokeMethod: 6,
  48. connectToSignal: 7,
  49. disconnectFromSignal: 8,
  50. setProperty: 9,
  51. response: 10,
  52. };
  53. var QWebChannel = function(transport, initCallback, logger)
  54. {
  55. if (typeof transport !== "object" || typeof transport.send !== "function") {
  56. console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
  57. " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
  58. return;
  59. }
  60. var channel = this;
  61. this.transport = transport;
  62. this.logger = logger;
  63. this.send = function(data)
  64. {
  65. if (typeof(data) !== "string") {
  66. data = JSON.stringify(data);
  67. }
  68. channel.transport.send(data);
  69. }
  70. this.transport.onmessage = function(message)
  71. {
  72. var data = message.data;
  73. if (typeof data === "string") {
  74. data = JSON.parse(data);
  75. }
  76. switch (data.type) {
  77. case QWebChannelMessageTypes.signal:
  78. channel.handleSignal(data);
  79. break;
  80. case QWebChannelMessageTypes.response:
  81. channel.handleResponse(data);
  82. break;
  83. case QWebChannelMessageTypes.propertyUpdate:
  84. channel.handlePropertyUpdate(data);
  85. break;
  86. default:
  87. console.error("invalid message received:", message.data);
  88. break;
  89. }
  90. }
  91. this.execCallbacks = {};
  92. this.execId = 0;
  93. this.exec = function(data, callback)
  94. {
  95. if (!callback) {
  96. // if no callback is given, send directly
  97. channel.send(data);
  98. return;
  99. }
  100. if (channel.execId === Number.MAX_VALUE) {
  101. // wrap
  102. channel.execId = Number.MIN_VALUE;
  103. }
  104. if (data.hasOwnProperty("id")) {
  105. console.error("Cannot exec message with property id: " + JSON.stringify(data));
  106. return;
  107. }
  108. data.id = channel.execId++;
  109. channel.execCallbacks[data.id] = callback;
  110. if (this.logger && this.inited)
  111. {
  112. if (data.type == QWebChannelMessageTypes.invokeMethod)
  113. {
  114. var uri = this.objects[data.object].objectName + "." + data.method;
  115. this.logger.onRequest(data.id, uri, data.args);
  116. }
  117. //TODO: else ?
  118. }
  119. channel.send(data);
  120. };
  121. this.objects = {};
  122. this.handleSignal = function(message)
  123. {
  124. var object = channel.objects[message.object];
  125. if (object) {
  126. object.signalEmitted(message.signal, message.args);
  127. } else {
  128. console.warn("Unhandled signal: " + message.object + "::" + message.signal);
  129. }
  130. }
  131. this.handleResponse = function(message)
  132. {
  133. if (!message.hasOwnProperty("id")) {
  134. console.error("Invalid response message received: ", JSON.stringify(message));
  135. return;
  136. }
  137. if (this.logger && this.inited) {
  138. this.logger.onResponse(message.id, message.data);
  139. }
  140. channel.execCallbacks[message.id](message.data);
  141. delete channel.execCallbacks[message.id];
  142. }
  143. this.handlePropertyUpdate = function(message)
  144. {
  145. message.data.forEach(data => {
  146. var object = channel.objects[data.object];
  147. if (object) {
  148. object.propertyUpdate(data.signals, data.properties);
  149. } else {
  150. console.warn("Unhandled property update: " + data.object + "::" + data.signal);
  151. }
  152. });
  153. channel.exec({type: QWebChannelMessageTypes.idle});
  154. }
  155. this.debug = function(message)
  156. {
  157. channel.send({type: QWebChannelMessageTypes.debug, data: message});
  158. };
  159. channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
  160. for (const objectName of Object.keys(data)) {
  161. new QObject(objectName, data[objectName], channel);
  162. }
  163. // now unwrap properties, which might reference other registered objects
  164. for (const objectName of Object.keys(channel.objects)) {
  165. channel.objects[objectName].unwrapProperties();
  166. }
  167. if (initCallback) {
  168. channel.inited = true;
  169. initCallback(channel);
  170. }
  171. channel.exec({type: QWebChannelMessageTypes.idle});
  172. });
  173. };
  174. function QObject(name, data, webChannel)
  175. {
  176. this.__id__ = name;
  177. webChannel.objects[name] = this;
  178. // List of callbacks that get invoked upon signal emission
  179. this.__objectSignals__ = {};
  180. // Cache of all properties, updated when a notify signal is emitted
  181. this.__propertyCache__ = {};
  182. var object = this;
  183. // ----------------------------------------------------------------------
  184. this.unwrapQObject = function(response)
  185. {
  186. if (response instanceof Array) {
  187. // support list of objects
  188. return response.map(qobj => object.unwrapQObject(qobj))
  189. }
  190. if (!(response instanceof Object))
  191. return response;
  192. if (!response["__QObject*__"] || response.id === undefined) {
  193. var jObj = {};
  194. for (const propName of Object.keys(response)) {
  195. jObj[propName] = object.unwrapQObject(response[propName]);
  196. }
  197. return jObj;
  198. }
  199. var objectId = response.id;
  200. if (webChannel.objects[objectId])
  201. return webChannel.objects[objectId];
  202. if (!response.data) {
  203. console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
  204. return;
  205. }
  206. var qObject = new QObject( objectId, response.data, webChannel );
  207. qObject.destroyed.connect(function() {
  208. if (webChannel.objects[objectId] === qObject) {
  209. delete webChannel.objects[objectId];
  210. // reset the now deleted QObject to an empty {} object
  211. // just assigning {} though would not have the desired effect, but the
  212. // below also ensures all external references will see the empty map
  213. // NOTE: this detour is necessary to workaround QTBUG-40021
  214. Object.keys(qObject).forEach(name => delete qObject[name]);
  215. }
  216. });
  217. // here we are already initialized, and thus must directly unwrap the properties
  218. qObject.unwrapProperties();
  219. return qObject;
  220. }
  221. this.unwrapProperties = function()
  222. {
  223. for (const propertyIdx of Object.keys(object.__propertyCache__)) {
  224. object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
  225. }
  226. }
  227. function addSignal(signalData, isPropertyNotifySignal)
  228. {
  229. var signalName = signalData[0];
  230. var signalIndex = signalData[1];
  231. object[signalName] = {
  232. connect: function(callback) {
  233. if (typeof(callback) !== "function") {
  234. console.error("Bad callback given to connect to signal " + signalName);
  235. return;
  236. }
  237. object.__signaleNames__ = object.__signaleNames__ || {};
  238. object.__signaleNames__[signalIndex] = signalName;
  239. object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
  240. object.__objectSignals__[signalIndex].push(callback);
  241. if (webChannel.logger && webChannel.inited) {
  242. var fullName = webChannel.objects[object.__id__].objectName + "." + signalName;
  243. webChannel.logger.onConnectSignal(object.__objectSignals__[signalIndex].length - 1, fullName, callback);
  244. }
  245. // only required for "pure" signals, handled separately for properties in propertyUpdate
  246. if (isPropertyNotifySignal)
  247. return;
  248. // also note that we always get notified about the destroyed signal
  249. if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)")
  250. return;
  251. // and otherwise we only need to be connected only once
  252. if (object.__objectSignals__[signalIndex].length == 1) {
  253. webChannel.exec({
  254. type: QWebChannelMessageTypes.connectToSignal,
  255. object: object.__id__,
  256. signal: signalIndex
  257. });
  258. }
  259. },
  260. disconnect: function(callback) {
  261. if (typeof(callback) !== "function") {
  262. console.error("Bad callback given to disconnect from signal " + signalName);
  263. return;
  264. }
  265. object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
  266. var idx = object.__objectSignals__[signalIndex].indexOf(callback);
  267. if (idx === -1) {
  268. console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
  269. return;
  270. }
  271. if (webChannel.logger && webChannel.inited) {
  272. var fullName = webChannel.objects[object.__id__].objectName + "." + signalName;
  273. webChannel.logger.onDisconnectSignal(idx, fullName, callback);
  274. }
  275. object.__objectSignals__[signalIndex].splice(idx, 1);
  276. if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
  277. // only required for "pure" signals, handled separately for properties in propertyUpdate
  278. webChannel.exec({
  279. type: QWebChannelMessageTypes.disconnectFromSignal,
  280. object: object.__id__,
  281. signal: signalIndex
  282. });
  283. }
  284. }
  285. };
  286. }
  287. /**
  288. * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
  289. */
  290. function invokeSignalCallbacks(signalName, signalArgs)
  291. {
  292. var connections = object.__objectSignals__[signalName];
  293. if (connections) {
  294. connections.forEach(function(callback) {
  295. callback.apply(callback, signalArgs);
  296. });
  297. }
  298. }
  299. this.propertyUpdate = function(signals, propertyMap)
  300. {
  301. // update property cache
  302. for (const propertyIndex of Object.keys(propertyMap)) {
  303. var propertyValue = propertyMap[propertyIndex];
  304. object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue);
  305. }
  306. for (const signalName of Object.keys(signals)) {
  307. // Invoke all callbacks, as signalEmitted() does not. This ensures the
  308. // property cache is updated before the callbacks are invoked.
  309. invokeSignalCallbacks(signalName, signals[signalName]);
  310. }
  311. }
  312. this.signalEmitted = function(signalName, signalArgs)
  313. {
  314. if (webChannel.logger && webChannel.inited) {
  315. var fullName = webChannel.objects[object.__id__].objectName + "." + object.__signaleNames__[signalName];
  316. webChannel.logger.onSignal(fullName, signalArgs);
  317. }
  318. invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
  319. }
  320. function addMethod(methodData)
  321. {
  322. var methodName = methodData[0];
  323. var methodIdx = methodData[1];
  324. // Fully specified methods are invoked by id, others by name for host-side overload resolution
  325. var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName
  326. object[methodName] = function() {
  327. var args = [];
  328. var callback;
  329. var errCallback;
  330. for (var i = 0; i < arguments.length; ++i) {
  331. var argument = arguments[i];
  332. if (typeof argument === "function")
  333. callback = argument;
  334. else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
  335. args.push({
  336. "id": argument.__id__
  337. });
  338. else
  339. args.push(argument);
  340. }
  341. var result;
  342. // during test, webChannel.exec synchronously calls the callback
  343. // therefore, the promise must be constucted before calling
  344. // webChannel.exec to ensure the callback is set up
  345. if (!callback && (typeof(Promise) === 'function')) {
  346. result = new Promise(function(resolve, reject) {
  347. callback = resolve;
  348. errCallback = reject;
  349. });
  350. }
  351. webChannel.exec({
  352. "type": QWebChannelMessageTypes.invokeMethod,
  353. "object": object.__id__,
  354. "method": invokedMethod,
  355. "args": args
  356. }, function(response) {
  357. if (response !== undefined) {
  358. var result = object.unwrapQObject(response);
  359. if (callback) {
  360. (callback)(result);
  361. }
  362. } else if (errCallback) {
  363. (errCallback)();
  364. }
  365. });
  366. return result;
  367. };
  368. }
  369. function bindGetterSetter(propertyInfo)
  370. {
  371. var propertyIndex = propertyInfo[0];
  372. var propertyName = propertyInfo[1];
  373. var notifySignalData = propertyInfo[2];
  374. // initialize property cache with current value
  375. // NOTE: if this is an object, it is not directly unwrapped as it might
  376. // reference other QObject that we do not know yet
  377. object.__propertyCache__[propertyIndex] = propertyInfo[3];
  378. if (notifySignalData) {
  379. if (notifySignalData[0] === 1) {
  380. // signal name is optimized away, reconstruct the actual name
  381. notifySignalData[0] = propertyName + "Changed";
  382. }
  383. addSignal(notifySignalData, true);
  384. }
  385. Object.defineProperty(object, propertyName, {
  386. configurable: true,
  387. get: function () {
  388. var propertyValue = object.__propertyCache__[propertyIndex];
  389. if (propertyValue === undefined) {
  390. // This shouldn't happen
  391. console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__);
  392. }
  393. return propertyValue;
  394. },
  395. set: function(value) {
  396. if (value === undefined) {
  397. console.warn("Property setter for " + propertyName + " called with undefined value!");
  398. return;
  399. }
  400. object.__propertyCache__[propertyIndex] = value;
  401. var valueToSend = value;
  402. if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
  403. valueToSend = { "id": valueToSend.__id__ };
  404. webChannel.exec({
  405. "type": QWebChannelMessageTypes.setProperty,
  406. "object": object.__id__,
  407. "property": propertyIndex,
  408. "value": valueToSend
  409. });
  410. }
  411. });
  412. }
  413. // ----------------------------------------------------------------------
  414. data.methods.forEach(addMethod);
  415. data.properties.forEach(bindGetterSetter);
  416. data.signals.forEach(function(signal) { addSignal(signal, false); });
  417. Object.assign(object, data.enums);
  418. }
  419. //required for use with nodejs
  420. if (typeof module === 'object') {
  421. module.exports = {
  422. QWebChannel: QWebChannel
  423. };
  424. }