Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 18 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface CommonOutputApi {
get pid(): number | undefined;
get killed(): boolean;
get exitCode(): number | undefined;
get signalCode(): string | null;
}

export interface OutputApi extends AsyncIterable<string>, CommonOutputApi {
Expand Down Expand Up @@ -149,6 +150,10 @@ export class ExecProcess implements Result {
return undefined;
}

public get signalCode(): string | null {
return this._process?.signalCode ?? null;
}

public constructor(
command: string,
args?: readonly string[],
Expand Down Expand Up @@ -224,8 +229,8 @@ export class ExecProcess implements Result {

if (
this._options?.throwOnError &&
this.exitCode !== 0 &&
this.exitCode !== undefined
((this.exitCode !== 0 && this.exitCode !== undefined) ||
this.signalCode != null)
) {
throw new NonZeroExitError(this);
}
Expand Down Expand Up @@ -265,8 +270,8 @@ export class ExecProcess implements Result {

if (
this._options.throwOnError &&
this.exitCode !== 0 &&
this.exitCode !== undefined
((this.exitCode !== 0 && this.exitCode !== undefined) ||
this.signalCode != null)
) {
throw new NonZeroExitError(this, result);
}
Expand Down Expand Up @@ -406,14 +411,18 @@ export function xSync(
const stdout = spawnResult.stdout?.toString() ?? '';
const stderr = spawnResult.stderr?.toString() ?? '';
const exitCode = spawnResult.status ?? undefined;
const killed = spawnResult.signal != null;
const signalCode = spawnResult.signal ?? null;
const killed = signalCode != null;

const result: SyncResult = {
stdout,
stderr,
get exitCode() {
return exitCode;
},
get signalCode() {
return signalCode;
},
get pid() {
return spawnResult.pid;
},
Expand All @@ -430,7 +439,10 @@ export function xSync(
}
};

if (opts.throwOnError && exitCode !== 0 && exitCode !== undefined) {
if (
opts.throwOnError &&
((exitCode !== 0 && exitCode !== undefined) || signalCode != null)
) {
throw new NonZeroExitError(result, result);
}

Expand Down
10 changes: 9 additions & 1 deletion src/non-zero-exit-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ export class NonZeroExitError extends Error {
return undefined;
}

public get signalCode(): string | null {
return this.result.signalCode;
}

public constructor(
public readonly result: CommonOutputApi,
public readonly output?: Output
) {
super(`Process exited with non-zero status (${result.exitCode})`);
super(
result.signalCode != null
? `Process was killed with signal ${result.signalCode}`
: `Process exited with non-zero status (${result.exitCode})`
);
}
}
34 changes: 34 additions & 0 deletions src/test/main_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,32 @@ describe('exec (async)', () => {
expect(proc.exitCode).toBe(1);
});

test('throws when throwOnError=true and process is killed by signal', async () => {
const proc = x(
'node',
['-e', 'process.kill(process.pid, "SIGTERM")'],
{throwOnError: true}
);
const err = await proc.then(() => null).catch((e: unknown) => e);
expect(err).toBeInstanceOf(NonZeroExitError);
expect((err as NonZeroExitError).signalCode).toBe('SIGTERM');
expect(proc.exitCode).toBeUndefined();
expect(proc.signalCode).toBe('SIGTERM');
});

test('async iterator throws when throwOnError=true and process is killed by signal', async () => {
const proc = x(
'node',
['-e', 'process.kill(process.pid, "SIGTERM")'],
{throwOnError: true}
);
await expect(async () => {
for await (const _line of proc) {
// consume iterator
}
}).rejects.toThrow(NonZeroExitError);
});

test('supports stdin passed as a string', async () => {
let result = await x('node', ['-e', 'process.stdin.pipe(process.stdout)'], {
stdin: 'foo\nbar'
Expand Down Expand Up @@ -140,6 +166,14 @@ describe('exec (sync)', () => {
xSync('node', ['-e', 'process.exit(1);'], {throwOnError: true});
}).toThrow(NonZeroExitError);
});

test('throws when throwOnError=true and process is killed by signal', () => {
expect(() => {
xSync('node', ['-e', 'process.kill(process.pid, "SIGTERM")'], {
throwOnError: true
});
}).toThrow(NonZeroExitError);
});
});

if (isWindows) {
Expand Down