node-spec.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. const ChildProcess = require('child_process')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const dirtyChai = require('dirty-chai')
  5. const fs = require('fs')
  6. const path = require('path')
  7. const os = require('os')
  8. const { ipcRenderer, remote } = require('electron')
  9. const features = process.atomBinding('features')
  10. const { emittedOnce } = require('./events-helpers')
  11. const isCI = remote.getGlobal('isCi')
  12. chai.use(dirtyChai)
  13. describe('node feature', () => {
  14. const fixtures = path.join(__dirname, 'fixtures')
  15. describe('child_process', () => {
  16. beforeEach(function () {
  17. if (!features.isRunAsNodeEnabled()) {
  18. this.skip()
  19. }
  20. })
  21. describe('child_process.fork', () => {
  22. it('works in current process', (done) => {
  23. const child = ChildProcess.fork(path.join(fixtures, 'module', 'ping.js'))
  24. child.on('message', msg => {
  25. expect(msg).to.equal('message')
  26. done()
  27. })
  28. child.send('message')
  29. })
  30. it('preserves args', (done) => {
  31. const args = ['--expose_gc', '-test', '1']
  32. const child = ChildProcess.fork(path.join(fixtures, 'module', 'process_args.js'), args)
  33. child.on('message', (msg) => {
  34. expect(args).to.deep.equal(msg.slice(2))
  35. done()
  36. })
  37. child.send('message')
  38. })
  39. it('works in forked process', (done) => {
  40. const child = ChildProcess.fork(path.join(fixtures, 'module', 'fork_ping.js'))
  41. child.on('message', (msg) => {
  42. expect(msg).to.equal('message')
  43. done()
  44. })
  45. child.send('message')
  46. })
  47. it('works in forked process when options.env is specifed', (done) => {
  48. const child = ChildProcess.fork(path.join(fixtures, 'module', 'fork_ping.js'), [], {
  49. path: process.env['PATH']
  50. })
  51. child.on('message', (msg) => {
  52. expect(msg).to.equal('message')
  53. done()
  54. })
  55. child.send('message')
  56. })
  57. it('works in browser process', (done) => {
  58. const fork = remote.require('child_process').fork
  59. const child = fork(path.join(fixtures, 'module', 'ping.js'))
  60. child.on('message', (msg) => {
  61. expect(msg).to.equal('message')
  62. done()
  63. })
  64. child.send('message')
  65. })
  66. it('has String::localeCompare working in script', (done) => {
  67. const child = ChildProcess.fork(path.join(fixtures, 'module', 'locale-compare.js'))
  68. child.on('message', (msg) => {
  69. expect(msg).to.deep.equal([0, -1, 1])
  70. done()
  71. })
  72. child.send('message')
  73. })
  74. it('has setImmediate working in script', (done) => {
  75. const child = ChildProcess.fork(path.join(fixtures, 'module', 'set-immediate.js'))
  76. child.on('message', (msg) => {
  77. expect(msg).to.equal('ok')
  78. done()
  79. })
  80. child.send('message')
  81. })
  82. it('pipes stdio', (done) => {
  83. const child = ChildProcess.fork(path.join(fixtures, 'module', 'process-stdout.js'), { silent: true })
  84. let data = ''
  85. child.stdout.on('data', (chunk) => {
  86. data += String(chunk)
  87. })
  88. child.on('close', (code) => {
  89. expect(code).to.equal(0)
  90. expect(data).to.equal('pipes stdio')
  91. done()
  92. })
  93. })
  94. it('works when sending a message to a process forked with the --eval argument', (done) => {
  95. const source = "process.on('message', (message) => { process.send(message) })"
  96. const forked = ChildProcess.fork('--eval', [source])
  97. forked.once('message', (message) => {
  98. expect(message).to.equal('hello')
  99. done()
  100. })
  101. forked.send('hello')
  102. })
  103. })
  104. describe('child_process.spawn', () => {
  105. let child
  106. afterEach(() => {
  107. if (child != null) child.kill()
  108. })
  109. it('supports spawning Electron as a node process via the ELECTRON_RUN_AS_NODE env var', (done) => {
  110. child = ChildProcess.spawn(process.execPath, [path.join(__dirname, 'fixtures', 'module', 'run-as-node.js')], {
  111. env: {
  112. ELECTRON_RUN_AS_NODE: true
  113. }
  114. })
  115. let output = ''
  116. child.stdout.on('data', data => {
  117. output += data
  118. })
  119. child.stdout.on('close', () => {
  120. expect(JSON.parse(output)).to.deep.equal({
  121. processLog: process.platform === 'win32' ? 'function' : 'undefined',
  122. processType: 'undefined',
  123. window: 'undefined'
  124. })
  125. done()
  126. })
  127. })
  128. })
  129. })
  130. describe('contexts', () => {
  131. describe('setTimeout in fs callback', () => {
  132. it('does not crash', (done) => {
  133. fs.readFile(__filename, () => {
  134. setTimeout(done, 0)
  135. })
  136. })
  137. })
  138. describe('error thrown in renderer process node context', () => {
  139. it('gets emitted as a process uncaughtException event', (done) => {
  140. const error = new Error('boo!')
  141. const listeners = process.listeners('uncaughtException')
  142. process.removeAllListeners('uncaughtException')
  143. process.on('uncaughtException', (thrown) => {
  144. try {
  145. expect(thrown).to.equal(error)
  146. done()
  147. } catch (e) {
  148. done(e)
  149. } finally {
  150. process.removeAllListeners('uncaughtException')
  151. listeners.forEach((listener) => process.on('uncaughtException', listener))
  152. }
  153. })
  154. fs.readFile(__filename, () => {
  155. throw error
  156. })
  157. })
  158. })
  159. describe('error thrown in main process node context', () => {
  160. it('gets emitted as a process uncaughtException event', () => {
  161. const error = ipcRenderer.sendSync('handle-uncaught-exception', 'hello')
  162. expect(error).to.equal('hello')
  163. })
  164. })
  165. describe('promise rejection in main process node context', () => {
  166. it('gets emitted as a process unhandledRejection event', () => {
  167. const error = ipcRenderer.sendSync('handle-unhandled-rejection', 'hello')
  168. expect(error).to.equal('hello')
  169. })
  170. })
  171. describe('setTimeout called under Chromium event loop in browser process', () => {
  172. it('can be scheduled in time', (done) => {
  173. remote.getGlobal('setTimeout')(done, 0)
  174. })
  175. it('can be promisified', (done) => {
  176. remote.getGlobal('setTimeoutPromisified')(0).then(done)
  177. })
  178. })
  179. describe('setInterval called under Chromium event loop in browser process', () => {
  180. it('can be scheduled in time', (done) => {
  181. let interval = null
  182. let clearing = false
  183. const clear = () => {
  184. if (interval === null || clearing) return
  185. // interval might trigger while clearing (remote is slow sometimes)
  186. clearing = true
  187. remote.getGlobal('clearInterval')(interval)
  188. clearing = false
  189. interval = null
  190. done()
  191. }
  192. interval = remote.getGlobal('setInterval')(clear, 10)
  193. })
  194. })
  195. })
  196. describe('inspector', () => {
  197. let child = null
  198. let exitPromise = null
  199. beforeEach(function () {
  200. if (!features.isRunAsNodeEnabled()) {
  201. this.skip()
  202. }
  203. })
  204. afterEach(async () => {
  205. if (child && exitPromise) {
  206. const [code, signal] = await exitPromise
  207. expect(signal).to.equal(null)
  208. expect(code).to.equal(0)
  209. } else if (child) {
  210. child.kill()
  211. }
  212. })
  213. it('supports starting the v8 inspector with --inspect/--inspect-brk', (done) => {
  214. child = ChildProcess.spawn(process.execPath, ['--inspect-brk', path.join(__dirname, 'fixtures', 'module', 'run-as-node.js')], {
  215. env: {
  216. ELECTRON_RUN_AS_NODE: true
  217. }
  218. })
  219. let output = ''
  220. function cleanup () {
  221. child.stderr.removeListener('data', errorDataListener)
  222. child.stdout.removeListener('data', outDataHandler)
  223. }
  224. function errorDataListener (data) {
  225. output += data
  226. if (output.trim().startsWith('Debugger listening on ws://')) {
  227. cleanup()
  228. done()
  229. }
  230. }
  231. function outDataHandler (data) {
  232. cleanup()
  233. done(new Error(`Unexpected output: ${data.toString()}`))
  234. }
  235. child.stderr.on('data', errorDataListener)
  236. child.stdout.on('data', outDataHandler)
  237. })
  238. it('supports starting the v8 inspector with --inspect and a provided port', (done) => {
  239. child = ChildProcess.spawn(process.execPath, ['--inspect=17364', path.join(__dirname, 'fixtures', 'module', 'run-as-node.js')], {
  240. env: {
  241. ELECTRON_RUN_AS_NODE: true
  242. }
  243. })
  244. exitPromise = emittedOnce(child, 'exit')
  245. let output = ''
  246. function cleanup () {
  247. child.stderr.removeListener('data', errorDataListener)
  248. child.stdout.removeListener('data', outDataHandler)
  249. }
  250. function errorDataListener (data) {
  251. output += data
  252. if (output.trim().startsWith('Debugger listening on ws://')) {
  253. expect(output.trim()).to.contain(':17364', 'should be listening on port 17364')
  254. cleanup()
  255. done()
  256. }
  257. }
  258. function outDataHandler (data) {
  259. cleanup()
  260. done(new Error(`Unexpected output: ${data.toString()}`))
  261. }
  262. child.stderr.on('data', errorDataListener)
  263. child.stdout.on('data', outDataHandler)
  264. })
  265. it('does not start the v8 inspector when --inspect is after a -- argument', (done) => {
  266. child = ChildProcess.spawn(remote.process.execPath, [path.join(__dirname, 'fixtures', 'module', 'noop.js'), '--', '--inspect'])
  267. exitPromise = emittedOnce(child, 'exit')
  268. let output = ''
  269. function dataListener (data) {
  270. output += data
  271. }
  272. child.stderr.on('data', dataListener)
  273. child.stdout.on('data', dataListener)
  274. child.on('exit', () => {
  275. if (output.trim().startsWith('Debugger listening on ws://')) {
  276. done(new Error('Inspector was started when it should not have been'))
  277. } else {
  278. done()
  279. }
  280. })
  281. })
  282. it('does does not crash when quitting with the inspector connected', function (done) {
  283. // IPC Electron child process not supported on Windows
  284. if (process.platform === 'win32') return this.skip()
  285. child = ChildProcess.spawn(remote.process.execPath, [path.join(__dirname, 'fixtures', 'module', 'delay-exit'), '--inspect=0'], {
  286. stdio: ['ipc']
  287. })
  288. exitPromise = emittedOnce(child, 'exit')
  289. let output = ''
  290. function dataListener (data) {
  291. output += data
  292. if (output.trim().startsWith('Debugger listening on ws://') && output.endsWith('\n')) {
  293. const socketMatch = output.trim().match(/(ws:\/\/.+:[0-9]+\/.+?)\n/gm)
  294. if (socketMatch && socketMatch[0]) {
  295. child.stderr.removeListener('data', dataListener)
  296. child.stdout.removeListener('data', dataListener)
  297. const connection = new WebSocket(socketMatch[0])
  298. connection.onopen = () => {
  299. child.send('plz-quit')
  300. done()
  301. }
  302. }
  303. }
  304. }
  305. child.stderr.on('data', dataListener)
  306. child.stdout.on('data', dataListener)
  307. })
  308. it('supports js binding', (done) => {
  309. child = ChildProcess.spawn(process.execPath, ['--inspect', path.join(__dirname, 'fixtures', 'module', 'inspector-binding.js')], {
  310. env: {
  311. ELECTRON_RUN_AS_NODE: true
  312. },
  313. stdio: ['ipc']
  314. })
  315. exitPromise = emittedOnce(child, 'exit')
  316. child.on('message', ({ cmd, debuggerEnabled, success }) => {
  317. if (cmd === 'assert') {
  318. expect(debuggerEnabled).to.be.true()
  319. expect(success).to.be.true()
  320. done()
  321. }
  322. })
  323. })
  324. })
  325. describe('message loop', () => {
  326. describe('process.nextTick', () => {
  327. it('emits the callback', (done) => process.nextTick(done))
  328. it('works in nested calls', (done) => {
  329. process.nextTick(() => {
  330. process.nextTick(() => process.nextTick(done))
  331. })
  332. })
  333. })
  334. describe('setImmediate', () => {
  335. it('emits the callback', (done) => setImmediate(done))
  336. it('works in nested calls', (done) => {
  337. setImmediate(() => {
  338. setImmediate(() => setImmediate(done))
  339. })
  340. })
  341. })
  342. })
  343. describe('net.connect', () => {
  344. before(function () {
  345. if (!features.isRunAsNodeEnabled() || process.platform !== 'darwin') {
  346. this.skip()
  347. }
  348. })
  349. it('emit error when connect to a socket path without listeners', (done) => {
  350. const socketPath = path.join(os.tmpdir(), 'atom-shell-test.sock')
  351. const script = path.join(fixtures, 'module', 'create_socket.js')
  352. const child = ChildProcess.fork(script, [socketPath])
  353. child.on('exit', (code) => {
  354. expect(code).to.equal(0)
  355. const client = require('net').connect(socketPath)
  356. client.on('error', (error) => {
  357. expect(error.code).to.equal('ECONNREFUSED')
  358. done()
  359. })
  360. })
  361. })
  362. })
  363. describe('Buffer', () => {
  364. it('can be created from WebKit external string', () => {
  365. const p = document.createElement('p')
  366. p.innerText = '闲云潭影日悠悠,物换星移几度秋'
  367. const b = Buffer.from(p.innerText)
  368. expect(b.toString()).to.equal('闲云潭影日悠悠,物换星移几度秋')
  369. expect(Buffer.byteLength(p.innerText)).to.equal(45)
  370. })
  371. it('correctly parses external one-byte UTF8 string', () => {
  372. const p = document.createElement('p')
  373. p.innerText = 'Jøhänñéß'
  374. const b = Buffer.from(p.innerText)
  375. expect(b.toString()).to.equal('Jøhänñéß')
  376. expect(Buffer.byteLength(p.innerText)).to.equal(13)
  377. })
  378. it('does not crash when creating large Buffers', () => {
  379. let buffer = Buffer.from(new Array(4096).join(' '))
  380. expect(buffer.length).to.equal(4095)
  381. buffer = Buffer.from(new Array(4097).join(' '))
  382. expect(buffer.length).to.equal(4096)
  383. })
  384. it('does not crash for crypto operations', () => {
  385. const crypto = require('crypto')
  386. const data = 'lG9E+/g4JmRmedDAnihtBD4Dfaha/GFOjd+xUOQI05UtfVX3DjUXvrS98p7kZQwY3LNhdiFo7MY5rGft8yBuDhKuNNag9vRx/44IuClDhdQ='
  387. const key = 'q90K9yBqhWZnAMCMTOJfPQ=='
  388. const cipherText = '{"error_code":114,"error_message":"Tham số không hợp lệ","data":null}'
  389. for (let i = 0; i < 10000; ++i) {
  390. const iv = Buffer.from('0'.repeat(32), 'hex')
  391. const input = Buffer.from(data, 'base64')
  392. const decipher = crypto.createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), iv)
  393. const result = Buffer.concat([decipher.update(input), decipher.final()]).toString('utf8')
  394. expect(cipherText).to.equal(result)
  395. }
  396. })
  397. })
  398. describe('process.stdout', () => {
  399. it('does not throw an exception when accessed', () => {
  400. expect(() => process.stdout).to.not.throw()
  401. })
  402. it('does not throw an exception when calling write()', () => {
  403. expect(() => {
  404. process.stdout.write('test')
  405. }).to.not.throw()
  406. })
  407. it('should have isTTY defined on Mac and Linux', function () {
  408. if (isCI || process.platform === 'win32') {
  409. this.skip()
  410. return
  411. }
  412. expect(process.stdout.isTTY).to.be.a('boolean')
  413. })
  414. it('should have isTTY undefined on Windows', function () {
  415. if (isCI || process.platform !== 'win32') {
  416. this.skip()
  417. return
  418. }
  419. expect(process.stdout.isTTY).to.be.undefined()
  420. })
  421. })
  422. describe('process.stdin', () => {
  423. it('does not throw an exception when accessed', () => {
  424. expect(() => process.stdin).to.not.throw()
  425. })
  426. it('returns null when read from', () => {
  427. expect(process.stdin.read()).to.be.null()
  428. })
  429. })
  430. describe('process.version', () => {
  431. it('should not have -pre', () => {
  432. expect(process.version.endsWith('-pre')).to.be.false()
  433. })
  434. })
  435. describe('vm.runInNewContext', () => {
  436. it('should not crash', () => {
  437. require('vm').runInNewContext('')
  438. })
  439. })
  440. describe('crypto', () => {
  441. it('should list the ripemd160 hash in getHashes', () => {
  442. expect(require('crypto').getHashes()).to.include('ripemd160')
  443. })
  444. it('should be able to create a ripemd160 hash and use it', () => {
  445. const hash = require('crypto').createHash('ripemd160')
  446. hash.update('electron-ripemd160')
  447. expect(hash.digest('hex')).to.equal('fa7fec13c624009ab126ebb99eda6525583395fe')
  448. })
  449. it('should list aes-{128,256}-cfb in getCiphers', () => {
  450. expect(require('crypto').getCiphers()).to.include.members(['aes-128-cfb', 'aes-256-cfb'])
  451. })
  452. it('should be able to create an aes-128-cfb cipher', () => {
  453. require('crypto').createCipheriv('aes-128-cfb', '0123456789abcdef', '0123456789abcdef')
  454. })
  455. it('should be able to create an aes-256-cfb cipher', () => {
  456. require('crypto').createCipheriv('aes-256-cfb', '0123456789abcdef0123456789abcdef', '0123456789abcdef')
  457. })
  458. it('should list des-ede-cbc in getCiphers', () => {
  459. expect(require('crypto').getCiphers()).to.include('des-ede-cbc')
  460. })
  461. it('should be able to create an des-ede-cbc cipher', () => {
  462. const key = Buffer.from('0123456789abcdeff1e0d3c2b5a49786', 'hex')
  463. const iv = Buffer.from('fedcba9876543210', 'hex')
  464. require('crypto').createCipheriv('des-ede-cbc', key, iv)
  465. })
  466. it('should not crash when getting an ECDH key', () => {
  467. const ecdh = require('crypto').createECDH('prime256v1')
  468. expect(ecdh.generateKeys()).to.be.an.instanceof(Buffer)
  469. expect(ecdh.getPrivateKey()).to.be.an.instanceof(Buffer)
  470. })
  471. it('should not crash when generating DH keys or fetching DH fields', () => {
  472. const dh = require('crypto').createDiffieHellman('modp15')
  473. expect(dh.generateKeys()).to.be.an.instanceof(Buffer)
  474. expect(dh.getPublicKey()).to.be.an.instanceof(Buffer)
  475. expect(dh.getPrivateKey()).to.be.an.instanceof(Buffer)
  476. expect(dh.getPrime()).to.be.an.instanceof(Buffer)
  477. expect(dh.getGenerator()).to.be.an.instanceof(Buffer)
  478. })
  479. it('should not crash when creating an ECDH cipher', () => {
  480. const crypto = require('crypto')
  481. const dh = crypto.createECDH('prime256v1')
  482. dh.generateKeys()
  483. dh.setPrivateKey(dh.getPrivateKey())
  484. })
  485. })
  486. describe('fs.mkdir/mkdirSync', () => {
  487. it('does not hang with {recursive: true} on invalid names', function (done) {
  488. if (process.platform !== 'win32') {
  489. return this.skip()
  490. }
  491. expect(() => fs.mkdirSync('invalid2:', { recursive: true })).to.throw()
  492. fs.mkdir('invalid1:', { recursive: true }, (err) => {
  493. expect(err).to.not.be.null()
  494. done()
  495. })
  496. })
  497. })
  498. it('includes the electron version in process.versions', () => {
  499. expect(process.versions)
  500. .to.have.own.property('electron')
  501. .that.is.a('string')
  502. .and.matches(/^\d+\.\d+\.\d+(\S*)?$/)
  503. })
  504. it('includes the chrome version in process.versions', () => {
  505. expect(process.versions)
  506. .to.have.own.property('chrome')
  507. .that.is.a('string')
  508. .and.matches(/^\d+\.\d+\.\d+\.\d+$/)
  509. })
  510. it('can find a module using a package.json main field', () => {
  511. const result = ChildProcess.spawnSync(remote.process.execPath, [path.resolve(fixtures, 'api', 'electron-main-module', 'app.asar')])
  512. expect(result.status).to.equal(0)
  513. })
  514. })