api-ipc-spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import { EventEmitter } from 'events';
  2. import { expect } from 'chai';
  3. import { BrowserWindow, ipcMain, IpcMainInvokeEvent, MessageChannelMain, WebContents } from 'electron/main';
  4. import { closeAllWindows } from './window-helpers';
  5. import { emittedOnce } from './events-helpers';
  6. const v8Util = process._linkedBinding('electron_common_v8_util');
  7. describe('ipc module', () => {
  8. describe('invoke', () => {
  9. let w = (null as unknown as BrowserWindow);
  10. before(async () => {
  11. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  12. await w.loadURL('about:blank');
  13. });
  14. after(async () => {
  15. w.destroy();
  16. });
  17. async function rendererInvoke (...args: any[]) {
  18. const { ipcRenderer } = require('electron');
  19. try {
  20. const result = await ipcRenderer.invoke('test', ...args);
  21. ipcRenderer.send('result', { result });
  22. } catch (e) {
  23. ipcRenderer.send('result', { error: e.message });
  24. }
  25. }
  26. it('receives a response from a synchronous handler', async () => {
  27. ipcMain.handleOnce('test', (e: IpcMainInvokeEvent, arg: number) => {
  28. expect(arg).to.equal(123);
  29. return 3;
  30. });
  31. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  32. expect(arg).to.deep.equal({ result: 3 });
  33. resolve();
  34. }));
  35. await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`);
  36. await done;
  37. });
  38. it('receives a response from an asynchronous handler', async () => {
  39. ipcMain.handleOnce('test', async (e: IpcMainInvokeEvent, arg: number) => {
  40. expect(arg).to.equal(123);
  41. await new Promise(setImmediate);
  42. return 3;
  43. });
  44. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  45. expect(arg).to.deep.equal({ result: 3 });
  46. resolve();
  47. }));
  48. await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`);
  49. await done;
  50. });
  51. it('receives an error from a synchronous handler', async () => {
  52. ipcMain.handleOnce('test', () => {
  53. throw new Error('some error');
  54. });
  55. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  56. expect(arg.error).to.match(/some error/);
  57. resolve();
  58. }));
  59. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  60. await done;
  61. });
  62. it('receives an error from an asynchronous handler', async () => {
  63. ipcMain.handleOnce('test', async () => {
  64. await new Promise(setImmediate);
  65. throw new Error('some error');
  66. });
  67. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  68. expect(arg.error).to.match(/some error/);
  69. resolve();
  70. }));
  71. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  72. await done;
  73. });
  74. it('throws an error if no handler is registered', async () => {
  75. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  76. expect(arg.error).to.match(/No handler registered/);
  77. resolve();
  78. }));
  79. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  80. await done;
  81. });
  82. it('throws an error when invoking a handler that was removed', async () => {
  83. ipcMain.handle('test', () => {});
  84. ipcMain.removeHandler('test');
  85. const done = new Promise<void>(resolve => ipcMain.once('result', (e, arg) => {
  86. expect(arg.error).to.match(/No handler registered/);
  87. resolve();
  88. }));
  89. await w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  90. await done;
  91. });
  92. it('forbids multiple handlers', async () => {
  93. ipcMain.handle('test', () => {});
  94. try {
  95. expect(() => { ipcMain.handle('test', () => {}); }).to.throw(/second handler/);
  96. } finally {
  97. ipcMain.removeHandler('test');
  98. }
  99. });
  100. it('throws an error in the renderer if the reply callback is dropped', async () => {
  101. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  102. ipcMain.handleOnce('test', () => new Promise(resolve => {
  103. setTimeout(() => v8Util.requestGarbageCollectionForTesting());
  104. /* never resolve */
  105. }));
  106. w.webContents.executeJavaScript(`(${rendererInvoke})()`);
  107. const [, { error }] = await emittedOnce(ipcMain, 'result');
  108. expect(error).to.match(/reply was never sent/);
  109. });
  110. });
  111. describe('ordering', () => {
  112. let w = (null as unknown as BrowserWindow);
  113. before(async () => {
  114. w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  115. await w.loadURL('about:blank');
  116. });
  117. after(async () => {
  118. w.destroy();
  119. });
  120. it('between send and sendSync is consistent', async () => {
  121. const received: number[] = [];
  122. ipcMain.on('test-async', (e, i) => { received.push(i); });
  123. ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null; });
  124. const done = new Promise<void>(resolve => ipcMain.once('done', () => { resolve(); }));
  125. function rendererStressTest () {
  126. const { ipcRenderer } = require('electron');
  127. for (let i = 0; i < 1000; i++) {
  128. switch ((Math.random() * 2) | 0) {
  129. case 0:
  130. ipcRenderer.send('test-async', i);
  131. break;
  132. case 1:
  133. ipcRenderer.sendSync('test-sync', i);
  134. break;
  135. }
  136. }
  137. ipcRenderer.send('done');
  138. }
  139. try {
  140. w.webContents.executeJavaScript(`(${rendererStressTest})()`);
  141. await done;
  142. } finally {
  143. ipcMain.removeAllListeners('test-async');
  144. ipcMain.removeAllListeners('test-sync');
  145. }
  146. expect(received).to.have.lengthOf(1000);
  147. expect(received).to.deep.equal([...received].sort((a, b) => a - b));
  148. });
  149. it('between send, sendSync, and invoke is consistent', async () => {
  150. const received: number[] = [];
  151. ipcMain.handle('test-invoke', (e, i) => { received.push(i); });
  152. ipcMain.on('test-async', (e, i) => { received.push(i); });
  153. ipcMain.on('test-sync', (e, i) => { received.push(i); e.returnValue = null; });
  154. const done = new Promise<void>(resolve => ipcMain.once('done', () => { resolve(); }));
  155. function rendererStressTest () {
  156. const { ipcRenderer } = require('electron');
  157. for (let i = 0; i < 1000; i++) {
  158. switch ((Math.random() * 3) | 0) {
  159. case 0:
  160. ipcRenderer.send('test-async', i);
  161. break;
  162. case 1:
  163. ipcRenderer.sendSync('test-sync', i);
  164. break;
  165. case 2:
  166. ipcRenderer.invoke('test-invoke', i);
  167. break;
  168. }
  169. }
  170. ipcRenderer.send('done');
  171. }
  172. try {
  173. w.webContents.executeJavaScript(`(${rendererStressTest})()`);
  174. await done;
  175. } finally {
  176. ipcMain.removeHandler('test-invoke');
  177. ipcMain.removeAllListeners('test-async');
  178. ipcMain.removeAllListeners('test-sync');
  179. }
  180. expect(received).to.have.lengthOf(1000);
  181. expect(received).to.deep.equal([...received].sort((a, b) => a - b));
  182. });
  183. });
  184. describe('MessagePort', () => {
  185. afterEach(closeAllWindows);
  186. it('can send a port to the main process', async () => {
  187. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  188. w.loadURL('about:blank');
  189. const p = emittedOnce(ipcMain, 'port');
  190. await w.webContents.executeJavaScript(`(${function () {
  191. const channel = new MessageChannel();
  192. require('electron').ipcRenderer.postMessage('port', 'hi', [channel.port1]);
  193. }})()`);
  194. const [ev, msg] = await p;
  195. expect(msg).to.equal('hi');
  196. expect(ev.ports).to.have.length(1);
  197. expect(ev.senderFrame.parent).to.be.null();
  198. expect(ev.senderFrame.routingId).to.equal(w.webContents.mainFrame.routingId);
  199. const [port] = ev.ports;
  200. expect(port).to.be.an.instanceOf(EventEmitter);
  201. });
  202. it('can sent a message without a transfer', async () => {
  203. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  204. w.loadURL('about:blank');
  205. const p = emittedOnce(ipcMain, 'port');
  206. await w.webContents.executeJavaScript(`(${function () {
  207. require('electron').ipcRenderer.postMessage('port', 'hi');
  208. }})()`);
  209. const [ev, msg] = await p;
  210. expect(msg).to.equal('hi');
  211. expect(ev.ports).to.deep.equal([]);
  212. expect(ev.senderFrame.routingId).to.equal(w.webContents.mainFrame.routingId);
  213. });
  214. it('can communicate between main and renderer', async () => {
  215. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  216. w.loadURL('about:blank');
  217. const p = emittedOnce(ipcMain, 'port');
  218. await w.webContents.executeJavaScript(`(${function () {
  219. const channel = new MessageChannel();
  220. (channel.port2 as any).onmessage = (ev: any) => {
  221. channel.port2.postMessage(ev.data * 2);
  222. };
  223. require('electron').ipcRenderer.postMessage('port', '', [channel.port1]);
  224. }})()`);
  225. const [ev] = await p;
  226. expect(ev.ports).to.have.length(1);
  227. expect(ev.senderFrame.routingId).to.equal(w.webContents.mainFrame.routingId);
  228. const [port] = ev.ports;
  229. port.start();
  230. port.postMessage(42);
  231. const [ev2] = await emittedOnce(port, 'message');
  232. expect(ev2.data).to.equal(84);
  233. });
  234. it('can receive a port from a renderer over a MessagePort connection', async () => {
  235. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  236. w.loadURL('about:blank');
  237. function fn () {
  238. const channel1 = new MessageChannel();
  239. const channel2 = new MessageChannel();
  240. channel1.port2.postMessage('', [channel2.port1]);
  241. channel2.port2.postMessage('matryoshka');
  242. require('electron').ipcRenderer.postMessage('port', '', [channel1.port1]);
  243. }
  244. w.webContents.executeJavaScript(`(${fn})()`);
  245. const [{ ports: [port1] }] = await emittedOnce(ipcMain, 'port');
  246. port1.start();
  247. const [{ ports: [port2] }] = await emittedOnce(port1, 'message');
  248. port2.start();
  249. const [{ data }] = await emittedOnce(port2, 'message');
  250. expect(data).to.equal('matryoshka');
  251. });
  252. it('can forward a port from one renderer to another renderer', async () => {
  253. const w1 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  254. const w2 = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  255. w1.loadURL('about:blank');
  256. w2.loadURL('about:blank');
  257. w1.webContents.executeJavaScript(`(${function () {
  258. const channel = new MessageChannel();
  259. (channel.port2 as any).onmessage = (ev: any) => {
  260. require('electron').ipcRenderer.send('message received', ev.data);
  261. };
  262. require('electron').ipcRenderer.postMessage('port', '', [channel.port1]);
  263. }})()`);
  264. const [{ ports: [port] }] = await emittedOnce(ipcMain, 'port');
  265. await w2.webContents.executeJavaScript(`(${function () {
  266. require('electron').ipcRenderer.on('port', ({ ports: [port] }: any) => {
  267. port.postMessage('a message');
  268. });
  269. }})()`);
  270. w2.webContents.postMessage('port', '', [port]);
  271. const [, data] = await emittedOnce(ipcMain, 'message received');
  272. expect(data).to.equal('a message');
  273. });
  274. describe('close event', () => {
  275. describe('in renderer', () => {
  276. it('is emitted when the main process closes its end of the port', async () => {
  277. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  278. w.loadURL('about:blank');
  279. await w.webContents.executeJavaScript(`(${function () {
  280. const { ipcRenderer } = require('electron');
  281. ipcRenderer.on('port', e => {
  282. const [port] = e.ports;
  283. port.start();
  284. (port as any).onclose = () => {
  285. ipcRenderer.send('closed');
  286. };
  287. });
  288. }})()`);
  289. const { port1, port2 } = new MessageChannelMain();
  290. w.webContents.postMessage('port', null, [port2]);
  291. port1.close();
  292. await emittedOnce(ipcMain, 'closed');
  293. });
  294. it('is emitted when the other end of a port is garbage-collected', async () => {
  295. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  296. w.loadURL('about:blank');
  297. await w.webContents.executeJavaScript(`(${async function () {
  298. const { port2 } = new MessageChannel();
  299. await new Promise(resolve => {
  300. port2.start();
  301. (port2 as any).onclose = resolve;
  302. process._linkedBinding('electron_common_v8_util').requestGarbageCollectionForTesting();
  303. });
  304. }})()`);
  305. });
  306. it('is emitted when the other end of a port is sent to nowhere', async () => {
  307. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  308. w.loadURL('about:blank');
  309. ipcMain.once('do-a-gc', () => v8Util.requestGarbageCollectionForTesting());
  310. await w.webContents.executeJavaScript(`(${async function () {
  311. const { port1, port2 } = new MessageChannel();
  312. await new Promise(resolve => {
  313. port2.start();
  314. (port2 as any).onclose = resolve;
  315. require('electron').ipcRenderer.postMessage('nobody-listening', null, [port1]);
  316. require('electron').ipcRenderer.send('do-a-gc');
  317. });
  318. }})()`);
  319. });
  320. });
  321. });
  322. describe('MessageChannelMain', () => {
  323. it('can be created', () => {
  324. const { port1, port2 } = new MessageChannelMain();
  325. expect(port1).not.to.be.null();
  326. expect(port2).not.to.be.null();
  327. });
  328. it('can send messages within the process', async () => {
  329. const { port1, port2 } = new MessageChannelMain();
  330. port2.postMessage('hello');
  331. port1.start();
  332. const [ev] = await emittedOnce(port1, 'message');
  333. expect(ev.data).to.equal('hello');
  334. });
  335. it('can pass one end to a WebContents', async () => {
  336. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  337. w.loadURL('about:blank');
  338. await w.webContents.executeJavaScript(`(${function () {
  339. const { ipcRenderer } = require('electron');
  340. ipcRenderer.on('port', ev => {
  341. const [port] = ev.ports;
  342. port.onmessage = () => {
  343. ipcRenderer.send('done');
  344. };
  345. });
  346. }})()`);
  347. const { port1, port2 } = new MessageChannelMain();
  348. port1.postMessage('hello');
  349. w.webContents.postMessage('port', null, [port2]);
  350. await emittedOnce(ipcMain, 'done');
  351. });
  352. it('can be passed over another channel', async () => {
  353. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  354. w.loadURL('about:blank');
  355. await w.webContents.executeJavaScript(`(${function () {
  356. const { ipcRenderer } = require('electron');
  357. ipcRenderer.on('port', e1 => {
  358. e1.ports[0].onmessage = e2 => {
  359. e2.ports[0].onmessage = e3 => {
  360. ipcRenderer.send('done', e3.data);
  361. };
  362. };
  363. });
  364. }})()`);
  365. const { port1, port2 } = new MessageChannelMain();
  366. const { port1: port3, port2: port4 } = new MessageChannelMain();
  367. port1.postMessage(null, [port4]);
  368. port3.postMessage('hello');
  369. w.webContents.postMessage('port', null, [port2]);
  370. const [, message] = await emittedOnce(ipcMain, 'done');
  371. expect(message).to.equal('hello');
  372. });
  373. it('can send messages to a closed port', () => {
  374. const { port1, port2 } = new MessageChannelMain();
  375. port2.start();
  376. port2.on('message', () => { throw new Error('unexpected message received'); });
  377. port1.close();
  378. port1.postMessage('hello');
  379. });
  380. it('can send messages to a port whose remote end is closed', () => {
  381. const { port1, port2 } = new MessageChannelMain();
  382. port2.start();
  383. port2.on('message', () => { throw new Error('unexpected message received'); });
  384. port2.close();
  385. port1.postMessage('hello');
  386. });
  387. it('throws when passing null ports', () => {
  388. const { port1 } = new MessageChannelMain();
  389. expect(() => {
  390. port1.postMessage(null, [null] as any);
  391. }).to.throw(/conversion failure/);
  392. });
  393. it('throws when passing duplicate ports', () => {
  394. const { port1 } = new MessageChannelMain();
  395. const { port1: port3 } = new MessageChannelMain();
  396. expect(() => {
  397. port1.postMessage(null, [port3, port3]);
  398. }).to.throw(/duplicate/);
  399. });
  400. it('throws when passing ports that have already been neutered', () => {
  401. const { port1 } = new MessageChannelMain();
  402. const { port1: port3 } = new MessageChannelMain();
  403. port1.postMessage(null, [port3]);
  404. expect(() => {
  405. port1.postMessage(null, [port3]);
  406. }).to.throw(/already neutered/);
  407. });
  408. it('throws when passing itself', () => {
  409. const { port1 } = new MessageChannelMain();
  410. expect(() => {
  411. port1.postMessage(null, [port1]);
  412. }).to.throw(/contains the source port/);
  413. });
  414. describe('GC behavior', () => {
  415. it('is not collected while it could still receive messages', async () => {
  416. let trigger: Function;
  417. const promise = new Promise(resolve => { trigger = resolve; });
  418. const port1 = (() => {
  419. const { port1, port2 } = new MessageChannelMain();
  420. port2.on('message', (e) => { trigger(e.data); });
  421. port2.start();
  422. return port1;
  423. })();
  424. v8Util.requestGarbageCollectionForTesting();
  425. port1.postMessage('hello');
  426. expect(await promise).to.equal('hello');
  427. });
  428. });
  429. });
  430. const generateTests = (title: string, postMessage: (contents: WebContents) => WebContents['postMessage']) => {
  431. describe(title, () => {
  432. it('sends a message', async () => {
  433. const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
  434. w.loadURL('about:blank');
  435. await w.webContents.executeJavaScript(`(${function () {
  436. const { ipcRenderer } = require('electron');
  437. ipcRenderer.on('foo', (_e, msg) => {
  438. ipcRenderer.send('bar', msg);
  439. });
  440. }})()`);
  441. postMessage(w.webContents)('foo', { some: 'message' });
  442. const [, msg] = await emittedOnce(ipcMain, 'bar');
  443. expect(msg).to.deep.equal({ some: 'message' });
  444. });
  445. describe('error handling', () => {
  446. it('throws on missing channel', async () => {
  447. const w = new BrowserWindow({ show: false });
  448. await w.loadURL('about:blank');
  449. expect(() => {
  450. (postMessage(w.webContents) as any)();
  451. }).to.throw(/Insufficient number of arguments/);
  452. });
  453. it('throws on invalid channel', async () => {
  454. const w = new BrowserWindow({ show: false });
  455. await w.loadURL('about:blank');
  456. expect(() => {
  457. postMessage(w.webContents)(null as any, '', []);
  458. }).to.throw(/Error processing argument at index 0/);
  459. });
  460. it('throws on missing message', async () => {
  461. const w = new BrowserWindow({ show: false });
  462. await w.loadURL('about:blank');
  463. expect(() => {
  464. (postMessage(w.webContents) as any)('channel');
  465. }).to.throw(/Insufficient number of arguments/);
  466. });
  467. it('throws on non-serializable message', async () => {
  468. const w = new BrowserWindow({ show: false });
  469. await w.loadURL('about:blank');
  470. expect(() => {
  471. postMessage(w.webContents)('channel', w);
  472. }).to.throw(/An object could not be cloned/);
  473. });
  474. it('throws on invalid transferable list', async () => {
  475. const w = new BrowserWindow({ show: false });
  476. await w.loadURL('about:blank');
  477. expect(() => {
  478. postMessage(w.webContents)('', '', null as any);
  479. }).to.throw(/Invalid value for transfer/);
  480. });
  481. it('throws on transferring non-transferable', async () => {
  482. const w = new BrowserWindow({ show: false });
  483. await w.loadURL('about:blank');
  484. expect(() => {
  485. (postMessage(w.webContents) as any)('channel', '', [123]);
  486. }).to.throw(/Invalid value for transfer/);
  487. });
  488. it('throws when passing null ports', async () => {
  489. const w = new BrowserWindow({ show: false });
  490. await w.loadURL('about:blank');
  491. expect(() => {
  492. postMessage(w.webContents)('foo', null, [null] as any);
  493. }).to.throw(/Invalid value for transfer/);
  494. });
  495. it('throws when passing duplicate ports', async () => {
  496. const w = new BrowserWindow({ show: false });
  497. await w.loadURL('about:blank');
  498. const { port1 } = new MessageChannelMain();
  499. expect(() => {
  500. postMessage(w.webContents)('foo', null, [port1, port1]);
  501. }).to.throw(/duplicate/);
  502. });
  503. it('throws when passing ports that have already been neutered', async () => {
  504. const w = new BrowserWindow({ show: false });
  505. await w.loadURL('about:blank');
  506. const { port1 } = new MessageChannelMain();
  507. postMessage(w.webContents)('foo', null, [port1]);
  508. expect(() => {
  509. postMessage(w.webContents)('foo', null, [port1]);
  510. }).to.throw(/already neutered/);
  511. });
  512. });
  513. });
  514. };
  515. generateTests('WebContents.postMessage', contents => contents.postMessage.bind(contents));
  516. generateTests('WebFrameMain.postMessage', contents => contents.mainFrame.postMessage.bind(contents.mainFrame));
  517. });
  518. });