node-spec.ts 13 KB

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