Skip to content

Commit

Permalink
Support limit of processes at once (#214)
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavohenke committed Feb 10, 2020
1 parent 9a14ada commit a0cc081
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 27 deletions.
26 changes: 15 additions & 11 deletions README.md
Expand Up @@ -113,18 +113,21 @@ Help:
concurrently [options] <command ...>
General
-n, --names List of custom names to be used in prefix template.
Example names: "main,browser,server" [string]
--name-separator The character to split <names> on. Example usage:
concurrently -n "styles|scripts|server" --name-separator "|"
[default: ","]
-r, --raw Output only raw output of processes, disables prettifying
and concurrently coloring. [boolean]
-s, --success Return exit code of zero or one based on the success or
failure of the "first" child to terminate, the "last child",
or succeed only if "all" child processes succeed.
-m, --max-processes How many processes should run at once.
New processes only spawn after all restart tries of a
process. [number]
-n, --names List of custom names to be used in prefix template.
Example names: "main,browser,server" [string]
--name-separator The character to split <names> on. Example usage:
concurrently -n "styles|scripts|server" --name-separator
"|" [default: ","]
-r, --raw Output only raw output of processes, disables prettifying
and concurrently coloring. [boolean]
-s, --success Return exit code of zero or one based on the success or
failure of the "first" child to terminate, the "last
child", or succeed only if "all" child processes succeed.
[choices: "first", "last", "all"] [default: "all"]
--no-color Disables colors from logging [boolean]
--no-color Disables colors from logging [boolean]
Prefix styling
-p, --prefix Prefix used in logging for each process.
Expand Down Expand Up @@ -230,6 +233,7 @@ concurrently can be used programmatically by using the API documented below:
to read the input from, eg `process.stdin`.
- `killOthers`: an array of exitting conditions that will cause a process to kill others.
Can contain any of `success` or `failure`.
- `maxProcesses`: how many processes should run at once.
- `outputStream`: a [`Writable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_writable_streams)
to write logs to. Default: `process.stdout`.
- `prefix`: the prefix type to use when logging processes output.
Expand Down
10 changes: 9 additions & 1 deletion bin/concurrently.js
Expand Up @@ -13,6 +13,13 @@ const args = yargs
.alias('v', 'version')
.options({
// General
'm': {
alias: 'max-processes',
describe:
'How many processes should run at once.\n' +
'New processes only spawn after all restart tries of a process.',
type: 'number'
},
'n': {
alias: 'names',
describe:
Expand Down Expand Up @@ -125,7 +132,7 @@ const args = yargs
'Can be either the index or the name of the process.'
}
})
.group(['n', 'name-separator', 'raw', 's', 'no-color'], 'General')
.group(['m', 'n', 'name-separator', 'raw', 's', 'no-color'], 'General')
.group(['p', 'c', 'l', 't'], 'Prefix styling')
.group(['i', 'default-input-target'], 'Input handling')
.group(['k', 'kill-others-on-fail'], 'Killing other processes')
Expand All @@ -152,6 +159,7 @@ concurrently(args._.map((command, index) => {
killOthers: args.killOthers
? ['success', 'failure']
: (args.killOthersOnFail ? ['failure'] : []),
maxProcesses: args.maxProcesses,
raw: args.raw,
prefix: args.prefix,
prefixLength: args.prefixLength,
Expand Down
3 changes: 2 additions & 1 deletion index.js
Expand Up @@ -19,6 +19,7 @@ module.exports = (commands, options = {}) => {
});

return concurrently(commands, {
maxProcesses: options.maxProcesses,
raw: options.raw,
successCondition: options.successCondition,
controllers: [
Expand All @@ -30,7 +31,7 @@ module.exports = (commands, options = {}) => {
defaultInputTarget: options.defaultInputTarget,
inputStream: options.inputStream,
}),
new KillOnSignal(),
new KillOnSignal({ process }),
new RestartProcess({
logger,
delay: options.restartDelay,
Expand Down
19 changes: 18 additions & 1 deletion src/concurrently.js
Expand Up @@ -49,7 +49,12 @@ module.exports = (commands, options) => {
commands
);

commands.forEach(command => command.start());
const commandsLeft = commands.slice();
const maxProcesses = Math.max(1, Number(options.maxProcesses) || commandsLeft.length);
for (let i = 0; i < maxProcesses; i++) {
maybeRunMore(commandsLeft);
}

return new CompletionListener({ successCondition: options.successCondition }).listen(commands);
};

Expand All @@ -67,3 +72,15 @@ function parseCommand(command, parsers) {
_.castArray(command)
);
}

function maybeRunMore(commandsLeft) {
const command = commandsLeft.shift();
if (!command) {
return;
}

command.start();
command.close.subscribe(() => {
maybeRunMore(commandsLeft);
});
}
44 changes: 33 additions & 11 deletions src/concurrently.spec.js
@@ -1,24 +1,26 @@
const EventEmitter = require('events');
const { Subject } = require('rxjs');

const createFakeCommand = require('./flow-control/fixtures/fake-command');
const concurrently = require('./concurrently');

let spawn, kill, controllers;
beforeEach(() => {
const process = new EventEmitter();
process.pid = 1;

spawn = jest.fn(() => process);
kill = jest.fn();
controllers = [{ handle: jest.fn(arg => arg) }, { handle: jest.fn(arg => arg) }];
});

let spawn, kill, controllers, processes = [];
const create = (commands, options = {}) => concurrently(
commands,
Object.assign(options, { controllers, spawn, kill })
);

beforeEach(() => {
processes = [];
spawn = jest.fn(() => {
const process = new EventEmitter();
processes.push(process);
process.pid = processes.length;
return process;
});
kill = jest.fn();
controllers = [{ handle: jest.fn(arg => arg) }, { handle: jest.fn(arg => arg) }];
});

it('fails if commands is not an array', () => {
const bomb = () => create('foo');
expect(bomb).toThrowError();
Expand All @@ -36,6 +38,26 @@ it('spawns all commands', () => {
expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({}));
});

it('spawns commands up to configured limit at once', () => {
create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: 2 });
expect(spawn).toHaveBeenCalledTimes(2);
expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({}));
expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({}));

// Test out of order completion picking up new processes in-order
processes[1].emit('close', 1, null);
expect(spawn).toHaveBeenCalledTimes(3);
expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({}));

processes[0].emit('close', null, 'SIGINT');
expect(spawn).toHaveBeenCalledTimes(4);
expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));

// Shouldn't attempt to spawn anything else.
processes[2].emit('close', 1, null);
expect(spawn).toHaveBeenCalledTimes(4);
});

it('runs controllers with the commands', () => {
create(['echo', '"echo wrapped"']);

Expand Down
2 changes: 2 additions & 0 deletions src/defaults.js
Expand Up @@ -8,6 +8,8 @@ module.exports = {
defaultInputTarget: 0,
// Whether process.stdin should be forwarded to child processes
handleInput: false,
// How many processes to run at once
maxProcesses: 0,
nameSeparator: ',',
// Which prefix style to use when logging processes output.
prefix: '',
Expand Down
2 changes: 1 addition & 1 deletion src/flow-control/kill-on-signal.js
Expand Up @@ -2,7 +2,7 @@ const { map } = require('rxjs/operators');


module.exports = class KillOnSignal {
constructor({ process = global.process } = {}) {
constructor({ process }) {
this.process = process;
}

Expand Down
2 changes: 1 addition & 1 deletion src/get-spawn-opts.js
Expand Up @@ -4,7 +4,7 @@ module.exports = ({
colorSupport = supportsColor.stdout,
process = global.process,
raw = false
} = {}) => Object.assign(
}) => Object.assign(
{},
raw && { stdio: 'inherit' },
/^win/.test(process.platform) && { detached: false },
Expand Down

2 comments on commit a0cc081

@CainDS
Copy link

@CainDS CainDS commented on a0cc081 Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give a working example of this please?

Tried to get this to work by specifying it within package.json within the concurrently line but it didn't limit the commands running.

@gustavohenke
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not released yet @CainDS.

Please sign in to comment.