Skip to content

Commit

Permalink
Include command info and index in completion listener Promise
Browse files Browse the repository at this point in the history
Return command information and index in addition to the exit code in the
command close stream and the completion listener Promise. This way we can
associate the exit codes with the commands when using 'concurrently'
programmatically.

Resolves open-cli-tools#181.
  • Loading branch information
peruukki committed Jun 7, 2020
1 parent 54b6456 commit c5bf2fc
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 62 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
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, name, prefixColor, env, index, exitCode }`, where
> `index` is the index in the passed `commands` array. Default values (empty strings or objects) are returned for the
> fields that were not specified.
Example:

Expand Down
12 changes: 10 additions & 2 deletions src/command.js
Original file line number Diff line number Diff line change
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,14 @@ 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: 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
28 changes: 26 additions & 2 deletions src/command.spec.js
Original file line number Diff line number Diff line change
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,38 @@ 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 command = new Command({
command: 'cmd',
name: 'name',
prefixColor: 'green',
env: { VAR: 'yes' },
index: 1,
spawn: () => process,
});

command.close.subscribe(data => {
expect(data.command).toBe('cmd');
expect(data.name).toBe('name');
expect(data.prefixColor).toBe('green');
expect(data.env).toEqual({ VAR: 'yes' });
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 c5bf2fc

Please sign in to comment.