Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exit with non-zero code when subprocess terminated by signal #2023

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 8 additions & 8 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -1040,15 +1040,15 @@ Expecting one of '${allowedValues.join("', '")}'`);
}

// By default terminate process when spawned process terminates.
// Suppressing the exit if exitCallback defined is a bit messy and of limited use, but does allow process to stay running!
const exitCallback = this._exitCallback;
if (!exitCallback) {
proc.on('close', process.exit.bind(process));
} else {
proc.on('close', () => {
exitCallback(new CommanderError(process.exitCode || 0, 'commander.executeSubCommandAsync', '(close)'));
});
}
proc.on('close', (code, _signal) => {
code = code ?? 1; // code is null if spawned process terminated due to a signal
if (!exitCallback) {
process.exit(code);
} else {
exitCallback(new CommanderError(code, 'commander.executeSubCommandAsync', '(close)'));
}
});
proc.on('error', (err) => {
// @ts-ignore
if (err.code === 'ENOENT') {
Expand Down
61 changes: 34 additions & 27 deletions tests/command.executableSubcommand.signals.test.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
const childProcess = require('child_process');
const path = require('path');

// Test that a signal sent to the parent process is received by the executable subcommand process (which is listening).
const pmPath = path.join(__dirname, 'fixtures', 'pm');

// Disabling tests on Windows as:
// Disabling some tests on Windows as:
// "Windows does not support sending signals"
// https://nodejs.org/api/process.html#process_signal_events
const describeOrSkipOnWindows = (process.platform === 'win32') ? describe.skip : describe;

// Note: the previous (sinon) test had custom code for SIGUSR1, revisit if required:
// As described at https://nodejs.org/api/process.html#process_signal_events
// this signal will start a debugger and thus the process might output an
// additional error message:
// "Failed to open socket on port 5858, waiting 1000 ms before retrying".
describeOrSkipOnWindows('signals', () => {
test.each(['SIGINT', 'SIGHUP', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'])('when program sent %s then executableSubcommand sent signal too', (signal, done) => {
// Spawn program. The listen subcommand waits for a signal and writes the name of the signal to stdout.
const proc = childProcess.spawn(pmPath, ['listen'], {});

describeOrSkipOnWindows.each([['SIGINT'], ['SIGHUP'], ['SIGTERM'], ['SIGUSR1'], ['SIGUSR2']])(
'test signal handling in executableSubcommand', (value) => {
// Slightly tricky test, stick with callback and disable lint warning.
// eslint-disable-next-line jest/no-done-callback
test(`when command killed with ${value} then executableSubcommand receives ${value}`, (done) => {
const pmPath = path.join(__dirname, './fixtures/pm');
let processOutput = '';
proc.stdout.on('data', (data) => {
if (processOutput.length === 0) {
// Send signal to program.
proc.kill(`${signal}`);
}
processOutput += data.toString();
});
proc.on('close', (code) => {
// Check the child subcommand received the signal too.
expect(processOutput).toBe(`Listening for signal...${signal}`);
done();
});
});

// The child process writes to stdout.
const proc = childProcess.spawn(pmPath, ['listen'], {});
test('when executable subcommand sent signal then program exit code is non-zero', () => {
const { status } = childProcess.spawnSync(pmPath, ['terminate'], {});
expect(status).toBeGreaterThan(0);
});

let processOutput = '';
proc.stdout.on('data', (data) => {
if (processOutput.length === 0) {
proc.kill(`${value}`);
}
processOutput += data.toString();
});
proc.on('close', (code) => {
expect(processOutput).toBe(`Listening for signal...${value}`);
done();
});
});
test('when command has exitOverride and executable subcommand sent signal then exit code is non-zero', () => {
const { status } = childProcess.spawnSync(pmPath, ['exit-override', 'terminate'], {});
expect(status).toBeGreaterThan(0);
});

// Not a signal test, but closely related code so adding here.
test('when command has exitOverride and executable subcommand fails then program exit code is subcommand exit code', () => {
const { status } = childProcess.spawnSync(pmPath, ['exit-override', 'fail'], {});
expect(status).toEqual(42);
});
});
13 changes: 12 additions & 1 deletion tests/fixtures/pm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node

var { program } = require('../../');
const path = require('node:path');
const { program } = require('../../');

process.env.FORCE_COLOR = 0; // work-around bug in Jest: https://github.com/jestjs/jest/issues/14391

Expand All @@ -17,4 +18,14 @@ program
.command('specifyInstall', 'specify install subcommand', { executableFile: 'pm-install' })
.command('specifyPublish', 'specify publish subcommand', { executableFile: 'pm-publish' })
.command('silent', 'silently succeed')
.command('fail', 'exit with non-zero status code')
.command('terminate', 'terminate due to signal');

program
.command('exit-override')
.exitOverride((err) => { process.exit(err.exitCode); })
.command('fail', 'exit with non-zero status code', { executableFile: path.join(__dirname, 'pm-fail.js') })
.command('terminate', 'terminate due to signal', { executableFile: path.join(__dirname, 'pm-terminate.js') });

program
.parse(process.argv);
1 change: 1 addition & 0 deletions tests/fixtures/pm-fail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
process.exit(42);
1 change: 1 addition & 0 deletions tests/fixtures/pm-terminate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
process.kill(process.pid, 'SIGINT');