api-utility-process-spec.ts 18 KB

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