api-utility-process-spec.ts 14 KB

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