api-utility-process-spec.ts 17 KB

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