Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(util/exec): use spawn instead of exec #16414

Merged
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9e86048
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
46850eb
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
3ca3961
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
cda3a37
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
c9f0ba5
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
c864b4c
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
e2b6197
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 4, 2022
11ca433
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
b67a072
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
48c24a0
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
dc7c62a
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
19682da
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
93ac364
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
8537123
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
a01d7e3
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 5, 2022
38adab0
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 6, 2022
a7c7b21
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 6, 2022
1bc1319
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 7, 2022
a7ebe9f
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 7, 2022
094308f
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 8, 2022
e5ecca3
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 9, 2022
ee56cae
Merge branch 'main' into 16197-handle-zombies-with-spawn
Gabriel-Ladzaretti Jul 9, 2022
783e0f7
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 11, 2022
7800a77
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 11, 2022
011cdda
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 11, 2022
d58390a
Update lib/util/exec/common.ts
Gabriel-Ladzaretti Jul 11, 2022
8653aeb
Merge branch '16197-handle-zombies-with-spawn' of https://github.com/…
Gabriel-Ladzaretti Jul 11, 2022
46967ae
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 11, 2022
50723e3
Merge branch 'main' into 16197-handle-zombies-with-spawn
Gabriel-Ladzaretti Jul 11, 2022
ee41eab
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 11, 2022
ba6ba93
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 11, 2022
8f9b7b4
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 12, 2022
4832a83
Merge branch 'main' into 16197-handle-zombies-with-spawn
Gabriel-Ladzaretti Jul 12, 2022
930c142
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 12, 2022
05fce92
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 12, 2022
03526d3
Apply suggestions from code review
Gabriel-Ladzaretti Jul 12, 2022
1a68345
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 12, 2022
eed49ec
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 13, 2022
f539d9d
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 15, 2022
d9c4629
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 15, 2022
3fa4f25
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 21, 2022
3496fd1
Merge branch 'main' into 16197-handle-zombies-with-spawn
Gabriel-Ladzaretti Jul 22, 2022
bdaea44
refactor(util): use spawn instead of exec
Gabriel-Ladzaretti Jul 22, 2022
8597d8a
Merge branch 'main' into 16197-handle-zombies-with-spawn
viceice Jul 22, 2022
74d5c1e
Merge branch 'main' into 16197-handle-zombies-with-spawn
viceice Jul 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions lib/util/exec/common.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import type { ChildProcess, SendHandle, Serializable } from 'child_process';
import { Readable } from 'stream';
import { spawn } from '../../../test/exec-util';
import { partial } from '../../../test/util';
import { exec } from './common';
import type { RawExecOptions } from './types';

jest.mock('child_process');

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