diff --git a/lib/browser/init.ts b/lib/browser/init.ts index 7499282dcf..cf514bc4eb 100644 --- a/lib/browser/init.ts +++ b/lib/browser/init.ts @@ -72,8 +72,17 @@ if (process.platform === 'win32') { } } -// Map process.exit to app.exit, which quits gracefully. -process.exit = app.exit as () => never; +// Map process.exit to app.exit, which quits gracefully. When called without +// an explicit code, fall back to process.exitCode like Node.js does. +process.exit = ((code: number | string | undefined | null) => { + // Refs https://github.com/nodejs/node/blob/fc192ee030ee076b948ce7d9d72cba6c101989b8/lib/internal/process/per_thread.js#L229-L252 + if (code !== undefined) { + // Node.js handles any string to number conversion here for us + process.exitCode = code; + } + + app.exit(process.exitCode || 0); +}) as typeof process.exit; // Load the RPC server. require('@electron/internal/browser/rpc-server'); diff --git a/spec/node-spec.ts b/spec/node-spec.ts index 7660fbe46c..f1fd936982 100644 --- a/spec/node-spec.ts +++ b/spec/node-spec.ts @@ -17,7 +17,14 @@ import { spawn } from './lib/codesign-helpers'; import { withTempDirectory } from './lib/fs-helpers'; -import { getRemoteContext, ifdescribe, ifit, itremote, useRemoteContext } from './lib/spec-helpers'; +import { + getRemoteContext, + ifdescribe, + ifit, + itremote, + startRemoteControlApp, + useRemoteContext +} from './lib/spec-helpers'; const mainFixturesPath = path.resolve(__dirname, 'fixtures'); @@ -586,9 +593,115 @@ describe('node feature', () => { }); }); + describe('process.exit', () => { + it('exits with exit code zero when called without an argument', async () => { + const rc = await startRemoteControlApp(); + rc.remotely(() => { + setImmediate(() => process.exit()); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(0); + }); + + it('uses process.exitCode when called without an argument', async () => { + const rc = await startRemoteControlApp(); + rc.remotely(() => { + process.exitCode = 42; + setImmediate(() => process.exit()); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(42); + }); + + it('overrides process.exitCode when called with an argument', async () => { + const rc = await startRemoteControlApp(); + rc.remotely(() => { + process.exitCode = 42; + setImmediate(() => process.exit(11)); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(11); + }); + + it('can be called with a null argument', async () => { + const rc = await startRemoteControlApp(); + rc.remotely(() => { + setImmediate(() => process.exit(null)); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(0); + }); + + it('can be called with a number argument', async () => { + const rc = await startRemoteControlApp(); + rc.remotely(() => { + setImmediate(() => process.exit(7)); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(7); + }); + + it('throws with an invalid number argument', async () => { + const rc = await startRemoteControlApp(); + let stdout = ''; + rc.process.stdout!.on('data', (d) => { + stdout += d.toString(); + }); + rc.remotely(() => { + setImmediate(() => { + try { + process.exit(4.2); + } catch (err) { + console.log(err); + process.exit(99); + } + }); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(99); + expect(stdout).to.match( + /RangeError \[ERR_OUT_OF_RANGE\]: The value of "code" is out of range. It must be an integer./ + ); + }); + + it('can be called with a string argument', async () => { + const rc = await startRemoteControlApp(); + rc.remotely(() => { + setImmediate(() => process.exit('12')); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(12); + }); + + it('throws with an invalid string argument', async () => { + const rc = await startRemoteControlApp(); + let stdout = ''; + rc.process.stdout!.on('data', (d) => { + stdout += d.toString(); + }); + rc.remotely(() => { + setImmediate(() => { + try { + process.exit('invalid'); + } catch (err) { + console.log(err); + process.exit(99); + } + }); + }).catch(() => {}); + const [code] = await once(rc.process, 'exit'); + expect(code).to.equal(99); + expect(stdout).to.match(/TypeError \[ERR_INVALID_ARG_TYPE\]/); + }); + }); + describe('process.stdout', () => { useRemoteContext(); + it('is a real Node stream', () => { + expect((process.stdout as any)._type).to.not.be.undefined(); + }); + itremote('does not throw an exception when accessed', () => { expect(() => process.stdout).to.not.throw(); }); @@ -923,12 +1036,6 @@ describe('node feature', () => { }); }); - describe('process.stdout', () => { - it('is a real Node stream', () => { - expect((process.stdout as any)._type).to.not.be.undefined(); - }); - }); - describe('fs.readFile', () => { it('can accept a FileHandle as the Path argument', async () => { const filePathForHandle = path.resolve(mainFixturesPath, 'dogs-running.txt');