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

Use a wrapper to properly test CTRL-C event on Windows #359

Merged
merged 8 commits into from Aug 22, 2022
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
62 changes: 43 additions & 19 deletions bin/concurrently.spec.ts
@@ -1,5 +1,6 @@
import { subscribeSpyTo } from '@hirez_io/observer-spy';
import { spawn } from 'child_process';
import { sendCtrlC, spawnWithWrapper } from 'ctrlc-wrapper';
import { build } from 'esbuild';
import fs from 'fs';
import { escapeRegExp } from 'lodash';
Expand All @@ -11,8 +12,14 @@ import { map } from 'rxjs/operators';
import stringArgv from 'string-argv';

const isWindows = process.platform === 'win32';
const createKillMessage = (prefix: string) =>
new RegExp(escapeRegExp(prefix) + ' exited with code ' + (isWindows ? 1 : '(SIGTERM|143)'));
const createKillMessage = (prefix: string, signal: 'SIGTERM' | 'SIGINT') => {
const map: Record<string, string | number> = {
SIGTERM: isWindows ? 1 : '(SIGTERM|143)',
// Could theoretically be anything (e.g. 0) if process has SIGINT handler
SIGINT: isWindows ? '(3221225786|0)' : '(SIGINT|130|0)',
};
return new RegExp(escapeRegExp(prefix) + ' exited with code ' + map[signal]);
};

let tmpDir: string;

Expand All @@ -38,8 +45,9 @@ afterAll(() => {
* Creates a child process running 'concurrently' with the given args.
* Returns observables for its combined stdout + stderr output, close events, pid, and stdin stream.
*/
const run = (args: string) => {
const child = spawn('node', [path.join(tmpDir, 'concurrently.js'), ...stringArgv(args)], {
const run = (args: string, ctrlcWrapper?: boolean) => {
const spawnFn = ctrlcWrapper ? spawnWithWrapper : spawn;
const child = spawnFn('node', [path.join(tmpDir, 'concurrently.js'), ...stringArgv(args)], {
cwd: __dirname,
env: {
...process.env,
Expand Down Expand Up @@ -94,6 +102,7 @@ const run = (args: string) => {
};

return {
process: child,
stdin: child.stdin,
pid: child.pid,
log,
Expand Down Expand Up @@ -160,23 +169,36 @@ describe('exiting conditions', () => {
});

it('is of success when a SIGINT is sent', async () => {
const child = run('"node fixtures/read-echo.js"');
// Windows doesn't support sending signals like on POSIX platforms.
// However, in a console, processes can be interrupted with CTRL+C (like a SIGINT).
// This is what we simulate here with the help of a wrapper application.
const child = run('"node fixtures/read-echo.js"', isWindows ? true : false);
// Wait for command to have started before sending SIGINT
child.log.subscribe((line) => {
if (/READING/.test(line)) {
process.kill(child.pid, 'SIGINT');
if (isWindows) {
// Instruct the wrapper to send CTRL+C to its child
sendCtrlC(child.process);
} else {
process.kill(child.pid, 'SIGINT');
}
}
});
const lines = await child.getLogLines();
const exit = await child.exit;

// TODO
// Windows doesn't support sending signals like on POSIX platforms.
// In a console, processes can be interrupted with CTRL+C (SIGINT).
// However, there is no easy way to simulate this event.
// Calling 'process.kill' on a process in Windows means it
// is getting killed forcefully and abruptly (similar to SIGKILL),
// which then results in the exit code of '1'.
expect(exit.code).toBe(isWindows ? 1 : 0);
expect(exit.code).toBe(0);
expect(lines).toContainEqual(
expect.stringMatching(
createKillMessage(
isWindows
? // '^C' is echoed by read-echo.js (also happens without the wrapper)
'[0] ^Cnode fixtures/read-echo.js'
: '[0] node fixtures/read-echo.js',
'SIGINT'
)
)
);
});
});

Expand Down Expand Up @@ -281,7 +303,9 @@ describe('--kill-others', () => {
expect.stringContaining('Sending SIGTERM to other processes')
);
expect(lines).toContainEqual(
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10'))
expect.stringMatching(
createKillMessage('[0] node fixtures/sleep.mjs 10', 'SIGTERM')
)
);
});
});
Expand All @@ -294,7 +318,7 @@ describe('--kill-others', () => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10'))
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10', 'SIGTERM'))
);
});
});
Expand All @@ -319,7 +343,7 @@ describe('--kill-others-on-fail', () => {
expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
expect(lines).toContainEqual(
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10'))
expect.stringMatching(createKillMessage('[0] node fixtures/sleep.mjs 10', 'SIGTERM'))
);
});
});
Expand Down Expand Up @@ -359,7 +383,7 @@ describe('--handle-input', () => {
expect(exit.code).toBeGreaterThan(0);
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
expect(lines).toContainEqual(
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js'))
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js', 'SIGTERM'))
);
});

Expand All @@ -376,7 +400,7 @@ describe('--handle-input', () => {
expect(exit.code).toBeGreaterThan(0);
expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
expect(lines).toContainEqual(
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js'))
expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js', 'SIGTERM'))
);
});
});
Expand Down
70 changes: 70 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -72,6 +72,7 @@
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"coveralls-next": "^4.1.2",
"ctrlc-wrapper": "^0.0.4",
"esbuild": "^0.15.1",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
Expand Down