Skip to content

Commit

Permalink
api: include command info and index in completion listener Promise va…
Browse files Browse the repository at this point in the history
…lue (#228)

Resolves #181
  • Loading branch information
peruukki committed Jun 20, 2020
1 parent fc2817a commit 42526e7
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 62 deletions.
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -251,7 +251,10 @@ concurrently can be used programmatically by using the API documented below:
to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.ZZZ`

> Returns: a `Promise` that resolves if the run was successful (according to `successCondition` option),
> or rejects, containing an array with the exit codes of each command that has been run.
> or rejects, containing an array of objects with information for each command that has been run, in the order
> that the commands terminated. The objects have the shape `{ command, index, exitCode }`, where `command` is the object
> passed in the `commands` array and `index` its index there. Default values (empty strings or objects) are returned for
> the fields that were not specified.
Example:

Expand Down
14 changes: 12 additions & 2 deletions src/command.js
Expand Up @@ -5,11 +5,12 @@ module.exports = class Command {
return !!this.process;
}

constructor({ index, name, command, prefixColor, killProcess, spawn, spawnOpts }) {
constructor({ index, name, command, prefixColor, env, killProcess, spawn, spawnOpts }) {
this.index = index;
this.name = name;
this.command = command;
this.prefixColor = prefixColor;
this.env = env;
this.killProcess = killProcess;
this.spawn = spawn;
this.spawnOpts = spawnOpts;
Expand All @@ -31,7 +32,16 @@ module.exports = class Command {
});
Rx.fromEvent(child, 'close').subscribe(([exitCode, signal]) => {
this.process = undefined;
this.close.next(exitCode === null ? signal : exitCode);
this.close.next({
command: {
command: this.command,
name: this.name,
prefixColor: this.prefixColor,
env: this.env,
},
index: this.index,
exitCode: exitCode === null ? signal : exitCode,
});
});
child.stdout && pipeTo(Rx.fromEvent(child.stdout, 'data'), this.stdout);
child.stderr && pipeTo(Rx.fromEvent(child.stderr, 'data'), this.stderr);
Expand Down
29 changes: 27 additions & 2 deletions src/command.spec.js
Expand Up @@ -59,7 +59,7 @@ describe('#start()', () => {
const command = new Command({ spawn: () => process });

command.close.subscribe(data => {
expect(data).toBe(0);
expect(data.exitCode).toBe(0);
expect(command.process).toBeUndefined();
done();
});
Expand All @@ -73,14 +73,39 @@ describe('#start()', () => {
const command = new Command({ spawn: () => process });

command.close.subscribe(data => {
expect(data).toBe('SIGKILL');
expect(data.exitCode).toBe('SIGKILL');
done();
});

command.start();
process.emit('close', null, 'SIGKILL');
});

it('shares closes to the close stream with command info and index', done => {
const process = createProcess();
const commandInfo = {
command: 'cmd',
name: 'name',
prefixColor: 'green',
env: { VAR: 'yes' },
};
const command = new Command(
Object.assign({
index: 1,
spawn: () => process
}, commandInfo)
);

command.close.subscribe(data => {
expect(data.command).toEqual(commandInfo);
expect(data.index).toBe(1);
done();
});

command.start();
process.emit('close', 0, null);
});

it('shares stdout to the stdout stream', done => {
const process = createProcessWithIO();
const command = new Command({ spawn: () => process });
Expand Down
8 changes: 4 additions & 4 deletions src/completion-listener.js
Expand Up @@ -27,10 +27,10 @@ module.exports = class CompletionListener {
return Rx.merge(...closeStreams)
.pipe(
bufferCount(closeStreams.length),
switchMap(exitCodes =>
this.isSuccess(exitCodes)
? Rx.of(exitCodes, this.scheduler)
: Rx.throwError(exitCodes, this.scheduler)
switchMap(exitInfos =>
this.isSuccess(exitInfos.map(({ exitCode }) => exitCode))
? Rx.of(exitInfos, this.scheduler)
: Rx.throwError(exitInfos, this.scheduler)
),
take(1)
)
Expand Down
36 changes: 18 additions & 18 deletions src/completion-listener.spec.js
Expand Up @@ -19,70 +19,70 @@ describe('with default success condition set', () => {
it('succeeds if all processes exited with code 0', () => {
const result = createController().listen(commands);

commands[0].close.next(0);
commands[1].close.next(0);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 0 });

scheduler.flush();

return expect(result).resolves.toEqual([0, 0]);
return expect(result).resolves.toEqual([{ exitCode: 0 }, { exitCode: 0 }]);
});

it('fails if one of the processes exited with non-0 code', () => {
const result = createController().listen(commands);

commands[0].close.next(0);
commands[1].close.next(1);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 1 });

scheduler.flush();

expect(result).rejects.toEqual([0, 1]);
expect(result).rejects.toEqual([{ exitCode: 0 }, { exitCode: 1 }]);
});
});

describe('with success condition set to first', () => {
it('succeeds if first process to exit has code 0', () => {
const result = createController('first').listen(commands);

commands[1].close.next(0);
commands[0].close.next(1);
commands[1].close.next({ exitCode: 0 });
commands[0].close.next({ exitCode: 1 });

scheduler.flush();

return expect(result).resolves.toEqual([0, 1]);
return expect(result).resolves.toEqual([{ exitCode: 0 }, { exitCode: 1 }]);
});

it('fails if first process to exit has non-0 code', () => {
const result = createController('first').listen(commands);

commands[1].close.next(1);
commands[0].close.next(0);
commands[1].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 0 });

scheduler.flush();

return expect(result).rejects.toEqual([1, 0]);
return expect(result).rejects.toEqual([{ exitCode: 1 }, { exitCode: 0 }]);
});
});

describe('with success condition set to last', () => {
it('succeeds if last process to exit has code 0', () => {
const result = createController('last').listen(commands);

commands[1].close.next(1);
commands[0].close.next(0);
commands[1].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 0 });

scheduler.flush();

return expect(result).resolves.toEqual([1, 0]);
return expect(result).resolves.toEqual([{ exitCode: 1 }, { exitCode: 0 }]);
});

it('fails if last process to exit has non-0 code', () => {
const result = createController('last').listen(commands);

commands[1].close.next(0);
commands[0].close.next(1);
commands[1].close.next({ exitCode: 0 });
commands[0].close.next({ exitCode: 1 });

scheduler.flush();

return expect(result).rejects.toEqual([0, 1]);
return expect(result).rejects.toEqual([{ exitCode: 0 }, { exitCode: 1 }]);
});
});
5 changes: 3 additions & 2 deletions src/flow-control/kill-on-signal.js
Expand Up @@ -16,8 +16,9 @@ module.exports = class KillOnSignal {
});

return commands.map(command => {
const closeStream = command.close.pipe(map(value => {
return caughtSignal === 'SIGINT' ? 0 : value;
const closeStream = command.close.pipe(map(exitInfo => {
const exitCode = caughtSignal === 'SIGINT' ? 0 : exitInfo.exitCode;
return Object.assign({}, exitInfo, { exitCode });
}));
return new Proxy(command, {
get(target, prop) {
Expand Down
10 changes: 5 additions & 5 deletions src/flow-control/kill-on-signal.spec.js
Expand Up @@ -33,10 +33,10 @@ it('returns commands that map SIGINT to exit code 0', () => {
process.emit('SIGINT');

// A fake command's .kill() call won't trigger a close event automatically...
commands[0].close.next(1);
commands[0].close.next({ exitCode: 1 });

expect(callback).not.toHaveBeenCalledWith('SIGINT');
expect(callback).toHaveBeenCalledWith(0);
expect(callback).not.toHaveBeenCalledWith({ exitCode: 'SIGINT' });
expect(callback).toHaveBeenCalledWith({ exitCode: 0 });
});

it('returns commands that keep non-SIGINT exit codes', () => {
Expand All @@ -46,9 +46,9 @@ it('returns commands that keep non-SIGINT exit codes', () => {

const callback = jest.fn();
newCommands[0].close.subscribe(callback);
commands[0].close.next(1);
commands[0].close.next({ exitCode: 1 });

expect(callback).toHaveBeenCalledWith(1);
expect(callback).toHaveBeenCalledWith({ exitCode: 1 });
});

it('kills all commands on SIGINT', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/flow-control/kill-others.js
Expand Up @@ -18,7 +18,7 @@ module.exports = class KillOthers {
}

const closeStates = commands.map(command => command.close.pipe(
map(exitCode => exitCode === 0 ? 'success' : 'failure'),
map(({ exitCode }) => exitCode === 0 ? 'success' : 'failure'),
filter(state => conditions.includes(state))
));

Expand Down
8 changes: 4 additions & 4 deletions src/flow-control/kill-others.spec.js
Expand Up @@ -27,7 +27,7 @@ it('returns same commands', () => {
it('does not kill others if condition does not match', () => {
createWithConditions(['failure']).handle(commands);
commands[1].killable = true;
commands[0].close.next(0);
commands[0].close.next({ exitCode: 0 });

expect(logger.logGlobalEvent).not.toHaveBeenCalled();
expect(commands[0].kill).not.toHaveBeenCalled();
Expand All @@ -37,7 +37,7 @@ it('does not kill others if condition does not match', () => {
it('kills other killable processes on success', () => {
createWithConditions(['success']).handle(commands);
commands[1].killable = true;
commands[0].close.next(0);
commands[0].close.next({ exitCode: 0 });

expect(logger.logGlobalEvent).toHaveBeenCalledTimes(1);
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Sending SIGTERM to other processes..');
Expand All @@ -48,7 +48,7 @@ it('kills other killable processes on success', () => {
it('kills other killable processes on failure', () => {
createWithConditions(['failure']).handle(commands);
commands[1].killable = true;
commands[0].close.next(1);
commands[0].close.next({ exitCode: 1 });

expect(logger.logGlobalEvent).toHaveBeenCalledTimes(1);
expect(logger.logGlobalEvent).toHaveBeenCalledWith('Sending SIGTERM to other processes..');
Expand All @@ -58,7 +58,7 @@ it('kills other killable processes on failure', () => {

it('does not try to kill processes already dead', () => {
createWithConditions(['failure']).handle(commands);
commands[0].close.next(1);
commands[0].close.next({ exitCode: 1 });

expect(logger.logGlobalEvent).not.toHaveBeenCalled();
expect(commands[0].kill).not.toHaveBeenCalled();
Expand Down
4 changes: 2 additions & 2 deletions src/flow-control/log-exit.js
Expand Up @@ -4,8 +4,8 @@ module.exports = class LogExit {
}

handle(commands) {
commands.forEach(command => command.close.subscribe(code => {
this.logger.logCommandEvent(`${command.command} exited with code ${code}`, command);
commands.forEach(command => command.close.subscribe(({ exitCode }) => {
this.logger.logCommandEvent(`${command.command} exited with code ${exitCode}`, command);
}));

return commands;
Expand Down
4 changes: 2 additions & 2 deletions src/flow-control/log-exit.spec.js
Expand Up @@ -21,8 +21,8 @@ it('returns same commands', () => {
it('logs the close event of each command', () => {
controller.handle(commands);

commands[0].close.next(0);
commands[1].close.next('SIGTERM');
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 'SIGTERM' });

expect(logger.logCommandEvent).toHaveBeenCalledTimes(2);
expect(logger.logCommandEvent).toHaveBeenCalledWith(
Expand Down
6 changes: 3 additions & 3 deletions src/flow-control/restart-process.js
Expand Up @@ -18,7 +18,7 @@ module.exports = class RestartProcess {

commands.map(command => command.close.pipe(
take(this.tries),
takeWhile(code => code !== 0)
takeWhile(({ exitCode }) => exitCode !== 0)
)).map((failure, index) => Rx.merge(
// Delay the emission (so that the restarts happen on time),
// explicitly telling the subscriber that a restart is needed
Expand All @@ -36,9 +36,9 @@ module.exports = class RestartProcess {
}));

return commands.map(command => {
const closeStream = command.close.pipe(filter((value, emission) => {
const closeStream = command.close.pipe(filter(({ exitCode }, emission) => {
// We let all success codes pass, and failures only after restarting won't happen again
return value === 0 || emission >= this.tries;
return exitCode === 0 || emission >= this.tries;
}));

return new Proxy(command, {
Expand Down
32 changes: 16 additions & 16 deletions src/flow-control/restart-process.spec.js
Expand Up @@ -25,8 +25,8 @@ beforeEach(() => {
it('does not restart processes that complete with success', () => {
controller.handle(commands);

commands[0].close.next(0);
commands[1].close.next(0);
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 0 });

scheduler.flush();

Expand All @@ -37,8 +37,8 @@ it('does not restart processes that complete with success', () => {
it('restarts processes that fail after delay has passed', () => {
controller.handle(commands);

commands[0].close.next(1);
commands[1].close.next(0);
commands[0].close.next({ exitCode: 1 });
commands[1].close.next({ exitCode: 0 });

scheduler.flush();

Expand All @@ -54,10 +54,10 @@ it('restarts processes that fail after delay has passed', () => {
it('restarts processes up to tries', () => {
controller.handle(commands);

commands[0].close.next(1);
commands[0].close.next('SIGTERM');
commands[0].close.next('SIGTERM');
commands[1].close.next(0);
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 'SIGTERM' });
commands[0].close.next({ exitCode: 'SIGTERM' });
commands[1].close.next({ exitCode: 0 });

scheduler.flush();

Expand All @@ -72,9 +72,9 @@ it('restarts processes up to tries', () => {
it('restarts processes until they succeed', () => {
controller.handle(commands);

commands[0].close.next(1);
commands[0].close.next(0);
commands[1].close.next(0);
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 0 });
commands[1].close.next({ exitCode: 0 });

scheduler.flush();

Expand Down Expand Up @@ -105,11 +105,11 @@ describe('returned commands', () => {
newCommands[0].close.subscribe(callback);
newCommands[1].close.subscribe(callback);

commands[0].close.next(1);
commands[0].close.next(1);
commands[0].close.next(1);
commands[1].close.next(1);
commands[1].close.next(0);
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 1 });
commands[0].close.next({ exitCode: 1 });
commands[1].close.next({ exitCode: 1 });
commands[1].close.next({ exitCode: 0 });

scheduler.flush();

Expand Down

0 comments on commit 42526e7

Please sign in to comment.