Skip to content

Commit

Permalink
Add heaps of comments to undocumented code
Browse files Browse the repository at this point in the history
  • Loading branch information
gustavohenke committed Jan 2, 2022
1 parent 53c7a98 commit 44a3747
Show file tree
Hide file tree
Showing 15 changed files with 261 additions and 3 deletions.
54 changes: 52 additions & 2 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,50 @@ import { ChildProcess as BaseChildProcess, SpawnOptions } from 'child_process';
import * as Rx from 'rxjs';
import { EventEmitter, Writable } from 'stream';

/**
* Identifier for a command; if string, it's the command's name, if number, it's the index.
*/
export type CommandIdentifier = string | number;

export interface CommandInfo {
/**
* Command's name.
*/
name: string,

/**
* Which command line the command has.
*/
command: string,

/**
* Which environment variables should the spawned process have.
*/
env?: Record<string, any>,

/**
* The current working directory of the process when spawned.
*/
cwd?: string,
prefixColor?: string,
}

export interface CloseEvent {
command: CommandInfo;

/**
* The command's index among all commands ran.
*/
index: number,

/**
* Whether the command exited because it was killed.
*/
killed: boolean;

/**
* The exit code or signal for the command.
*/
exitCode: string | number;
timings: {
startDate: Date,
Expand All @@ -29,8 +59,19 @@ export interface TimerEvent {
endDate?: Date;
}

/**
* Subtype of NodeJS's child_process including only what's actually needed for a command to work.
*/
export type ChildProcess = EventEmitter & Pick<BaseChildProcess, 'pid' | 'stdin' | 'stdout' | 'stderr'>;
export type KillProcess = (pid: number, signal?: string | number) => void;

/**
* Interface for a function that must kill the process with `pid`, optionally sending `signal` to it.
*/
export type KillProcess = (pid: number, signal?: string) => void;

/**
* Interface for a function that spawns a command and returns its child process instance.
*/
export type SpawnCommand = (command: string, options: SpawnOptions) => ChildProcess;

export class Command implements CommandInfo {
Expand Down Expand Up @@ -87,6 +128,9 @@ export class Command implements CommandInfo {
this.spawnOpts = spawnOpts;
}

/**
* Starts this command, piping output, error and close events onto the corresponding observables.
*/
start() {
const child = this.spawn(this.command, this.spawnOpts);
this.process = child;
Expand Down Expand Up @@ -125,14 +169,20 @@ export class Command implements CommandInfo {
this.stdin = child.stdin;
}

kill(code?: string | number) {
/**
* Kills this command, optionally specifying a signal to send to it.
*/
kill(code?: string) {
if (this.killable) {
this.killed = true;
this.killProcess(this.pid, code);
}
}
};

/**
* Pipes all events emitted by `stream` into `subject`.
*/
function pipeTo<T>(stream: Rx.Observable<T>, subject: Rx.Subject<T>) {
stream.subscribe(event => subject.next(event));
}
27 changes: 26 additions & 1 deletion src/completion-listener.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
import * as Rx from 'rxjs';
import { bufferCount, switchMap, take } from 'rxjs/operators';
import { CloseEvent, Command } from './command';
import { Command } from './command';

/**
* Defines which command(s) in a list must exit successfully (with an exit code of `0`):
*
* - `first`: only the first specified command;
* - `last`: only the last specified command;
* - `all`: all commands.
*/
export type SuccessCondition = 'first' | 'last' | 'all';

/**
* Provides logic to determine whether lists of commands ran successfully.
*/
export class CompletionListener {
private readonly successCondition: SuccessCondition;
private readonly scheduler?: Rx.SchedulerLike;

constructor({ successCondition = 'all', scheduler }: {
/**
* How this instance will define that a list of commands ran successfully.
* Defaults to `all`.
*
* @see {SuccessCondition}
*/
successCondition?: SuccessCondition,

/**
* For testing only.
*/
scheduler?: Rx.SchedulerLike,
}) {
this.successCondition = successCondition;
Expand All @@ -31,6 +51,11 @@ export class CompletionListener {
}
}

/**
* Given a list of commands, wait for all of them to exit and then evaluate their exit codes.
*
* @returns A Promise that resolves if the success condition is met, or rejects otherwise.
*/
listen(commands: Command[]): Promise<unknown> {
const closeStreams = commands.map(command => command.close);
return Rx.merge(...closeStreams)
Expand Down
53 changes: 53 additions & 0 deletions src/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,74 @@ const defaults: ConcurrentlyOptions = {
cwd: undefined,
};

/**
* A command that is to be passed into `concurrently()`.
* If value is a string, then that's the command's command line.
* Fine grained options can be defined by using the object format.
*/
export type ConcurrentlyCommandInput = string | Partial<CommandInfo>;

export type ConcurrentlyOptions = {
logger?: Logger,

/**
* Which stream should the commands output be written to.
*/
outputStream?: Writable,
group?: boolean,
prefixColors?: string[],

/**
* Maximum number of commands to run at once.
*
* If undefined, then all processes will start in parallel.
* Setting this value to 1 will achieve sequential running.
*/
maxProcesses?: number,

/**
* Whether commands should be spawned in raw mode.
* Defaults to false.
*/
raw?: boolean,

/**
* The current working directory of commands which didn't specify one.
* Defaults to `process.cwd()`.
*/
cwd?: string,

/**
* @see CompletionListener
*/
successCondition?: SuccessCondition,

/**
* Which flow controllers should be applied on commands spawned by concurrently.
* Defaults to an empty array.
*/
controllers: FlowController[],

/**
* A function that will spawn commands.
* Defaults to the `spawn-command` module.
*/
spawn: SpawnCommand,

/**
* A function that will kill processes.
* Defaults to the `tree-kill` module.
*/
kill: KillProcess,
};

/**
* Core concurrently functionality -- spawns the given commands concurrently and
* returns a promise that will await for them to finish.
*
* @see CompletionListener
* @returns A promise that resolves when the commands ran successfully, or rejects otherwise.
*/
export function concurrently(baseCommands: ConcurrentlyCommandInput[], baseOptions?: Partial<ConcurrentlyOptions>) {
assert.ok(Array.isArray(baseCommands), '[concurrently] commands should be an array');
assert.notStrictEqual(baseCommands.length, 0, '[concurrently] no commands provided');
Expand Down
6 changes: 6 additions & 0 deletions src/flow-control/flow-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Command } from '../command';

/**
* Interface for a class that controls and/or watches the behavior of commands.
*
* This may include logging their output, creating interactions between them, or changing when they
* actually finish.
*/
export interface FlowController {
handle(commands: Command[]): { commands: Command[], onFinish?: () => void };
}
9 changes: 9 additions & 0 deletions src/flow-control/input-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import * as defaults from '../defaults';
import { Logger } from '../logger';
import { FlowController } from './flow-controller';

/**
* Sends input from concurrently through to commands.
*
* Input can start with a command identifier, in which case it will be sent to that specific command.
* For instance, `0:bla` will send `bla` to command at index `0`, and `server:stop` will send `stop`
* to command with name `server`.
*
* If the input doesn't start with a command identifier, it is then always sent to the default target.
*/
export class InputHandler implements FlowController {
private readonly logger: Logger;
private readonly defaultInputTarget: CommandIdentifier;
Expand Down
4 changes: 4 additions & 0 deletions src/flow-control/kill-on-signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { map } from 'rxjs/operators';
import { Command } from '../command';
import { FlowController } from './flow-controller';

/**
* Watches the main concurrently process for signals and sends the same signal down to each spawned
* command.
*/
export class KillOnSignal implements FlowController {
private readonly process: EventEmitter;

Expand Down
3 changes: 3 additions & 0 deletions src/flow-control/kill-others.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { filter, map } from 'rxjs/operators';

export type ProcessCloseCondition = 'failure' | 'success';

/**
* Sends a SIGTERM signal to all commands when one of the exits with a matching condition.
*/
export class KillOthers implements FlowController {
private readonly logger: Logger;
private readonly conditions: ProcessCloseCondition[];
Expand Down
3 changes: 3 additions & 0 deletions src/flow-control/log-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Command } from '../command';
import { Logger } from '../logger';
import { FlowController } from './flow-controller';

/**
* Logs when commands failed executing, e.g. due to the executable not existing in the system.
*/
export class LogError implements FlowController {
private readonly logger: Logger;

Expand Down
3 changes: 3 additions & 0 deletions src/flow-control/log-exit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Command } from '../command';
import { Logger } from '../logger';
import { FlowController } from './flow-controller';

/**
* Logs the exit code/signal of commands.
*/
export class LogExit implements FlowController {
private readonly logger: Logger;

Expand Down
3 changes: 3 additions & 0 deletions src/flow-control/log-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Command } from '../command';
import { Logger } from '../logger';
import { FlowController } from './flow-controller';

/**
* Logs the stdout and stderr output of commands.
*/
export class LogOutput implements FlowController {
private readonly logger: Logger;
constructor({ logger }: { logger: Logger }) {
Expand Down
4 changes: 4 additions & 0 deletions src/flow-control/log-timings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface TimingInfo {
killed: boolean,
command: string,
}

/**
* Logs timing information about commands as they start/stop and then a summary when all commands finish.
*/
export class LogTimings implements FlowController {
static mapCloseEventToTimingInfo({ command, timings, killed, exitCode }: CloseEvent): TimingInfo {
const readableDurationMs = (timings.endDate.getTime() - timings.startDate.getTime()).toLocaleString();
Expand Down
3 changes: 3 additions & 0 deletions src/flow-control/restart-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import * as defaults from '../defaults';
import { Logger } from '../logger';
import { FlowController } from './flow-controller';

/**
* Restarts commands that fail up to a defined number of times.
*/
export class RestartProcess implements FlowController {
private readonly logger: Logger;
private readonly scheduler?: Rx.SchedulerLike;
Expand Down
42 changes: 42 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,31 @@ import { Logger } from './logger';

export type ConcurrentlyOptions = BaseConcurrentlyOptions & {
// Logger options
/**
* Which command(s) should have their output hidden.
*/
hide?: CommandIdentifier | CommandIdentifier[],

/**
* The prefix format to use when logging a command's output.
* Defaults to the command's index.
*/
prefix?: string,

/**
* How many characters should a prefix have at most, used when the prefix format is `command`.
*/
prefixLength?: number,

/**
* Whether output should be formatted to include prefixes and whether "event" logs will be logged.
*/
raw?: boolean,

/**
* Date format used when logging date/time.
* @see https://date-fns.org/v2.0.1/docs/format
*/
timestampFormat?: string,

// Input handling options
Expand All @@ -27,13 +48,34 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & {
pauseInputStreamOnFinish?: boolean,

// Restarting options
/**
* How much time in milliseconds to wait before restarting a command.
*
* @see RestartProcess
*/
restartDelay?: number,

/**
* How many times commands should be restarted when they exit with a failure.
*
* @see RestartProcess
*/
restartTries?: number,

// Process killing options
/**
* Under which condition(s) should other commands be killed when the first one exits.
*
* @see KillOthers
*/
killOthers?: ProcessCloseCondition | ProcessCloseCondition[],

// Timing options
/**
* Whether to output timing information for processes.
*
* @see LogTimings
*/
timings?: boolean,
};

Expand Down
Loading

0 comments on commit 44a3747

Please sign in to comment.