api-utility-process-spec.ts 19 KB

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