concurrently 3.6.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.travis.yml +3 -4
  2. package/.vscode/settings.json +5 -0
  3. package/CONTRIBUTING.md +1 -4
  4. package/README.md +131 -94
  5. package/appveyor.yml +1 -1
  6. package/bin/concurrently.js +164 -0
  7. package/bin/concurrently.spec.js +340 -0
  8. package/bin/epilogue.txt +46 -0
  9. package/{test/support → bin/fixtures}/read-echo.js +0 -2
  10. package/index.js +56 -0
  11. package/package.json +28 -31
  12. package/src/command-parser/expand-npm-shortcut.js +13 -0
  13. package/src/command-parser/expand-npm-shortcut.spec.js +36 -0
  14. package/src/command-parser/expand-npm-wildcard.js +34 -0
  15. package/src/command-parser/expand-npm-wildcard.spec.js +58 -0
  16. package/src/command-parser/strip-quotes.js +12 -0
  17. package/src/command-parser/strip-quotes.spec.js +14 -0
  18. package/src/command.js +50 -0
  19. package/src/command.spec.js +142 -0
  20. package/src/completion-listener.js +35 -0
  21. package/src/completion-listener.spec.js +88 -0
  22. package/src/concurrently.js +69 -0
  23. package/src/concurrently.spec.js +77 -0
  24. package/src/defaults.js +27 -0
  25. package/src/flow-control/fixtures/fake-command.js +16 -0
  26. package/src/flow-control/input-handler.js +39 -0
  27. package/src/flow-control/input-handler.spec.js +79 -0
  28. package/src/flow-control/kill-on-signal.js +29 -0
  29. package/src/flow-control/kill-on-signal.spec.js +70 -0
  30. package/src/flow-control/kill-others.js +35 -0
  31. package/src/flow-control/kill-others.spec.js +66 -0
  32. package/src/flow-control/log-error.js +20 -0
  33. package/src/flow-control/log-error.spec.js +40 -0
  34. package/src/flow-control/log-exit.js +13 -0
  35. package/src/flow-control/log-exit.spec.js +36 -0
  36. package/src/flow-control/log-output.js +14 -0
  37. package/src/flow-control/log-output.spec.js +41 -0
  38. package/src/flow-control/restart-process.js +51 -0
  39. package/src/flow-control/restart-process.spec.js +129 -0
  40. package/src/get-spawn-opts.js +12 -0
  41. package/src/get-spawn-opts.spec.js +18 -0
  42. package/src/logger.js +109 -0
  43. package/src/logger.spec.js +178 -0
  44. package/src/findChild.js +0 -11
  45. package/src/main.js +0 -563
  46. package/src/parseCmds.js +0 -65
  47. package/src/pkgInfo.js +0 -17
  48. package/test/support/signal.js +0 -13
  49. package/test/test-findChild.js +0 -39
  50. package/test/test-functional.js +0 -396
  51. package/test/test-parseCmds.js +0 -204
  52. package/test/utils.js +0 -68
