Skip to content

Commit

Permalink
feat(util/exec): use spawn instead of exec (#16414)
Browse files Browse the repository at this point in the history
* refactor(util): use spawn instead of exec

 - Use child_process.spawn instead of child_process.exec

* refactor(util): use spawn instead of exec

 - Use child_process.spawn instead of child_process.exec

* refactor(util): use spawn instead of exec

 - Use child_process.spawn instead of child_process.exec

* refactor(util): use spawn instead of exec

 - Use child_process.spawn instead of child_process.exec

* refactor(util): use spawn instead of exec

 - init spawn-util

* refactor(util): use spawn instead of exec

 - spawn-util

* refactor(util): use spawn instead of exec

 - init index-spawn.spec.ts

* refactor(util): use spawn instead of exec

 - fixed various tests

* refactor(util): use spawn instead of exec

 - fix all artifacts.spec.ts

* refactor(util): use spawn instead of exec

 - fix all artifacts.spec.ts

* refactor(util): use spawn instead of exec

 - fix npm post update imports

* refactor(util): use spawn instead of exec

 - revert renaming to minimize PR diff

* refactor(util): use spawn instead of exec

 - revert renaming to minimize PR diff

* refactor(util): use spawn instead of exec

 - revert renaming to minimize PR diff

* refactor(util): use spawn instead of exec

 - revert renaming to minimize PR diff

* refactor(util): use spawn instead of exec

 - revert renaming to minimize PR diff
 - destroy stdio when terminating child process

* refactor(util): use spawn instead of exec

 - delete and revert dev related changes

* refactor(util): use spawn instead of exec
 - fix support for windows

* refactor(util): use spawn instead of exec

 - handle SIGSTOP and such
 - add test coverage

* refactor(util): use spawn instead of exec

 - now converts to strings when resolving/rejecting

* refactor(util): use spawn instead of exec

 - logs improvements
 - force shell (exec like)
 - fix tests

* refactor(util): use spawn instead of exec

 - strongly type listeners

* refactor(util): use spawn instead of exec

 - create helper mock for spawn

* refactor(util): use spawn instead of exec

 - cr changes

* Update lib/util/exec/common.ts

Co-authored-by: Sergei Zharinov <[email protected]>

* refactor(util): use spawn instead of exec

 - documentation

* refactor(util): use spawn instead of exec

 - revert unnecessary formatting

* refactor(util): use spawn instead of exec

* refactor(util): use spawn instead of exec

 - added ExecError class

* refactor(util): use spawn instead of exec

 - exec-error.ts restructure

* refactor(util): use spawn instead of exec

* Apply suggestions from code review

Co-authored-by: Sergei Zharinov <[email protected]>

* refactor(util): use spawn instead of exec

* refactor(util): use spawn instead of exec

* refactor(util): use spawn instead of exec

 - deprecated RawExecOptions.encoding property

* refactor(util): use spawn instead of exec

* refactor(util): use spawn instead of exec

* refactor(util): use spawn instead of exec

Co-authored-by: Sergei Zharinov <[email protected]>
  • Loading branch information
Gabriel-Ladzaretti and zharinov authored Jul 22, 2022
1 parent c2b19d8 commit 892595a
Show file tree
Hide file tree
Showing 5 changed files with 483 additions and 5 deletions.
285 changes: 285 additions & 0 deletions lib/util/exec/common.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import { spawn as _spawn } from 'child_process';
import type { ChildProcess, SendHandle, Serializable } from 'child_process';
import { Readable } from 'stream';
import { mockedFunction, partial } from '../../../test/util';
import { exec } from './common';
import type { RawExecOptions } from './types';

jest.mock('child_process');
const spawn = mockedFunction(_spawn);

type MessageListener = (message: Serializable, sendHandle: SendHandle) => void;
type NoArgListener = () => void;
type EndListener = (code: number | null, signal: NodeJS.Signals | null) => void;
type ErrorListener = (err: Error) => void;

type Listener = MessageListener | NoArgListener | EndListener | ErrorListener;

interface Events {
close?: EndListener;
disconnect?: NoArgListener;
error?: ErrorListener;
exit?: EndListener;
message?: MessageListener;
spawn?: NoArgListener;
}

interface StubArgs {
cmd: string;
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
encoding?: BufferEncoding;
error?: Error;
stdout?: string;
stderr?: string;
timeout?: number;
}

function getReadable(
data: string | undefined,
encoding: BufferEncoding
): Readable {
const readable = new Readable();
readable._read = (size: number): void => {
/*do nothing*/
};

readable.destroy = (error?: Error | undefined): Readable => {
return readable;
};

if (data !== undefined) {
readable.push(data, encoding);
readable.push(null);
}

return readable;
}

function getSpawnStub(args: StubArgs): ChildProcess {
const {
cmd,
error,
exitCode,
exitSignal,
stdout,
stderr,
encoding,
timeout,
} = args;
const listeners: Events = {};

// init listeners
const on = (name: string, cb: Listener) => {
const event = name as keyof Events;
if (listeners[event]) {
return;
}
switch (event) {
case 'exit':
listeners.exit = cb as EndListener;
break;
case 'error':
listeners.error = cb as ErrorListener;
break;
default:
break;
}
};

// init readable streams
const stdoutStream = getReadable(stdout, encoding ?? 'utf8');
const stderrStream = getReadable(stderr, encoding ?? 'utf8');

// define class methods
const emit = (name: string, ...arg: (string | number | Error)[]): boolean => {
const event = name as keyof Events;

switch (event) {
case 'error':
listeners.error?.(arg[0] as Error);
break;
case 'exit':
listeners.exit?.(arg[0] as number, arg[1] as NodeJS.Signals);
break;
default:
break;
}

return !!listeners[event];
};

const unref = (): void => {
/* do nothing*/
};

const kill = (signal?: number | NodeJS.Signals | undefined): boolean => {
/* do nothing*/
return true;
};

// queue events and wait for event loop to clear
setTimeout(() => {
if (error) {
listeners.error?.(error);
}
listeners.exit?.(exitCode, exitSignal);
}, 0);

if (timeout) {
setTimeout(() => {
listeners.exit?.(null, 'SIGTERM');
}, timeout);
}

return {
on,
spawnargs: cmd.split(/\s+/),
stdout: stdoutStream,
stderr: stderrStream,
emit,
unref,
kill,
} as ChildProcess;
}

describe('util/exec/common', () => {
const cmd = 'ls -l';
const stdout = 'out message';
const stderr = 'err message';

beforeEach(() => {
jest.resetAllMocks();
});

describe('rawExec', () => {
it('command exits with code 0', async () => {
const cmd = 'ls -l';
const stub = getSpawnStub({
cmd,
exitCode: 0,
exitSignal: null,
stdout,
stderr,
});
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(
cmd,
partial<RawExecOptions>({ encoding: 'utf8', shell: 'bin/bash' })
)
).resolves.toEqual({
stderr,
stdout,
});
});

it('command exits with code 1', async () => {
const cmd = 'ls -l';
const stderr = 'err';
const exitCode = 1;
const stub = getSpawnStub({ cmd, exitCode, exitSignal: null, stderr });
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(cmd, partial<RawExecOptions>({ encoding: 'utf8' }))
).rejects.toMatchObject({
cmd,
message: 'Process exited with exit code "1"',
exitCode,
stderr,
});
});

it('process terminated with SIGTERM', async () => {
const cmd = 'ls -l';
const exitSignal = 'SIGTERM';
const stub = getSpawnStub({ cmd, exitCode: null, exitSignal });
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(cmd, partial<RawExecOptions>({ encoding: 'utf8' }))
).rejects.toMatchObject({
cmd,
signal: exitSignal,
message: 'Process signaled with "SIGTERM"',
});
});

it('process does nothing when signaled with SIGSTOP and eventually times out', async () => {
const cmd = 'ls -l';
const stub = getSpawnStub({
cmd,
exitCode: null,
exitSignal: 'SIGSTOP',
timeout: 500,
});
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(cmd, partial<RawExecOptions>({ encoding: 'utf8' }))
).toReject();
});

it('process exits due to error', async () => {
const cmd = 'ls -l';
const errMsg = 'error message';
const stub = getSpawnStub({
cmd,
exitCode: null,
exitSignal: null,
error: new Error(errMsg),
});
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(cmd, partial<RawExecOptions>({ encoding: 'utf8' }))
).rejects.toMatchObject({ cmd: 'ls -l', message: 'error message' });
});

it('process exits with error due to exceeded stdout maxBuffer', async () => {
const cmd = 'ls -l';
const stub = getSpawnStub({
cmd,
exitCode: null,
exitSignal: null,
stdout: 'some message',
});
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(
cmd,
partial<RawExecOptions>({
encoding: 'utf8',
maxBuffer: 5,
})
)
).rejects.toMatchObject({
cmd: 'ls -l',
message: 'stdout maxBuffer exceeded',
stderr: '',
stdout: '',
});
});

it('process exits with error due to exceeded stderr maxBuffer', async () => {
const stub = getSpawnStub({
cmd,
exitCode: null,
exitSignal: null,
stderr: 'some message',
});
spawn.mockImplementationOnce((cmd, opts) => stub);
await expect(
exec(
cmd,
partial<RawExecOptions>({
encoding: 'utf8',
maxBuffer: 5,
})
)
).rejects.toMatchObject({
cmd: 'ls -l',
message: 'stderr maxBuffer exceeded',
stderr: '',
stdout: '',
});
});
});
});
Loading

0 comments on commit 892595a

Please sign in to comment.