api-utility-process-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. import { expect } from 'chai';
  2. import * as childProcess from 'child_process';
  3. import * as path from 'path';
  4. import { BrowserWindow, MessageChannelMain, utilityProcess, app } from 'electron/main';
  5. import { ifit } from './lib/spec-helpers';
  6. import { closeWindow } from './lib/window-helpers';
  7. import { once } from 'events';
  8. import { setImmediate } from 'timers/promises';
  9. const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process');
  10. const isWindowsOnArm = process.platform === 'win32' && process.arch === 'arm64';
  11. describe('utilityProcess module', () => {
  12. describe('UtilityProcess constructor', () => {
  13. it('throws when empty script path is provided', async () => {
  14. expect(() => {
  15. /* eslint-disable no-new */
  16. utilityProcess.fork('');
  17. /* eslint-disable no-new */
  18. }).to.throw();
  19. });
  20. it('throws when options.stdio is not valid', async () => {
  21. expect(() => {
  22. /* eslint-disable no-new */
  23. utilityProcess.fork(path.join(fixturesPath, 'empty.js'), [], {
  24. execArgv: ['--test', '--test2'],
  25. serviceName: 'test',
  26. stdio: 'ipc'
  27. });
  28. /* eslint-disable no-new */
  29. }).to.throw(/stdio must be of the following values: inherit, pipe, ignore/);
  30. expect(() => {
  31. /* eslint-disable no-new */
  32. utilityProcess.fork(path.join(fixturesPath, 'empty.js'), [], {
  33. execArgv: ['--test', '--test2'],
  34. serviceName: 'test',
  35. stdio: ['ignore', 'ignore']
  36. });
  37. /* eslint-disable no-new */
  38. }).to.throw(/configuration missing for stdin, stdout or stderr/);
  39. expect(() => {
  40. /* eslint-disable no-new */
  41. utilityProcess.fork(path.join(fixturesPath, 'empty.js'), [], {
  42. execArgv: ['--test', '--test2'],
  43. serviceName: 'test',
  44. stdio: ['pipe', 'inherit', 'inherit']
  45. });
  46. /* eslint-disable no-new */
  47. }).to.throw(/stdin value other than ignore is not supported/);
  48. });
  49. });
  50. describe('lifecycle events', () => {
  51. it('emits \'spawn\' when child process successfully launches', async () => {
  52. const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
  53. await once(child, 'spawn');
  54. });
  55. it('emits \'exit\' when child process exits gracefully', async () => {
  56. const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
  57. const [code] = await once(child, 'exit');
  58. expect(code).to.equal(0);
  59. });
  60. it('emits \'exit\' when child process crashes', async () => {
  61. const child = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
  62. // Do not check for exit code in this case,
  63. // SIGSEGV code can be 139 or 11 across our different CI pipeline.
  64. await once(child, 'exit');
  65. });
  66. it('emits \'exit\' corresponding to the child process', async () => {
  67. const child1 = utilityProcess.fork(path.join(fixturesPath, 'endless.js'));
  68. await once(child1, 'spawn');
  69. const child2 = utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
  70. await once(child2, 'exit');
  71. expect(child1.kill()).to.be.true();
  72. await once(child1, 'exit');
  73. });
  74. it('emits \'exit\' when there is uncaught exception', async () => {
  75. const child = utilityProcess.fork(path.join(fixturesPath, 'exception.js'));
  76. const [code] = await once(child, 'exit');
  77. expect(code).to.equal(1);
  78. });
  79. it('emits \'exit\' when process.exit is called', async () => {
  80. const exitCode = 2;
  81. const child = utilityProcess.fork(path.join(fixturesPath, 'custom-exit.js'), [`--exitCode=${exitCode}`]);
  82. const [code] = await once(child, 'exit');
  83. expect(code).to.equal(exitCode);
  84. });
  85. });
  86. describe('app \'child-process-gone\' event', () => {
  87. it('with default serviceName', async () => {
  88. utilityProcess.fork(path.join(fixturesPath, 'crash.js'));
  89. const [, details] = await once(app, 'child-process-gone') as [any, Electron.Details];
  90. expect(details.type).to.equal('Utility');
  91. expect(details.serviceName).to.equal('node.mojom.NodeService');
  92. expect(details.name).to.equal('Node Utility Process');
  93. expect(details.reason).to.be.oneOf(['crashed', 'abnormal-exit']);
  94. });
  95. it('with custom serviceName', async () => {
  96. utilityProcess.fork(path.join(fixturesPath, 'crash.js'), [], { serviceName: 'Hello World!' });
  97. const [, details] = await once(app, 'child-process-gone') as [any, Electron.Details];
  98. expect(details.type).to.equal('Utility');
  99. expect(details.serviceName).to.equal('node.mojom.NodeService');
  100. expect(details.name).to.equal('Hello World!');
  101. expect(details.reason).to.be.oneOf(['crashed', 'abnormal-exit']);
  102. });
  103. });
  104. describe('app.getAppMetrics()', () => {
  105. it('with default serviceName', async () => {
  106. const child = utilityProcess.fork(path.join(fixturesPath, 'endless.js'));
  107. await once(child, 'spawn');
  108. expect(child.pid).to.not.be.null();
  109. await setImmediate();
  110. const details = app.getAppMetrics().find(item => item.pid === child.pid)!;
  111. expect(details).to.be.an('object');
  112. expect(details.type).to.equal('Utility');
  113. expect(details.serviceName).to.to.equal('node.mojom.NodeService');
  114. expect(details.name).to.equal('Node Utility Process');
  115. });
  116. it('with custom serviceName', async () => {
  117. const child = utilityProcess.fork(path.join(fixturesPath, 'endless.js'), [], { serviceName: 'Hello World!' });
  118. await once(child, 'spawn');
  119. expect(child.pid).to.not.be.null();
  120. await setImmediate();
  121. const details = app.getAppMetrics().find(item => item.pid === child.pid)!;
  122. expect(details).to.be.an('object');
  123. expect(details.type).to.equal('Utility');
  124. expect(details.serviceName).to.to.equal('node.mojom.NodeService');
  125. expect(details.name).to.equal('Hello World!');
  126. });
  127. });
  128. describe('kill() API', () => {
  129. it('terminates the child process gracefully', async () => {
  130. const child = utilityProcess.fork(path.join(fixturesPath, 'endless.js'), [], {
  131. serviceName: 'endless'
  132. });
  133. await once(child, 'spawn');
  134. expect(child.kill()).to.be.true();
  135. await once(child, 'exit');
  136. });
  137. });
  138. describe('pid property', () => {
  139. it('is valid when child process launches successfully', async () => {
  140. const child = utilityProcess.fork(path.join(fixturesPath, 'empty.js'));
  141. await once(child, 'spawn');
  142. expect(child.pid).to.not.be.null();
  143. });
  144. it('is undefined when child process fails to launch', async () => {
  145. const child = utilityProcess.fork(path.join(fixturesPath, 'does-not-exist.js'));
  146. expect(child.pid).to.be.undefined();
  147. });
  148. });
  149. describe('stdout property', () => {
  150. it('is null when child process launches with default stdio', async () => {
  151. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'));
  152. await once(child, 'spawn');
  153. expect(child.stdout).to.be.null();
  154. expect(child.stderr).to.be.null();
  155. await once(child, 'exit');
  156. });
  157. it('is null when child process launches with ignore stdio configuration', async () => {
  158. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
  159. stdio: 'ignore'
  160. });
  161. await once(child, 'spawn');
  162. expect(child.stdout).to.be.null();
  163. expect(child.stderr).to.be.null();
  164. await once(child, 'exit');
  165. });
  166. it('is valid when child process launches with pipe stdio configuration', async () => {
  167. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
  168. stdio: 'pipe'
  169. });
  170. await once(child, 'spawn');
  171. expect(child.stdout).to.not.be.null();
  172. let log = '';
  173. child.stdout!.on('data', (chunk) => {
  174. log += chunk.toString('utf8');
  175. });
  176. await once(child, 'exit');
  177. expect(log).to.equal('hello\n');
  178. });
  179. });
  180. describe('stderr property', () => {
  181. it('is null when child process launches with default stdio', async () => {
  182. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'));
  183. await once(child, 'spawn');
  184. expect(child.stdout).to.be.null();
  185. expect(child.stderr).to.be.null();
  186. await once(child, 'exit');
  187. });
  188. it('is null when child process launches with ignore stdio configuration', async () => {
  189. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
  190. stdio: 'ignore'
  191. });
  192. await once(child, 'spawn');
  193. expect(child.stderr).to.be.null();
  194. await once(child, 'exit');
  195. });
  196. ifit(!isWindowsOnArm)('is valid when child process launches with pipe stdio configuration', async () => {
  197. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
  198. stdio: ['ignore', 'pipe', 'pipe']
  199. });
  200. await once(child, 'spawn');
  201. expect(child.stderr).to.not.be.null();
  202. let log = '';
  203. child.stderr!.on('data', (chunk) => {
  204. log += chunk.toString('utf8');
  205. });
  206. await once(child, 'exit');
  207. expect(log).to.equal('world');
  208. });
  209. });
  210. describe('postMessage() API', () => {
  211. it('establishes a default ipc channel with the child process', async () => {
  212. const result = 'I will be echoed.';
  213. const child = utilityProcess.fork(path.join(fixturesPath, 'post-message.js'));
  214. await once(child, 'spawn');
  215. child.postMessage(result);
  216. const [data] = await once(child, 'message');
  217. expect(data).to.equal(result);
  218. const exit = once(child, 'exit');
  219. expect(child.kill()).to.be.true();
  220. await exit;
  221. });
  222. it('supports queuing messages on the receiving end', async () => {
  223. const child = utilityProcess.fork(path.join(fixturesPath, 'post-message-queue.js'));
  224. const p = once(child, 'spawn');
  225. child.postMessage('This message');
  226. child.postMessage(' is');
  227. child.postMessage(' queued');
  228. await p;
  229. const [data] = await once(child, 'message');
  230. expect(data).to.equal('This message is queued');
  231. const exit = once(child, 'exit');
  232. expect(child.kill()).to.be.true();
  233. await exit;
  234. });
  235. });
  236. describe('behavior', () => {
  237. it('supports starting the v8 inspector with --inspect-brk', (done) => {
  238. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
  239. stdio: 'pipe',
  240. execArgv: ['--inspect-brk']
  241. });
  242. let output = '';
  243. const cleanup = () => {
  244. child.stderr!.removeListener('data', listener);
  245. child.stdout!.removeListener('data', listener);
  246. child.once('exit', () => { done(); });
  247. child.kill();
  248. };
  249. const listener = (data: Buffer) => {
  250. output += data;
  251. if (/Debugger listening on ws:/m.test(output)) {
  252. cleanup();
  253. }
  254. };
  255. child.stderr!.on('data', listener);
  256. child.stdout!.on('data', listener);
  257. });
  258. it('supports starting the v8 inspector with --inspect and a provided port', (done) => {
  259. const child = utilityProcess.fork(path.join(fixturesPath, 'log.js'), [], {
  260. stdio: 'pipe',
  261. execArgv: ['--inspect=17364']
  262. });
  263. let output = '';
  264. const cleanup = () => {
  265. child.stderr!.removeListener('data', listener);
  266. child.stdout!.removeListener('data', listener);
  267. child.once('exit', () => { done(); });
  268. child.kill();
  269. };
  270. const listener = (data: Buffer) => {
  271. output += data;
  272. if (/Debugger listening on ws:/m.test(output)) {
  273. expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
  274. cleanup();
  275. }
  276. };
  277. child.stderr!.on('data', listener);
  278. child.stdout!.on('data', listener);
  279. });
  280. it('supports changing dns verbatim with --dns-result-order', (done) => {
  281. const child = utilityProcess.fork(path.join(fixturesPath, 'dns-result-order.js'), [], {
  282. stdio: 'pipe',
  283. execArgv: ['--dns-result-order=ipv4first']
  284. });
  285. let output = '';
  286. const cleanup = () => {
  287. child.stderr!.removeListener('data', listener);
  288. child.stdout!.removeListener('data', listener);
  289. child.once('exit', () => { done(); });
  290. child.kill();
  291. };
  292. const listener = (data: Buffer) => {
  293. output += data;
  294. expect(output.trim()).to.contain('ipv4first', 'default verbatim should be ipv4first');
  295. cleanup();
  296. };
  297. child.stderr!.on('data', listener);
  298. child.stdout!.on('data', listener);
  299. });
  300. ifit(process.platform !== 'win32')('supports redirecting stdout to parent process', async () => {
  301. const result = 'Output from utility process';
  302. const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'inherit-stdout'), `--payload=${result}`]);
  303. let output = '';
  304. appProcess.stdout.on('data', (data: Buffer) => { output += data; });
  305. await once(appProcess, 'exit');
  306. expect(output).to.equal(result);
  307. });
  308. ifit(process.platform !== 'win32')('supports redirecting stderr to parent process', async () => {
  309. const result = 'Error from utility process';
  310. const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'inherit-stderr'), `--payload=${result}`]);
  311. let output = '';
  312. appProcess.stderr.on('data', (data: Buffer) => { output += data; });
  313. await once(appProcess, 'exit');
  314. expect(output).to.include(result);
  315. });
  316. it('can establish communication channel with sandboxed renderer', async () => {
  317. const result = 'Message from sandboxed renderer';
  318. const w = new BrowserWindow({
  319. show: false,
  320. webPreferences: {
  321. preload: path.join(fixturesPath, 'preload.js')
  322. }
  323. });
  324. await w.loadFile(path.join(__dirname, 'fixtures', 'blank.html'));
  325. // Create Message port pair for Renderer <-> Utility Process.
  326. const { port1: rendererPort, port2: childPort1 } = new MessageChannelMain();
  327. w.webContents.postMessage('port', result, [rendererPort]);
  328. // Send renderer and main channel port to utility process.
  329. const child = utilityProcess.fork(path.join(fixturesPath, 'receive-message.js'));
  330. await once(child, 'spawn');
  331. child.postMessage('', [childPort1]);
  332. const [data] = await once(child, 'message');
  333. expect(data).to.equal(result);
  334. // Cleanup.
  335. const exit = once(child, 'exit');
  336. expect(child.kill()).to.be.true();
  337. await exit;
  338. await closeWindow(w);
  339. });
  340. ifit(process.platform === 'linux')('allows executing a setuid binary with child_process', async () => {
  341. const child = utilityProcess.fork(path.join(fixturesPath, 'suid.js'));
  342. await once(child, 'spawn');
  343. const [data] = await once(child, 'message');
  344. expect(data).to.not.be.empty();
  345. const exit = once(child, 'exit');
  346. expect(child.kill()).to.be.true();
  347. await exit;
  348. });
  349. it('inherits parent env as default', async () => {
  350. const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'env-app')], {
  351. env: {
  352. FROM: 'parent',
  353. ...process.env
  354. }
  355. });
  356. let output = '';
  357. appProcess.stdout.on('data', (data: Buffer) => { output += data; });
  358. await once(appProcess.stdout, 'end');
  359. const result = process.platform === 'win32' ? '\r\nparent' : 'parent';
  360. expect(output).to.equal(result);
  361. });
  362. it('does not inherit parent env when custom env is provided', async () => {
  363. const appProcess = childProcess.spawn(process.execPath, [path.join(fixturesPath, 'env-app'), '--create-custom-env'], {
  364. env: {
  365. FROM: 'parent',
  366. ...process.env
  367. }
  368. });
  369. let output = '';
  370. appProcess.stdout.on('data', (data: Buffer) => { output += data; });
  371. await once(appProcess.stdout, 'end');
  372. const result = process.platform === 'win32' ? '\r\nchild' : 'child';
  373. expect(output).to.equal(result);
  374. });
  375. it('changes working directory with cwd', async () => {
  376. const child = utilityProcess.fork('./log.js', [], {
  377. cwd: fixturesPath,
  378. stdio: ['ignore', 'pipe', 'ignore']
  379. });
  380. await once(child, 'spawn');
  381. expect(child.stdout).to.not.be.null();
  382. let log = '';
  383. child.stdout!.on('data', (chunk) => {
  384. log += chunk.toString('utf8');
  385. });
  386. await once(child, 'exit');
  387. expect(log).to.equal('hello\n');
  388. });
  389. it('does not crash when running eval', async () => {
  390. const child = utilityProcess.fork('./eval.js', [], {
  391. cwd: fixturesPath,
  392. stdio: 'ignore'
  393. });
  394. await once(child, 'spawn');
  395. const [data] = await once(child, 'message');
  396. expect(data).to.equal(42);
  397. // Cleanup.
  398. const exit = once(child, 'exit');
  399. expect(child.kill()).to.be.true();
  400. await exit;
  401. });
  402. });
  403. });