node-spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import { expect } from 'chai';
  2. import * as childProcess from 'child_process';
  3. import * as path from 'path';
  4. import * as util from 'util';
  5. import { emittedOnce } from './events-helpers';
  6. import { ifdescribe, ifit } from './spec-helpers';
  7. import { webContents, WebContents } from 'electron/main';
  8. const features = process._linkedBinding('electron_common_features');
  9. const mainFixturesPath = path.resolve(__dirname, 'fixtures');
  10. describe('node feature', () => {
  11. const fixtures = path.join(__dirname, '..', 'spec', 'fixtures');
  12. describe('child_process', () => {
  13. describe('child_process.fork', () => {
  14. it('Works in browser process', async () => {
  15. const child = childProcess.fork(path.join(fixtures, 'module', 'ping.js'));
  16. const message = emittedOnce(child, 'message');
  17. child.send('message');
  18. const [msg] = await message;
  19. expect(msg).to.equal('message');
  20. });
  21. });
  22. });
  23. it('does not hang when using the fs module in the renderer process', async () => {
  24. const appPath = path.join(mainFixturesPath, 'apps', 'libuv-hang', 'main.js');
  25. const appProcess = childProcess.spawn(process.execPath, [appPath], {
  26. cwd: path.join(mainFixturesPath, 'apps', 'libuv-hang'),
  27. stdio: 'inherit'
  28. });
  29. const [code] = await emittedOnce(appProcess, 'close');
  30. expect(code).to.equal(0);
  31. });
  32. describe('contexts', () => {
  33. describe('setTimeout called under Chromium event loop in browser process', () => {
  34. it('Can be scheduled in time', (done) => {
  35. setTimeout(done, 0);
  36. });
  37. it('Can be promisified', (done) => {
  38. util.promisify(setTimeout)(0).then(done);
  39. });
  40. });
  41. describe('setInterval called under Chromium event loop in browser process', () => {
  42. it('can be scheduled in time', (done) => {
  43. let interval: any = null;
  44. let clearing = false;
  45. const clear = () => {
  46. if (interval === null || clearing) return;
  47. // interval might trigger while clearing (remote is slow sometimes)
  48. clearing = true;
  49. clearInterval(interval);
  50. clearing = false;
  51. interval = null;
  52. done();
  53. };
  54. interval = setInterval(clear, 10);
  55. });
  56. });
  57. });
  58. describe('NODE_OPTIONS', () => {
  59. let child: childProcess.ChildProcessWithoutNullStreams;
  60. let exitPromise: Promise<any[]>;
  61. it('Fails for options disallowed by Node.js itself', (done) => {
  62. after(async () => {
  63. const [code, signal] = await exitPromise;
  64. expect(signal).to.equal(null);
  65. // Exit code 9 indicates cli flag parsing failure
  66. expect(code).to.equal(9);
  67. child.kill();
  68. });
  69. const env = Object.assign({}, process.env, { NODE_OPTIONS: '--v8-options' });
  70. child = childProcess.spawn(process.execPath, { env });
  71. exitPromise = emittedOnce(child, 'exit');
  72. let output = '';
  73. let success = false;
  74. const cleanup = () => {
  75. child.stderr.removeListener('data', listener);
  76. child.stdout.removeListener('data', listener);
  77. };
  78. const listener = (data: Buffer) => {
  79. output += data;
  80. if (/electron: --v8-options is not allowed in NODE_OPTIONS/m.test(output)) {
  81. success = true;
  82. cleanup();
  83. done();
  84. }
  85. };
  86. child.stderr.on('data', listener);
  87. child.stdout.on('data', listener);
  88. child.on('exit', () => {
  89. if (!success) {
  90. cleanup();
  91. done(new Error(`Unexpected output: ${output.toString()}`));
  92. }
  93. });
  94. });
  95. it('Disallows crypto-related options', (done) => {
  96. after(() => {
  97. child.kill();
  98. });
  99. const env = Object.assign({}, process.env, { NODE_OPTIONS: '--use-openssl-ca' });
  100. child = childProcess.spawn(process.execPath, ['--enable-logging'], { env });
  101. let output = '';
  102. const cleanup = () => {
  103. child.stderr.removeListener('data', listener);
  104. child.stdout.removeListener('data', listener);
  105. };
  106. const listener = (data: Buffer) => {
  107. output += data;
  108. if (/The NODE_OPTION --use-openssl-ca is not supported in Electron/m.test(output)) {
  109. cleanup();
  110. done();
  111. }
  112. };
  113. child.stderr.on('data', listener);
  114. child.stdout.on('data', listener);
  115. });
  116. it('does allow --require in non-packaged apps', async () => {
  117. const appPath = path.join(fixtures, 'module', 'noop.js');
  118. const env = Object.assign({}, process.env, {
  119. NODE_OPTIONS: `--require=${path.join(fixtures, 'module', 'fail.js')}`
  120. });
  121. // App should exit with code 1.
  122. const child = childProcess.spawn(process.execPath, [appPath], { env });
  123. const [code] = await emittedOnce(child, 'exit');
  124. expect(code).to.equal(1);
  125. });
  126. it('does not allow --require in packaged apps', async () => {
  127. const appPath = path.join(fixtures, 'module', 'noop.js');
  128. const env = Object.assign({}, process.env, {
  129. ELECTRON_FORCE_IS_PACKAGED: 'true',
  130. NODE_OPTIONS: `--require=${path.join(fixtures, 'module', 'fail.js')}`
  131. });
  132. // App should exit with code 0.
  133. const child = childProcess.spawn(process.execPath, [appPath], { env });
  134. const [code] = await emittedOnce(child, 'exit');
  135. expect(code).to.equal(0);
  136. });
  137. });
  138. describe('Node.js cli flags', () => {
  139. let child: childProcess.ChildProcessWithoutNullStreams;
  140. let exitPromise: Promise<any[]>;
  141. ifit(features.isRunAsNodeEnabled())('Prohibits crypto-related flags in ELECTRON_RUN_AS_NODE mode', (done) => {
  142. after(async () => {
  143. const [code, signal] = await exitPromise;
  144. expect(signal).to.equal(null);
  145. expect(code).to.equal(9);
  146. child.kill();
  147. });
  148. child = childProcess.spawn(process.execPath, ['--force-fips'], {
  149. env: { ELECTRON_RUN_AS_NODE: 'true' }
  150. });
  151. exitPromise = emittedOnce(child, 'exit');
  152. let output = '';
  153. const cleanup = () => {
  154. child.stderr.removeListener('data', listener);
  155. child.stdout.removeListener('data', listener);
  156. };
  157. const listener = (data: Buffer) => {
  158. output += data;
  159. if (/.*The Node.js cli flag --force-fips is not supported in Electron/m.test(output)) {
  160. cleanup();
  161. done();
  162. }
  163. };
  164. child.stderr.on('data', listener);
  165. child.stdout.on('data', listener);
  166. });
  167. });
  168. ifdescribe(features.isRunAsNodeEnabled())('inspector', () => {
  169. let child: childProcess.ChildProcessWithoutNullStreams;
  170. let exitPromise: Promise<any[]>;
  171. afterEach(async () => {
  172. if (child && exitPromise) {
  173. const [code, signal] = await exitPromise;
  174. expect(signal).to.equal(null);
  175. expect(code).to.equal(0);
  176. } else if (child) {
  177. child.kill();
  178. }
  179. child = null as any;
  180. exitPromise = null as any;
  181. });
  182. it('Supports starting the v8 inspector with --inspect/--inspect-brk', (done) => {
  183. child = childProcess.spawn(process.execPath, ['--inspect-brk', path.join(fixtures, 'module', 'run-as-node.js')], {
  184. env: { ELECTRON_RUN_AS_NODE: 'true' }
  185. });
  186. let output = '';
  187. const cleanup = () => {
  188. child.stderr.removeListener('data', listener);
  189. child.stdout.removeListener('data', listener);
  190. };
  191. const listener = (data: Buffer) => {
  192. output += data;
  193. if (/Debugger listening on ws:/m.test(output)) {
  194. cleanup();
  195. done();
  196. }
  197. };
  198. child.stderr.on('data', listener);
  199. child.stdout.on('data', listener);
  200. });
  201. it('Supports starting the v8 inspector with --inspect and a provided port', async () => {
  202. child = childProcess.spawn(process.execPath, ['--inspect=17364', path.join(fixtures, 'module', 'run-as-node.js')], {
  203. env: { ELECTRON_RUN_AS_NODE: 'true' }
  204. });
  205. exitPromise = emittedOnce(child, 'exit');
  206. let output = '';
  207. const listener = (data: Buffer) => { output += data; };
  208. const cleanup = () => {
  209. child.stderr.removeListener('data', listener);
  210. child.stdout.removeListener('data', listener);
  211. };
  212. child.stderr.on('data', listener);
  213. child.stdout.on('data', listener);
  214. await emittedOnce(child, 'exit');
  215. cleanup();
  216. if (/^Debugger listening on ws:/m.test(output)) {
  217. expect(output.trim()).to.contain(':17364', 'should be listening on port 17364');
  218. } else {
  219. throw new Error(`Unexpected output: ${output.toString()}`);
  220. }
  221. });
  222. it('Does not start the v8 inspector when --inspect is after a -- argument', async () => {
  223. child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'noop.js'), '--', '--inspect']);
  224. exitPromise = emittedOnce(child, 'exit');
  225. let output = '';
  226. const listener = (data: Buffer) => { output += data; };
  227. child.stderr.on('data', listener);
  228. child.stdout.on('data', listener);
  229. await emittedOnce(child, 'exit');
  230. if (output.trim().startsWith('Debugger listening on ws://')) {
  231. throw new Error('Inspector was started when it should not have been');
  232. }
  233. });
  234. // IPC Electron child process not supported on Windows
  235. ifit(process.platform !== 'win32')('does not crash when quitting with the inspector connected', function (done) {
  236. child = childProcess.spawn(process.execPath, [path.join(fixtures, 'module', 'delay-exit'), '--inspect=0'], {
  237. stdio: ['ipc']
  238. }) as childProcess.ChildProcessWithoutNullStreams;
  239. exitPromise = emittedOnce(child, 'exit');
  240. const cleanup = () => {
  241. child.stderr.removeListener('data', listener);
  242. child.stdout.removeListener('data', listener);
  243. };
  244. let output = '';
  245. const success = false;
  246. function listener (data: Buffer) {
  247. output += data;
  248. console.log(data.toString()); // NOTE: temporary debug logging to try to catch flake.
  249. const match = /^Debugger listening on (ws:\/\/.+:\d+\/.+)\n/m.exec(output.trim());
  250. if (match) {
  251. cleanup();
  252. // NOTE: temporary debug logging to try to catch flake.
  253. child.stderr.on('data', (m) => console.log(m.toString()));
  254. child.stdout.on('data', (m) => console.log(m.toString()));
  255. const w = (webContents as any).create({}) as WebContents;
  256. w.loadURL('about:blank')
  257. .then(() => w.executeJavaScript(`new Promise(resolve => {
  258. const connection = new WebSocket(${JSON.stringify(match[1])})
  259. connection.onopen = () => {
  260. connection.onclose = () => resolve()
  261. connection.close()
  262. }
  263. })`))
  264. .then(() => {
  265. (w as any).destroy();
  266. child.send('plz-quit');
  267. done();
  268. });
  269. }
  270. }
  271. child.stderr.on('data', listener);
  272. child.stdout.on('data', listener);
  273. child.on('exit', () => {
  274. if (!success) cleanup();
  275. });
  276. });
  277. it('Supports js binding', async () => {
  278. child = childProcess.spawn(process.execPath, ['--inspect', path.join(fixtures, 'module', 'inspector-binding.js')], {
  279. env: { ELECTRON_RUN_AS_NODE: 'true' },
  280. stdio: ['ipc']
  281. }) as childProcess.ChildProcessWithoutNullStreams;
  282. exitPromise = emittedOnce(child, 'exit');
  283. const [{ cmd, debuggerEnabled, success }] = await emittedOnce(child, 'message');
  284. expect(cmd).to.equal('assert');
  285. expect(debuggerEnabled).to.be.true();
  286. expect(success).to.be.true();
  287. });
  288. });
  289. it('Can find a module using a package.json main field', () => {
  290. const result = childProcess.spawnSync(process.execPath, [path.resolve(fixtures, 'api', 'electron-main-module', 'app.asar')]);
  291. expect(result.status).to.equal(0);
  292. });
  293. ifit(features.isRunAsNodeEnabled())('handles Promise timeouts correctly', async () => {
  294. const scriptPath = path.join(fixtures, 'module', 'node-promise-timer.js');
  295. const child = childProcess.spawn(process.execPath, [scriptPath], {
  296. env: { ELECTRON_RUN_AS_NODE: 'true' }
  297. });
  298. const [code, signal] = await emittedOnce(child, 'exit');
  299. expect(code).to.equal(0);
  300. expect(signal).to.equal(null);
  301. child.kill();
  302. });
  303. it('performs microtask checkpoint correctly', (done) => {
  304. const f3 = async () => {
  305. return new Promise((resolve, reject) => {
  306. reject(new Error('oops'));
  307. });
  308. };
  309. process.once('unhandledRejection', () => done('catch block is delayed to next tick'));
  310. setTimeout(() => {
  311. f3().catch(() => done());
  312. });
  313. });
  314. });