@@ -0,0 +1,12 @@
1
+ module.exports = class StripQuotes {
2
+ parse(commandInfo) {
3
+ let { command } = commandInfo;
4
+
5
+ // Removes the quotes surrounding a command.
6
+ if (command[0] === '"' || command[0] === '\'') {
7
+ command = command.substr(1, command.length - 2);
8
+ }
9
+
10
+ return Object.assign({}, commandInfo, { command });
11
+ }
12
+ };
@@ -0,0 +1,14 @@
1
+ const StripQuotes = require('./strip-quotes');
2
+ const parser = new StripQuotes();
3
+
4
+ it('returns command as is if no single/double quote at the beginning', () => {
5
+ expect(parser.parse({ command: 'echo foo' })).toEqual({ command: 'echo foo' });
6
+ });
7
+
8
+ it('strips single quotes', () => {
9
+ expect(parser.parse({ command: '\'echo foo\'' })).toEqual({ command: 'echo foo' });
10
+ });
11
+
12
+ it('strips double quotes', () => {
13
+ expect(parser.parse({ command: '"echo foo"' })).toEqual({ command: 'echo foo' });
14
+ });
package/src/command.js ADDED
@@ -0,0 +1,50 @@
1
+ const Rx = require('rxjs');
2
+
3
+ module.exports = class Command {
4
+ get killable() {
5
+ return !!this.process;
6
+ }
7
+
8
+ constructor({ index, name, command, prefixColor, killProcess, spawn, spawnOpts }) {
9
+ this.index = index;
10
+ this.name = name;
11
+ this.command = command;
12
+ this.prefixColor = prefixColor;
13
+ this.killProcess = killProcess;
14
+ this.spawn = spawn;
15
+ this.spawnOpts = spawnOpts;
16
+
17
+ this.error = new Rx.Subject();
18
+ this.close = new Rx.Subject();
19
+ this.stdout = new Rx.Subject();
20
+ this.stderr = new Rx.Subject();
21
+ }
22
+
23
+ start() {
24
+ const child = this.spawn(this.command, this.spawnOpts);
25
+ this.process = child;
26
+ this.pid = child.pid;
27
+
28
+ Rx.fromEvent(child, 'error').subscribe(event => {
29
+ this.process = undefined;
30
+ this.error.next(event);
31
+ });
32
+ Rx.fromEvent(child, 'close').subscribe(([exitCode, signal]) => {
33
+ this.process = undefined;
34
+ this.close.next(exitCode === null ? signal : exitCode);
35
+ });
36
+ child.stdout && pipeTo(Rx.fromEvent(child.stdout, 'data'), this.stdout);
37
+ child.stderr && pipeTo(Rx.fromEvent(child.stderr, 'data'), this.stderr);
38
+ this.stdin = child.stdin;
39
+ }
40
+
41
+ kill(code) {
42
+ if (this.killable) {
43
+ this.killProcess(this.pid, code);
44
+ }
45
+ }
46
+ };
47
+
48
+ function pipeTo(stream, subject) {
49
+ stream.subscribe(event => subject.next(event));
50
+ }
@@ -0,0 +1,142 @@
1
+ const EventEmitter = require('events');
2
+ const Command = require('./command');
3
+
4
+ const createProcess = () => {
5
+ const process = new EventEmitter();
6
+ process.pid = 1;
7
+ return process;
8
+ };
9
+
10
+ const createProcessWithIO = () => {
11
+ const process = createProcess();
12
+ return Object.assign(process, {
13
+ stdout: new EventEmitter(),
14
+ stderr: new EventEmitter(),
15
+ stdin: new EventEmitter()
16
+ });
17
+ };
18
+
19
+ describe('#start()', () => {
20
+ it('spawns process with given command and options', () => {
21
+ const spawn = jest.fn().mockReturnValue(createProcess());
22
+ const command = new Command({
23
+ spawn,
24
+ spawnOpts: { bla: true },
25
+ command: 'echo foo',
26
+ });
27
+ command.start();
28
+
29
+ expect(spawn).toHaveBeenCalledTimes(1);
30
+ expect(spawn).toHaveBeenCalledWith(command.command, { bla: true });
31
+ });
32
+
33
+ it('sets stdin, process and PID', () => {
34
+ const process = createProcessWithIO();
35
+ const command = new Command({ spawn: () => process });
36
+
37
+ command.start();
38
+ expect(command.process).toBe(process);
39
+ expect(command.pid).toBe(process.pid);
40
+ expect(command.stdin).toBe(process.stdin);
41
+ });
42
+
43
+ it('shares errors to the error stream', done => {
44
+ const process = createProcess();
45
+ const command = new Command({ spawn: () => process });
46
+
47
+ command.error.subscribe(data => {
48
+ expect(data).toBe('foo');
49
+ expect(command.process).toBeUndefined();
50
+ done();
51
+ });
52
+
53
+ command.start();
54
+ process.emit('error', 'foo');
55
+ });
56
+
57
+ it('shares closes to the close stream with exit code', done => {
58
+ const process = createProcess();
59
+ const command = new Command({ spawn: () => process });
60
+
61
+ command.close.subscribe(data => {
62
+ expect(data).toBe(0);
63
+ expect(command.process).toBeUndefined();
64
+ done();
65
+ });
66
+
67
+ command.start();
68
+ process.emit('close', 0, null);
69
+ });
70
+
71
+ it('shares closes to the close stream with signal', done => {
72
+ const process = createProcess();
73
+ const command = new Command({ spawn: () => process });
74
+
75
+ command.close.subscribe(data => {
76
+ expect(data).toBe('SIGKILL');
77
+ done();
78
+ });
79
+
80
+ command.start();
81
+ process.emit('close', null, 'SIGKILL');
82
+ });
83
+
84
+ it('shares stdout to the stdout stream', done => {
85
+ const process = createProcessWithIO();
86
+ const command = new Command({ spawn: () => process });
87
+
88
+ command.stdout.subscribe(data => {
89
+ expect(data.toString()).toBe('hello');
90
+ done();
91
+ });
92
+
93
+ command.start();
94
+ process.stdout.emit('data', Buffer.from('hello'));
95
+ });
96
+
97
+ it('shares stderr to the stdout stream', done => {
98
+ const process = createProcessWithIO();
99
+ const command = new Command({ spawn: () => process });
100
+
101
+ command.stderr.subscribe(data => {
102
+ expect(data.toString()).toBe('dang');
103
+ done();
104
+ });
105
+
106
+ command.start();
107
+ process.stderr.emit('data', Buffer.from('dang'));
108
+ });
109
+ });
110
+
111
+ describe('#kill()', () => {
112
+ let process, killProcess, command;
113
+ beforeEach(() => {
114
+ process = createProcess();
115
+ killProcess = jest.fn();
116
+ command = new Command({ spawn: () => process, killProcess });
117
+ });
118
+
119
+ it('kills process', () => {
120
+ command.start();
121
+ command.kill();
122
+
123
+ expect(killProcess).toHaveBeenCalledTimes(1);
124
+ expect(killProcess).toHaveBeenCalledWith(command.pid, undefined);
125
+ });
126
+
127
+ it('kills process with some signal', () => {
128
+ command.start();
129
+ command.kill('SIGKILL');
130
+
131
+ expect(killProcess).toHaveBeenCalledTimes(1);
132
+ expect(killProcess).toHaveBeenCalledWith(command.pid, 'SIGKILL');
133
+ });
134
+
135
+ it('does not try to kill inexistent process', () => {
136
+ command.start();
137
+ process.emit('error');
138
+ command.kill();
139
+
140
+ expect(killProcess).not.toHaveBeenCalled();
141
+ });
142
+ });
@@ -0,0 +1,35 @@
1
+ const Rx = require('rxjs');
2
+ const { bufferCount, map, switchMap, take } = require('rxjs/operators');
3
+
4
+ module.exports = class CompletionListener {
5
+ constructor({ successCondition, scheduler }) {
6
+ this.successCondition = successCondition;
7
+ this.scheduler = scheduler;
8
+ }
9
+
10
+ listen(commands) {
11
+ const closeStreams = commands.map(command => command.close);
12
+ const allClosed = Rx.zip(...closeStreams);
13
+ return Rx.merge(...closeStreams).pipe(
14
+ bufferCount(closeStreams.length),
15
+ map(exitCodes => {
16
+ switch (this.successCondition) {
17
+ /* eslint-disable indent */
18
+ case 'first':
19
+ return exitCodes[0] === 0;
20
+
21
+ case 'last':
22
+ return exitCodes[exitCodes.length - 1] === 0;
23
+
24
+ default:
25
+ return exitCodes.every(exitCode => exitCode === 0);
26
+ /* eslint-enable indent */
27
+ }
28
+ }),
29
+ switchMap(success => success
30
+ ? Rx.of(null, this.scheduler)
31
+ : Rx.throwError(new Error(), this.scheduler)),
32
+ take(1)
33
+ ).toPromise();
34
+ }
35
+ };
@@ -0,0 +1,88 @@
1
+ const { TestScheduler } = require('rxjs/testing');
2
+
3
+ const createFakeCommand = require('./flow-control/fixtures/fake-command');
4
+ const CompletionListener = require('./completion-listener');
5
+
6
+ let commands, scheduler;
7
+ beforeEach(() => {
8
+ commands = [createFakeCommand('foo'), createFakeCommand('bar')];
9
+ scheduler = new TestScheduler();
10
+ });
11
+
12
+ const createController = successCondition => new CompletionListener({
13
+ successCondition,
14
+ scheduler
15
+ });
16
+
17
+ describe('with default success condition set', () => {
18
+ it('succeeds if all processes exited with code 0', () => {
19
+ const result = createController().listen(commands);
20
+
21
+ commands[0].close.next(0);
22
+ commands[1].close.next(0);
23
+
24
+ scheduler.flush();
25
+
26
+ return expect(result).resolves.toBeNull();
27
+ });
28
+
29
+ it('fails if one of the processes exited with non-0 code', () => {
30
+ const result = createController().listen(commands);
31
+
32
+ commands[0].close.next(0);
33
+ commands[1].close.next(1);
34
+
35
+ scheduler.flush();
36
+
37
+ expect(result).rejects.toThrowError();
38
+ });
39
+ });
40
+
41
+
42
+ describe('with success condition set to first', () => {
43
+ it('succeeds if first process to exit has code 0', () => {
44
+ const result = createController('first').listen(commands);
45
+
46
+ commands[1].close.next(0);
47
+ commands[0].close.next(1);
48
+
49
+ scheduler.flush();
50
+
51
+ return expect(result).resolves.toBeNull();
52
+ });
53
+
54
+ it('fails if first process to exit has non-0 code', () => {
55
+ const result = createController('first').listen(commands);
56
+
57
+ commands[1].close.next(1);
58
+ commands[0].close.next(0);
59
+
60
+ scheduler.flush();
61
+
62
+ return expect(result).rejects.toThrowError();
63
+ });
64
+ });
65
+
66
+ describe('with success condition set to last', () => {
67
+ it('succeeds if last process to exit has code 0', () => {
68
+ const result = createController('last').listen(commands);
69
+
70
+ commands[1].close.next(1);
71
+ commands[0].close.next(0);
72
+
73
+ scheduler.flush();
74
+
75
+ return expect(result).resolves.toBeNull();
76
+ });
77
+
78
+ it('fails if last process to exit has non-0 code', () => {
79
+ const result = createController('last').listen(commands);
80
+
81
+ commands[1].close.next(0);
82
+ commands[0].close.next(1);
83
+
84
+ scheduler.flush();
85
+
86
+ return expect(result).rejects.toThrowError();
87
+ });
88
+ });
@@ -0,0 +1,69 @@
1
+ const assert = require('assert');
2
+ const _ = require('lodash');
3
+ const spawn = require('spawn-command');
4
+ const treeKill = require('tree-kill');
5
+
6
+ const StripQuotes = require('./command-parser/strip-quotes');
7
+ const ExpandNpmShortcut = require('./command-parser/expand-npm-shortcut');
8
+ const ExpandNpmWildcard = require('./command-parser/expand-npm-wildcard');
9
+
10
+ const CompletionListener = require('./completion-listener');
11
+
12
+ const getSpawnOpts = require('./get-spawn-opts');
13
+ const Command = require('./command');
14
+
15
+ const defaults = {
16
+ spawn,
17
+ kill: treeKill,
18
+ raw: false,
19
+ controllers: []
20
+ };
21
+
22
+ module.exports = (commands, options) => {
23
+ assert.ok(Array.isArray(commands), '[concurrently] commands should be an array');
24
+ assert.notStrictEqual(commands.length, 0, '[concurrently] no commands provided');
25
+
26
+ options = _.defaults(options, defaults);
27
+
28
+ const commandParsers = [
29
+ new StripQuotes(),
30
+ new ExpandNpmShortcut(),
31
+ new ExpandNpmWildcard()
32
+ ];
33
+
34
+ const spawnOpts = getSpawnOpts({ raw: options.raw });
35
+
36
+ commands = _(commands)
37
+ .map(mapToCommandInfo)
38
+ .flatMap(command => parseCommand(command, commandParsers))
39
+ .map((command, index) => new Command(Object.assign({
40
+ index,
41
+ spawnOpts,
42
+ killProcess: options.kill,
43
+ spawn: options.spawn,
44
+ }, command)))
45
+ .value();
46
+
47
+ commands = options.controllers.reduce(
48
+ (prevCommands, controller) => controller.handle(prevCommands),
49
+ commands
50
+ );
51
+
52
+ commands.forEach(command => command.start());
53
+ return new CompletionListener({ successCondition: options.successCondition }).listen(commands);
54
+ };
55
+
56
+ function mapToCommandInfo(command) {
57
+ return {
58
+ command: command.command || command,
59
+ name: command.name || '',
60
+ prefixColor: command.prefixColor || '',
61
+ };
62
+ }
63
+
64
+ function parseCommand(command, parsers) {
65
+ return parsers.reduce(
66
+ (commands, parser) => _.flatMap(commands, command => parser.parse(command)),
67
+ _.castArray(command)
68
+ );
69
+ }
@@ -0,0 +1,77 @@
1
+ const EventEmitter = require('events');
2
+ const { Subject } = require('rxjs');
3
+
4
+ const createFakeCommand = require('./flow-control/fixtures/fake-command');
5
+ const concurrently = require('./concurrently');
6
+
7
+ let spawn, kill, controllers;
8
+ beforeEach(() => {
9
+ const process = new EventEmitter();
10
+ process.pid = 1;
11
+
12
+ spawn = jest.fn(() => process);
13
+ kill = jest.fn();
14
+ controllers = [{ handle: jest.fn(arg => arg) }, { handle: jest.fn(arg => arg) }];
15
+ });
16
+
17
+ const create = (commands, options = {}) => concurrently(
18
+ commands,
19
+ Object.assign(options, { controllers, spawn, kill })
20
+ );
21
+
22
+ it('fails if commands is not an array', () => {
23
+ const bomb = () => create('foo');
24
+ expect(bomb).toThrowError();
25
+ });
26
+
27
+ it('fails if no commands were provided', () => {
28
+ const bomb = () => create([]);
29
+ expect(bomb).toThrowError();
30
+ });
31
+
32
+ it('spawns all commands', () => {
33
+ create(['echo', 'kill']);
34
+ expect(spawn).toHaveBeenCalledTimes(2);
35
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({}));
36
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({}));
37
+ });
38
+
39
+ it('runs controllers with the commands', () => {
40
+ create(['echo', '"echo wrapped"']);
41
+
42
+ controllers.forEach(controller => {
43
+ expect(controller.handle).toHaveBeenCalledWith([
44
+ expect.objectContaining({ command: 'echo', index: 0 }),
45
+ expect.objectContaining({ command: 'echo wrapped', index: 1 }),
46
+ ]);
47
+ });
48
+ });
49
+
50
+ it('runs commands with a name or prefix color', () => {
51
+ create([
52
+ { command: 'echo', prefixColor: 'red', name: 'foo' },
53
+ 'kill'
54
+ ]);
55
+
56
+ controllers.forEach(controller => {
57
+ expect(controller.handle).toHaveBeenCalledWith([
58
+ expect.objectContaining({ command: 'echo', index: 0, name: 'foo', prefixColor: 'red' }),
59
+ expect.objectContaining({ command: 'kill', index: 1, name: '', prefixColor: '' }),
60
+ ]);
61
+ });
62
+ });
63
+
64
+ it('passes commands wrapped from a controller to the next one', () => {
65
+ const fakeCommand = createFakeCommand('banana', 'banana');
66
+ controllers[0].handle.mockReturnValue([fakeCommand]);
67
+
68
+ create(['echo']);
69
+
70
+ expect(controllers[0].handle).toHaveBeenCalledWith([
71
+ expect.objectContaining({ command: 'echo', index: 0 })
72
+ ]);
73
+
74
+ expect(controllers[1].handle).toHaveBeenCalledWith([fakeCommand]);
75
+
76
+ expect(fakeCommand.start).toHaveBeenCalledTimes(1);
77
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * This file is meant to be a shared place for default configs.
3
+ * It's read by the flow controllers, the executable, etc.
4
+ *
5
+ * Refer to tests for the meaning of the different possible values.
6
+ */
7
+ module.exports = {
8
+ defaultInputTarget: 0,
9
+ // Whether process.stdin should be forwarded to child processes
10
+ handleInput: false,
11
+ nameSeparator: ',',
12
+ // Which prefix style to use when logging processes output.
13
+ prefix: '',
14
+ // Refer to https://www.npmjs.com/package/chalk
15
+ prefixColors: 'gray.dim',
16
+ // How many bytes we'll show on the command prefix
17
+ prefixLength: 10,
18
+ raw: false,
19
+ // Number of attempts of restarting a process, if it exits with non-0 code
20
+ restartTries: 0,
21
+ // How many milliseconds concurrently should wait before restarting a process.
22
+ restartDelay: 0,
23
+ // Condition of success for concurrently itself.
24
+ success: 'all',
25
+ // Refer to https://date-fns.org/v1.29.0/docs/format
26
+ timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS'
27
+ };
@@ -0,0 +1,16 @@
1
+ const { createMockInstance } = require('jest-create-mock-instance');
2
+ const { Writable } = require('stream');
3
+ const { Subject } = require('rxjs');
4
+
5
+ module.exports = (name = 'foo', command = 'echo foo', index = 0) => ({
6
+ index,
7
+ name,
8
+ command,
9
+ close: new Subject(),
10
+ error: new Subject(),
11
+ stderr: new Subject(),
12
+ stdout: new Subject(),
13
+ stdin: createMockInstance(Writable),
14
+ start: jest.fn(),
15
+ kill: jest.fn()
16
+ });
@@ -0,0 +1,39 @@
1
+ const Rx = require('rxjs');
2
+ const { map } = require('rxjs/operators');
3
+
4
+ const defaults = require('../defaults');
5
+
6
+ module.exports = class InputHandler {
7
+ constructor({ defaultInputTarget, inputStream, logger }) {
8
+ this.defaultInputTarget = defaultInputTarget || defaults.defaultInputTarget;
9
+ this.inputStream = inputStream;
10
+ this.logger = logger;
11
+ }
12
+
13
+ handle(commands) {
14
+ if (!this.inputStream) {
15
+ return commands;
16
+ }
17
+
18
+ Rx.fromEvent(this.inputStream, 'data')
19
+ .pipe(map(data => data.toString()))
20
+ .subscribe(data => {
21
+ let [targetId, input] = data.split(':', 2);
22
+ targetId = input ? targetId : this.defaultInputTarget;
23
+ input = input || data;
24
+
25
+ const command = commands.find(command => (
26
+ command.name === targetId ||
27
+ command.index.toString() === targetId.toString()
28
+ ));
29
+
30
+ if (command && command.stdin) {
31
+ command.stdin.write(input);
32
+ } else {
33
+ this.logger.logGlobalEvent(`Unable to find command ${targetId}, or it has no stdin open\n`);
34
+ }
35
+ });
36
+
37
+ return commands;
38
+ }
39
+ };
@@ -0,0 +1,79 @@
1
+ const EventEmitter = require('events');
2
+ const { createMockInstance } = require('jest-create-mock-instance');
3
+
4
+ const Logger = require('../logger');
5
+ const createFakeCommand = require('./fixtures/fake-command');
6
+ const InputHandler = require('./input-handler');
7
+
8
+ let commands, controller, inputStream, logger;
9
+
10
+ beforeEach(() => {
11
+ commands = [
12
+ createFakeCommand('foo', 'echo foo', 0),
13
+ createFakeCommand('bar', 'echo bar', 1),
14
+ ];
15
+ inputStream = new EventEmitter();
16
+ logger = createMockInstance(Logger);
17
+ controller = new InputHandler({
18
+ defaultInputTarget: 0,
19
+ inputStream,
20
+ logger
21
+ });
22
+ });
23
+
24
+ it('returns same commands', () => {
25
+ expect(controller.handle(commands)).toBe(commands);
26
+
27
+ controller = new InputHandler({ logger });
28
+ expect(controller.handle(commands)).toBe(commands);
29
+ });
30
+
31
+ it('forwards input stream to default target ID', () => {
32
+ controller.handle(commands);
33
+
34
+ inputStream.emit('data', Buffer.from('something'));
35
+
36
+ expect(commands[0].stdin.write).toHaveBeenCalledTimes(1);
37
+ expect(commands[0].stdin.write).toHaveBeenCalledWith('something');
38
+ expect(commands[1].stdin.write).not.toHaveBeenCalled();
39
+ });
40
+
41
+ it('forwards input stream to target index specified in input', () => {
42
+ controller.handle(commands);
43
+
44
+ inputStream.emit('data', Buffer.from('1:something'));
45
+
46
+ expect(commands[0].stdin.write).not.toHaveBeenCalled();
47
+ expect(commands[1].stdin.write).toHaveBeenCalledTimes(1);
48
+ expect(commands[1].stdin.write).toHaveBeenCalledWith('something');
49
+ });
50
+
51
+ it('forwards input stream to target name specified in input', () => {
52
+ controller.handle(commands);
53
+
54
+ inputStream.emit('data', Buffer.from('bar:something'));
55
+
56
+ expect(commands[0].stdin.write).not.toHaveBeenCalled();
57
+ expect(commands[1].stdin.write).toHaveBeenCalledTimes(1);
58
+ expect(commands[1].stdin.write).toHaveBeenCalledWith('something');
59
+ });
60
+
61
+ it('logs error if command has no stdin open', () => {
62
+ commands[0].stdin = null;
63
+ controller.handle(commands);
64
+
65
+ inputStream.emit('data', Buffer.from('something'));
66
+
67
+ expect(commands[1].stdin.write).not.toHaveBeenCalled();
68
+ expect(logger.logGlobalEvent).toHaveBeenCalledWith('Unable to find command 0, or it has no stdin open\n');
69
+ });
70
+
71
+ it('logs error if command is not found', () => {
72
+ controller.handle(commands);
73
+
74
+ inputStream.emit('data', Buffer.from('foobar:something'));
75
+
76
+ expect(commands[0].stdin.write).not.toHaveBeenCalled();
77
+ expect(commands[1].stdin.write).not.toHaveBeenCalled();
78
+ expect(logger.logGlobalEvent).toHaveBeenCalledWith('Unable to find command foobar, or it has no stdin open\n');
79
+ });
@@ -0,0 +1,29 @@
1
+ const { map } = require('rxjs/operators');
2
+
3
+
4
+ module.exports = class KillOnSignal {
5
+ constructor({ process = global.process } = {}) {
6
+ this.process = process;
7
+ }
8
+
9
+ handle(commands) {
10
+ let caughtSignal;
11
+ ['SIGINT', 'SIGTERM'].forEach(signal => {
12
+ this.process.on(signal, () => {
13
+ caughtSignal = signal;
14
+ commands.forEach(command => command.kill(signal));
15
+ });
16
+ });
17
+
18
+ return commands.map(command => {
19
+ const closeStream = command.close.pipe(map(value => {
20
+ return caughtSignal === 'SIGINT' ? 0 : value;
21
+ }));
22
+ return new Proxy(command, {
23
+ get(target, prop) {
24
+ return prop === 'close' ? closeStream : target[prop];
25
+ }
26
+ });
27
+ });
28
+ }
29
+ };