api-utility-process-spec.ts 14 KB

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