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.
- package/.travis.yml +3 -4
- package/.vscode/settings.json +5 -0
- package/CONTRIBUTING.md +1 -4
- package/README.md +131 -94
- package/appveyor.yml +1 -1
- package/bin/concurrently.js +164 -0
- package/bin/concurrently.spec.js +340 -0
- package/bin/epilogue.txt +46 -0
- package/{test/support → bin/fixtures}/read-echo.js +0 -2
- package/index.js +56 -0
- package/package.json +28 -31
- package/src/command-parser/expand-npm-shortcut.js +13 -0
- package/src/command-parser/expand-npm-shortcut.spec.js +36 -0
- package/src/command-parser/expand-npm-wildcard.js +34 -0
- package/src/command-parser/expand-npm-wildcard.spec.js +58 -0
- package/src/command-parser/strip-quotes.js +12 -0
- package/src/command-parser/strip-quotes.spec.js +14 -0
- package/src/command.js +50 -0
- package/src/command.spec.js +142 -0
- package/src/completion-listener.js +35 -0
- package/src/completion-listener.spec.js +88 -0
- package/src/concurrently.js +69 -0
- package/src/concurrently.spec.js +77 -0
- package/src/defaults.js +27 -0
- package/src/flow-control/fixtures/fake-command.js +16 -0
- package/src/flow-control/input-handler.js +39 -0
- package/src/flow-control/input-handler.spec.js +79 -0
- package/src/flow-control/kill-on-signal.js +29 -0
- package/src/flow-control/kill-on-signal.spec.js +70 -0
- package/src/flow-control/kill-others.js +35 -0
- package/src/flow-control/kill-others.spec.js +66 -0
- package/src/flow-control/log-error.js +20 -0
- package/src/flow-control/log-error.spec.js +40 -0
- package/src/flow-control/log-exit.js +13 -0
- package/src/flow-control/log-exit.spec.js +36 -0
- package/src/flow-control/log-output.js +14 -0
- package/src/flow-control/log-output.spec.js +41 -0
- package/src/flow-control/restart-process.js +51 -0
- package/src/flow-control/restart-process.spec.js +129 -0
- package/src/get-spawn-opts.js +12 -0
- package/src/get-spawn-opts.spec.js +18 -0
- package/src/logger.js +109 -0
- package/src/logger.spec.js +178 -0
- package/src/findChild.js +0 -11
- package/src/main.js +0 -563
- package/src/parseCmds.js +0 -65
- package/src/pkgInfo.js +0 -17
- package/test/support/signal.js +0 -13
- package/test/test-findChild.js +0 -39
- package/test/test-functional.js +0 -396
- package/test/test-parseCmds.js +0 -204
- package/test/utils.js +0 -68
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
|
|
3
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
4
|
+
const KillOnSignal = require('./kill-on-signal');
|
|
5
|
+
|
|
6
|
+
let commands, controller, process;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
process = new EventEmitter();
|
|
9
|
+
commands = [
|
|
10
|
+
createFakeCommand(),
|
|
11
|
+
createFakeCommand(),
|
|
12
|
+
];
|
|
13
|
+
controller = new KillOnSignal({ process });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns commands that keep non-close streams from original commands', () => {
|
|
17
|
+
const newCommands = controller.handle(commands);
|
|
18
|
+
newCommands.forEach((newCommand, i) => {
|
|
19
|
+
expect(newCommand.close).not.toBe(commands[i].close);
|
|
20
|
+
expect(newCommand.error).toBe(commands[i].error);
|
|
21
|
+
expect(newCommand.stdout).toBe(commands[i].stdout);
|
|
22
|
+
expect(newCommand.stderr).toBe(commands[i].stderr);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns commands that map SIGINT to exit code 0', () => {
|
|
27
|
+
const newCommands = controller.handle(commands);
|
|
28
|
+
expect(newCommands).not.toBe(commands);
|
|
29
|
+
expect(newCommands).toHaveLength(commands.length);
|
|
30
|
+
|
|
31
|
+
const callback = jest.fn();
|
|
32
|
+
newCommands[0].close.subscribe(callback);
|
|
33
|
+
process.emit('SIGINT');
|
|
34
|
+
|
|
35
|
+
// A fake command's .kill() call won't trigger a close event automatically...
|
|
36
|
+
commands[0].close.next(1);
|
|
37
|
+
|
|
38
|
+
expect(callback).not.toHaveBeenCalledWith('SIGINT');
|
|
39
|
+
expect(callback).toHaveBeenCalledWith(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns commands that keep non-SIGINT exit codes', () => {
|
|
43
|
+
const newCommands = controller.handle(commands);
|
|
44
|
+
expect(newCommands).not.toBe(commands);
|
|
45
|
+
expect(newCommands).toHaveLength(commands.length);
|
|
46
|
+
|
|
47
|
+
const callback = jest.fn();
|
|
48
|
+
newCommands[0].close.subscribe(callback);
|
|
49
|
+
commands[0].close.next(1);
|
|
50
|
+
|
|
51
|
+
expect(callback).toHaveBeenCalledWith(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('kills all commands on SIGINT', () => {
|
|
55
|
+
controller.handle(commands);
|
|
56
|
+
process.emit('SIGINT');
|
|
57
|
+
|
|
58
|
+
expect(process.listenerCount('SIGINT')).toBe(1);
|
|
59
|
+
expect(commands[0].kill).toHaveBeenCalledWith('SIGINT');
|
|
60
|
+
expect(commands[1].kill).toHaveBeenCalledWith('SIGINT');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('kills all commands on SIGTERM', () => {
|
|
64
|
+
controller.handle(commands);
|
|
65
|
+
process.emit('SIGTERM');
|
|
66
|
+
|
|
67
|
+
expect(process.listenerCount('SIGTERM')).toBe(1);
|
|
68
|
+
expect(commands[0].kill).toHaveBeenCalledWith('SIGTERM');
|
|
69
|
+
expect(commands[1].kill).toHaveBeenCalledWith('SIGTERM');
|
|
70
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const { filter, map } = require('rxjs/operators');
|
|
3
|
+
|
|
4
|
+
module.exports = class KillOthers {
|
|
5
|
+
constructor({ logger, conditions }) {
|
|
6
|
+
this.logger = logger;
|
|
7
|
+
this.conditions = _.castArray(conditions);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
handle(commands) {
|
|
11
|
+
const conditions = this.conditions.filter(condition => (
|
|
12
|
+
condition === 'failure' ||
|
|
13
|
+
condition === 'success'
|
|
14
|
+
));
|
|
15
|
+
|
|
16
|
+
if (!conditions.length) {
|
|
17
|
+
return commands;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const closeStates = commands.map(command => command.close.pipe(
|
|
21
|
+
map(exitCode => exitCode === 0 ? 'success' : 'failure'),
|
|
22
|
+
filter(state => conditions.includes(state))
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
closeStates.forEach(closeState => closeState.subscribe(() => {
|
|
26
|
+
const killableCommands = commands.filter(command => command.killable);
|
|
27
|
+
if (killableCommands.length) {
|
|
28
|
+
this.logger.logGlobalEvent('Sending SIGTERM to other processes..');
|
|
29
|
+
killableCommands.forEach(command => command.kill());
|
|
30
|
+
}
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
return commands;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
|
|
3
|
+
const Logger = require('../logger');
|
|
4
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
5
|
+
const KillOthers = require('./kill-others');
|
|
6
|
+
|
|
7
|
+
let commands, logger;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
commands = [
|
|
10
|
+
createFakeCommand(),
|
|
11
|
+
createFakeCommand()
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
logger = createMockInstance(Logger);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const createWithConditions = conditions => new KillOthers({
|
|
18
|
+
logger,
|
|
19
|
+
conditions
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns same commands', () => {
|
|
23
|
+
expect(createWithConditions(['foo']).handle(commands)).toBe(commands);
|
|
24
|
+
expect(createWithConditions(['failure']).handle(commands)).toBe(commands);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not kill others if condition does not match', () => {
|
|
28
|
+
createWithConditions(['failure']).handle(commands);
|
|
29
|
+
commands[1].killable = true;
|
|
30
|
+
commands[0].close.next(0);
|
|
31
|
+
|
|
32
|
+
expect(logger.logGlobalEvent).not.toHaveBeenCalled();
|
|
33
|
+
expect(commands[0].kill).not.toHaveBeenCalled();
|
|
34
|
+
expect(commands[1].kill).not.toHaveBeenCalled();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('kills other killable processes on success', () => {
|
|
38
|
+
createWithConditions(['success']).handle(commands);
|
|
39
|
+
commands[1].killable = true;
|
|
40
|
+
commands[0].close.next(0);
|
|
41
|
+
|
|
42
|
+
expect(logger.logGlobalEvent).toHaveBeenCalledTimes(1);
|
|
43
|
+
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Sending SIGTERM to other processes..');
|
|
44
|
+
expect(commands[0].kill).not.toHaveBeenCalled();
|
|
45
|
+
expect(commands[1].kill).toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('kills other killable processes on failure', () => {
|
|
49
|
+
createWithConditions(['failure']).handle(commands);
|
|
50
|
+
commands[1].killable = true;
|
|
51
|
+
commands[0].close.next(1);
|
|
52
|
+
|
|
53
|
+
expect(logger.logGlobalEvent).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Sending SIGTERM to other processes..');
|
|
55
|
+
expect(commands[0].kill).not.toHaveBeenCalled();
|
|
56
|
+
expect(commands[1].kill).toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('does not try to kill processes already dead', () => {
|
|
60
|
+
createWithConditions(['failure']).handle(commands);
|
|
61
|
+
commands[0].close.next(1);
|
|
62
|
+
|
|
63
|
+
expect(logger.logGlobalEvent).not.toHaveBeenCalled();
|
|
64
|
+
expect(commands[0].kill).not.toHaveBeenCalled();
|
|
65
|
+
expect(commands[1].kill).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { of } = require('rxjs');
|
|
2
|
+
|
|
3
|
+
module.exports = class LogExit {
|
|
4
|
+
constructor({ logger }) {
|
|
5
|
+
this.logger = logger;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
handle(commands) {
|
|
9
|
+
commands.forEach(command => command.error.subscribe(event => {
|
|
10
|
+
this.logger.logCommandEvent(
|
|
11
|
+
`Error occurred when executing command: ${command.command}`,
|
|
12
|
+
command
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
this.logger.logCommandEvent(event.stack || event, command);
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
return commands;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
const Logger = require('../logger');
|
|
3
|
+
const LogError = require('./log-error');
|
|
4
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
5
|
+
|
|
6
|
+
let controller, logger, commands;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
commands = [
|
|
9
|
+
createFakeCommand(),
|
|
10
|
+
createFakeCommand(),
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
logger = createMockInstance(Logger);
|
|
14
|
+
controller = new LogError({ logger });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns same commands', () => {
|
|
18
|
+
expect(controller.handle(commands)).toBe(commands);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('logs the error event of each command', () => {
|
|
22
|
+
controller.handle(commands);
|
|
23
|
+
commands[0].error.next('error from command 0');
|
|
24
|
+
|
|
25
|
+
const error = new Error('some error message');
|
|
26
|
+
commands[1].error.next(error);
|
|
27
|
+
|
|
28
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(4);
|
|
29
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
30
|
+
`Error occurred when executing command: ${commands[0].command}`,
|
|
31
|
+
commands[0]
|
|
32
|
+
);
|
|
33
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith('error from command 0', commands[0]);
|
|
34
|
+
|
|
35
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
36
|
+
`Error occurred when executing command: ${commands[1].command}`,
|
|
37
|
+
commands[1]
|
|
38
|
+
);
|
|
39
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(error.stack, commands[1]);
|
|
40
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module.exports = class LogExit {
|
|
2
|
+
constructor({ logger }) {
|
|
3
|
+
this.logger = logger;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
handle(commands) {
|
|
7
|
+
commands.forEach(command => command.close.subscribe(code => {
|
|
8
|
+
this.logger.logCommandEvent(`${command.command} exited with code ${code}`, command);
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
return commands;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
const Logger = require('../logger');
|
|
3
|
+
const LogExit = require('./log-exit');
|
|
4
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
5
|
+
|
|
6
|
+
let controller, logger, commands;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
commands = [
|
|
9
|
+
createFakeCommand(),
|
|
10
|
+
createFakeCommand(),
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
logger = createMockInstance(Logger);
|
|
14
|
+
controller = new LogExit({ logger });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns same commands', () => {
|
|
18
|
+
expect(controller.handle(commands)).toBe(commands);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('logs the close event of each command', () => {
|
|
22
|
+
controller.handle(commands);
|
|
23
|
+
|
|
24
|
+
commands[0].close.next(0);
|
|
25
|
+
commands[1].close.next('SIGTERM');
|
|
26
|
+
|
|
27
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);
|
|
28
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
29
|
+
`${commands[0].command} exited with code 0`,
|
|
30
|
+
commands[0]
|
|
31
|
+
);
|
|
32
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
33
|
+
`${commands[1].command} exited with code SIGTERM`,
|
|
34
|
+
commands[1]
|
|
35
|
+
);
|
|
36
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = class LogOutput {
|
|
2
|
+
constructor({ logger }) {
|
|
3
|
+
this.logger = logger;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
handle(commands) {
|
|
7
|
+
commands.forEach(command => {
|
|
8
|
+
command.stdout.subscribe(text => this.logger.logCommandText(text.toString(), command));
|
|
9
|
+
command.stderr.subscribe(text => this.logger.logCommandText(text.toString(), command));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return commands;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
const Logger = require('../logger');
|
|
3
|
+
const LogOutput = require('./log-output');
|
|
4
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
5
|
+
|
|
6
|
+
let controller, logger, commands;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
commands = [
|
|
9
|
+
createFakeCommand(),
|
|
10
|
+
createFakeCommand(),
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
logger = createMockInstance(Logger);
|
|
14
|
+
controller = new LogOutput({ logger });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns same commands', () => {
|
|
18
|
+
expect(controller.handle(commands)).toBe(commands);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('logs the stdout of each command', () => {
|
|
22
|
+
controller.handle(commands);
|
|
23
|
+
|
|
24
|
+
commands[0].stdout.next(Buffer.from('foo'));
|
|
25
|
+
commands[1].stdout.next(Buffer.from('bar'));
|
|
26
|
+
|
|
27
|
+
expect(logger.logCommandText).toHaveBeenCalledTimes(2);
|
|
28
|
+
expect(logger.logCommandText).toHaveBeenCalledWith('foo', commands[0]);
|
|
29
|
+
expect(logger.logCommandText).toHaveBeenCalledWith('bar', commands[1]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('logs the stderr of each command', () => {
|
|
33
|
+
controller.handle(commands);
|
|
34
|
+
|
|
35
|
+
commands[0].stderr.next(Buffer.from('foo'));
|
|
36
|
+
commands[1].stderr.next(Buffer.from('bar'));
|
|
37
|
+
|
|
38
|
+
expect(logger.logCommandText).toHaveBeenCalledTimes(2);
|
|
39
|
+
expect(logger.logCommandText).toHaveBeenCalledWith('foo', commands[0]);
|
|
40
|
+
expect(logger.logCommandText).toHaveBeenCalledWith('bar', commands[1]);
|
|
41
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const Rx = require('rxjs');
|
|
2
|
+
const { defaultIfEmpty, delay, filter, mapTo, skip, take, takeWhile } = require('rxjs/operators');
|
|
3
|
+
|
|
4
|
+
const defaults = require('../defaults');
|
|
5
|
+
|
|
6
|
+
module.exports = class RestartProcess {
|
|
7
|
+
constructor({ delay, tries, logger, scheduler }) {
|
|
8
|
+
this.delay = +delay || defaults.restartDelay;
|
|
9
|
+
this.tries = +tries || defaults.restartTries;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
this.scheduler = scheduler;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
handle(commands) {
|
|
15
|
+
if (this.tries === 0) {
|
|
16
|
+
return commands;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
commands.map(command => command.close.pipe(
|
|
20
|
+
take(this.tries),
|
|
21
|
+
takeWhile(code => code !== 0)
|
|
22
|
+
)).map((failure, index) => Rx.merge(
|
|
23
|
+
// Delay the emission (so that the restarts happen on time),
|
|
24
|
+
// explicitly telling the subscriber that a restart is needed
|
|
25
|
+
failure.pipe(delay(this.delay, this.scheduler), mapTo(true)),
|
|
26
|
+
// Skip the first N emissions (as these would be duplicates of the above),
|
|
27
|
+
// meaning it will be empty because of success, or failed all N times,
|
|
28
|
+
// and no more restarts should be attempted.
|
|
29
|
+
failure.pipe(skip(this.tries), defaultIfEmpty(false))
|
|
30
|
+
).subscribe(restart => {
|
|
31
|
+
const command = commands[index];
|
|
32
|
+
if (restart) {
|
|
33
|
+
this.logger.logCommandEvent(`${command.command} restarted`, command);
|
|
34
|
+
command.start();
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
return commands.map(command => {
|
|
39
|
+
const closeStream = command.close.pipe(filter((value, emission) => {
|
|
40
|
+
// We let all success codes pass, and failures only after restarting won't happen again
|
|
41
|
+
return value === 0 || emission >= this.tries;
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return new Proxy(command, {
|
|
45
|
+
get(target, prop) {
|
|
46
|
+
return prop === 'close' ? closeStream : target[prop];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
const { TestScheduler } = require('rxjs/testing');
|
|
3
|
+
|
|
4
|
+
const Logger = require('../logger');
|
|
5
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
6
|
+
const RestartProcess = require('./restart-process');
|
|
7
|
+
|
|
8
|
+
let commands, controller, logger, scheduler;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
commands = [
|
|
11
|
+
createFakeCommand(),
|
|
12
|
+
createFakeCommand()
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
logger = createMockInstance(Logger);
|
|
16
|
+
scheduler = new TestScheduler();
|
|
17
|
+
controller = new RestartProcess({
|
|
18
|
+
logger,
|
|
19
|
+
scheduler,
|
|
20
|
+
delay: 100,
|
|
21
|
+
tries: 2
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('does not restart processes that complete with success', () => {
|
|
26
|
+
controller.handle(commands);
|
|
27
|
+
|
|
28
|
+
commands[0].close.next(0);
|
|
29
|
+
commands[1].close.next(0);
|
|
30
|
+
|
|
31
|
+
scheduler.flush();
|
|
32
|
+
|
|
33
|
+
expect(commands[0].start).toHaveBeenCalledTimes(0);
|
|
34
|
+
expect(commands[1].start).toHaveBeenCalledTimes(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('restarts processes that fail after delay has passed', () => {
|
|
38
|
+
controller.handle(commands);
|
|
39
|
+
|
|
40
|
+
commands[0].close.next(1);
|
|
41
|
+
commands[1].close.next(0);
|
|
42
|
+
|
|
43
|
+
scheduler.flush();
|
|
44
|
+
|
|
45
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
47
|
+
`${commands[0].command} restarted`,
|
|
48
|
+
commands[0]
|
|
49
|
+
);
|
|
50
|
+
expect(commands[0].start).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(commands[1].start).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('restarts processes up to tries', () => {
|
|
55
|
+
controller.handle(commands);
|
|
56
|
+
|
|
57
|
+
commands[0].close.next(1);
|
|
58
|
+
commands[0].close.next('SIGTERM');
|
|
59
|
+
commands[0].close.next('SIGTERM');
|
|
60
|
+
commands[1].close.next(0);
|
|
61
|
+
|
|
62
|
+
scheduler.flush();
|
|
63
|
+
|
|
64
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);
|
|
65
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
66
|
+
`${commands[0].command} restarted`,
|
|
67
|
+
commands[0]
|
|
68
|
+
);
|
|
69
|
+
expect(commands[0].start).toHaveBeenCalledTimes(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('restarts processes until they succeed', () => {
|
|
73
|
+
controller.handle(commands);
|
|
74
|
+
|
|
75
|
+
commands[0].close.next(1);
|
|
76
|
+
commands[0].close.next(0);
|
|
77
|
+
commands[1].close.next(0);
|
|
78
|
+
|
|
79
|
+
scheduler.flush();
|
|
80
|
+
|
|
81
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
83
|
+
`${commands[0].command} restarted`,
|
|
84
|
+
commands[0]
|
|
85
|
+
);
|
|
86
|
+
expect(commands[0].start).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('returned commands', () => {
|
|
90
|
+
it('are the same if 0 tries are to be attempted', () => {
|
|
91
|
+
controller = new RestartProcess({ logger, scheduler });
|
|
92
|
+
expect(controller.handle(commands)).toBe(commands);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('are not the same, but with same length if 1+ tries are to be attempted', () => {
|
|
96
|
+
const newCommands = controller.handle(commands);
|
|
97
|
+
expect(newCommands).not.toBe(commands);
|
|
98
|
+
expect(newCommands).toHaveLength(commands.length);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('skip close events followed by restarts', () => {
|
|
102
|
+
const newCommands = controller.handle(commands);
|
|
103
|
+
|
|
104
|
+
const callback = jest.fn();
|
|
105
|
+
newCommands[0].close.subscribe(callback);
|
|
106
|
+
newCommands[1].close.subscribe(callback);
|
|
107
|
+
|
|
108
|
+
commands[0].close.next(1);
|
|
109
|
+
commands[0].close.next(1);
|
|
110
|
+
commands[0].close.next(1);
|
|
111
|
+
commands[1].close.next(1);
|
|
112
|
+
commands[1].close.next(0);
|
|
113
|
+
|
|
114
|
+
scheduler.flush();
|
|
115
|
+
|
|
116
|
+
// 1 failure from commands[0], 1 success from commands[1]
|
|
117
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('keep non-close streams from original commands', () => {
|
|
121
|
+
const newCommands = controller.handle(commands);
|
|
122
|
+
newCommands.forEach((newCommand, i) => {
|
|
123
|
+
expect(newCommand.close).not.toBe(commands[i].close);
|
|
124
|
+
expect(newCommand.error).toBe(commands[i].error);
|
|
125
|
+
expect(newCommand.stdout).toBe(commands[i].stdout);
|
|
126
|
+
expect(newCommand.stderr).toBe(commands[i].stderr);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const supportsColor = require('supports-color');
|
|
2
|
+
|
|
3
|
+
module.exports = ({
|
|
4
|
+
colorSupport = supportsColor,
|
|
5
|
+
process = global.process,
|
|
6
|
+
raw = false
|
|
7
|
+
} = {}) => Object.assign(
|
|
8
|
+
{},
|
|
9
|
+
raw && { stdio: 'inherit' },
|
|
10
|
+
/^win/.test(process.platform) && { detached: false },
|
|
11
|
+
colorSupport && { env: Object.assign({ FORCE_COLOR: colorSupport.level }, process.env) }
|
|
12
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const getSpawnOpts = require('./get-spawn-opts');
|
|
2
|
+
|
|
3
|
+
it('sets detached mode to false for Windows platform', () => {
|
|
4
|
+
expect(getSpawnOpts({ process: { platform: 'win32' } }).detached).toBe(false);
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
it('sets stdio to inherit when raw', () => {
|
|
8
|
+
expect(getSpawnOpts({ raw: true }).stdio).toBe('inherit');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('merges FORCE_COLOR into env vars if color supported', () => {
|
|
12
|
+
const process = { env: { foo: 'bar' } };
|
|
13
|
+
expect(getSpawnOpts({ process, colorSupport: false }).env).toBeUndefined();
|
|
14
|
+
expect(getSpawnOpts({ process, colorSupport: { level: 1 } }).env).toEqual({
|
|
15
|
+
FORCE_COLOR: 1,
|
|
16
|
+
foo: 'bar'
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
const formatDate = require('date-fns/format');
|
|
4
|
+
|
|
5
|
+
const defaults = require('./defaults');
|
|
6
|
+
|
|
7
|
+
module.exports = class Logger {
|
|
8
|
+
constructor({ outputStream, prefixFormat, prefixLength, raw, timestampFormat }) {
|
|
9
|
+
this.raw = raw;
|
|
10
|
+
this.outputStream = outputStream;
|
|
11
|
+
this.prefixFormat = prefixFormat;
|
|
12
|
+
this.prefixLength = prefixLength || defaults.prefixLength;
|
|
13
|
+
this.timestampFormat = timestampFormat || defaults.timestampFormat;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
shortenText(text) {
|
|
17
|
+
if (!text || text.length <= this.prefixLength) {
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ellipsis = '..';
|
|
22
|
+
const prefixLength = this.prefixLength - ellipsis.length;
|
|
23
|
+
const endLength = Math.floor(prefixLength / 2);
|
|
24
|
+
const beginningLength = prefixLength - endLength;
|
|
25
|
+
|
|
26
|
+
const beginnning = text.substring(0, beginningLength);
|
|
27
|
+
const end = text.substring(text.length - endLength, text.length);
|
|
28
|
+
return beginnning + ellipsis + end;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getPrefixesFor(command) {
|
|
32
|
+
return {
|
|
33
|
+
none: '',
|
|
34
|
+
pid: command.pid,
|
|
35
|
+
index: command.index,
|
|
36
|
+
name: command.name,
|
|
37
|
+
command: this.shortenText(command.command),
|
|
38
|
+
time: formatDate(Date.now(), this.timestampFormat)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getPrefix(command) {
|
|
43
|
+
const prefix = this.prefixFormat || (command.name ? 'name' : 'index');
|
|
44
|
+
if (prefix === 'none') {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const prefixes = this.getPrefixesFor(command);
|
|
49
|
+
if (Object.keys(prefixes).includes(prefix)) {
|
|
50
|
+
return `[${prefixes[prefix]}]`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return _.reduce(prefixes, (prev, val, key) => {
|
|
54
|
+
const keyRegex = new RegExp(_.escapeRegExp(`{${key}}`), 'g');
|
|
55
|
+
return prev.replace(keyRegex, val);
|
|
56
|
+
}, prefix).trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
colorText(command, text) {
|
|
60
|
+
const color = _.get(chalk, command.prefixColor, chalk.gray.dim);
|
|
61
|
+
return color(text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logCommandEvent(text, command) {
|
|
65
|
+
if (this.raw) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.logCommandText(chalk.gray.dim(text) + '\n', command);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logCommandText(text, command) {
|
|
73
|
+
const prefix = this.colorText(command, this.getPrefix(command));
|
|
74
|
+
return this.log(prefix + (prefix ? ' ' : ''), text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logGlobalEvent(text) {
|
|
78
|
+
if (this.raw) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.log(chalk.gray.dim('-->') + ' ', chalk.gray.dim(text) + '\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
log(prefix, text) {
|
|
86
|
+
if (this.raw) {
|
|
87
|
+
return this.outputStream.write(text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// #70 - replace some ANSI code that would impact clearing lines
|
|
91
|
+
text = text.replace(/\u2026/g, '...');
|
|
92
|
+
|
|
93
|
+
const lines = text.split('\n').map((line, index, lines) => {
|
|
94
|
+
// First line will write prefix only if we finished the last write with a LF.
|
|
95
|
+
// Last line won't write prefix because it should be empty.
|
|
96
|
+
if (index === 0 || index === lines.length - 1) {
|
|
97
|
+
return line;
|
|
98
|
+
}
|
|
99
|
+
return prefix + line;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!this.lastChar || this.lastChar === '\n') {
|
|
103
|
+
this.outputStream.write(prefix);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.lastChar = text[text.length - 1];
|
|
107
|
+
this.outputStream.write(lines.join('\n'));
|
|
108
|
+
}
|
|
109
|
+
};
|