Skip to content

Commit 7540058

Browse files
evangraykfacebook-github-bot
authored andcommitted
Update EjecaError to be a custom type with custom toString
Summary: We were creating `EjecaError`s via `new Error` and a type cast, and assigning extra properties. But this means `.toString` would not print the additional fields like exit code and stdout. This makes it hard to debug issues, as we don't see stdout/stderr in the logs (though users should still see in their output UI). This error format might be visible in the UI for certain errors, though I think that's fine? I also noticed the EjecaError type is missing `code`, but some callers still check that. We should fix that too (not in this diff) Reviewed By: muirdm Differential Revision: D67994455 fbshipit-source-id: f7b2933187c17800037716609120f76417f2cdb6
1 parent 4c783ee commit 7540058

File tree

4 files changed

+38
-21
lines changed

4 files changed

+38
-21
lines changed

addons/isl-server/src/github/queryGraphQL.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default async function queryGraphQL<TData, TVariables>(
5353
return json.data;
5454
} catch (error: unknown) {
5555
if (isEjecaError(error)) {
56+
// FIXME: we're never setting `code` in ejeca, so this is always false!
5657
if (error.code === 'ENOENT' || error.code === 'EACCES') {
5758
// `gh` not installed on path
5859
throw new Error(`GhNotInstalledError: ${(error as Error).stack}`);

addons/isl-server/src/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ export function parseExecJson<T>(
164164
});
165165
}
166166

167+
// FIXME: we're not ever setting `code`!
167168
export function isEjecaError(s: unknown): s is EjecaError & {code?: string} {
168169
return s != null && typeof s === 'object' && 'exitCode' in s;
169170
}

addons/isl/integrationTests/ejeca.test.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
*/
77

88
import type {EjecaChildProcess} from 'shared/ejeca';
9+
910
import {ejeca} from 'shared/ejeca';
1011

1112
describe('test running binaries', () => {
1213
it('we can get both streams and awaitables', async () => {
13-
let spawned = ejeca('node', ['-e', "console.log('uno') ; console.error('dos')"]);
14+
const spawned = ejeca('node', ['-e', "console.log('uno') ; console.error('dos')"]);
1415
let streamOut = '';
1516
let streamErr = '';
1617
spawned.stdout?.on('data', data => {
@@ -33,11 +34,8 @@ describe('test running binaries', () => {
3334

3435
it('when erroring out the command name is present', async () => {
3536
const spawned = ejeca('node', ['-', 'foo("bar")'], {input: 'babar'});
36-
await expect(spawned).rejects.toThrow(
37-
expect.objectContaining({
38-
escapedCommand: 'node "-" "foo(\\"bar\\")"',
39-
message: 'Command `node "-" "foo(\\"bar\\")"` exited with non-zero status',
40-
}),
37+
await expect(spawned).rejects.toThrowErrorMatchingInlineSnapshot(
38+
`"Command \`node "-" "foo(\\"bar\\")"\` exited with non-zero status with exit code 1"`,
4139
);
4240
});
4341

@@ -120,7 +118,7 @@ console.log("Goodbye");
120118
killArgs: Parameters<EjecaChildProcess['kill']> = [],
121119
expectedSignal?: string,
122120
) => {
123-
let spawned = ejeca('node', ['-', ...pythonArgs], {
121+
const spawned = ejeca('node', ['-', ...pythonArgs], {
124122
input: sighandlerScript,
125123
});
126124
setTimeout(() => spawned.kill(...killArgs), 1000);

addons/shared/ejeca.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type {ChildProcess, IOType, SpawnOptions, Serializable} from 'node:child_process';
99
import type {Stream} from 'node:stream';
1010

11+
import {truncate} from './utils';
1112
import getStream from 'get-stream';
1213
import {spawn} from 'node:child_process';
1314
import {Readable} from 'node:stream';
@@ -171,8 +172,6 @@ export interface EjecaReturn {
171172
killed: boolean;
172173
}
173174

174-
export interface EjecaError extends Error, EjecaReturn {}
175-
176175
interface EjecaChildPromise {
177176
catch<ResultType = never>(
178177
onRejected?: (reason: EjecaError) => ResultType | PromiseLike<ResultType>,
@@ -244,7 +243,7 @@ function getSpawnedPromise(
244243
values => {
245244
const [{exitCode, signal}, stdout, stderr] = values;
246245
const stripfinalNl = options?.stripFinalNewline ?? true;
247-
const ret = {
246+
const ret: EjecaReturn = {
248247
exitCode,
249248
signal,
250249
stdout: maybeStripFinalNewline(stdout, stripfinalNl),
@@ -253,23 +252,41 @@ function getSpawnedPromise(
253252
escapedCommand,
254253
};
255254
if (exitCode !== 0 || signal != undefined) {
256-
const err = new Error(
257-
`Command \`${ret.escapedCommand.slice(0, 50)}\` ` +
258-
(signal != null ? 'was killed' : 'exited with non-zero status'),
259-
) as unknown as EjecaError;
260-
err.exitCode = ret.exitCode;
261-
err.signal = ret.signal;
262-
err.stdout = ret.stdout;
263-
err.stderr = ret.stderr;
264-
err.killed = ret.killed;
265-
err.escapedCommand = ret.escapedCommand;
266-
throw err;
255+
throw new EjecaError(ret);
267256
}
268257
return ret;
269258
},
270259
);
271260
}
272261

262+
export class EjecaError extends Error implements EjecaReturn {
263+
escapedCommand: string;
264+
exitCode: number;
265+
signal?: string;
266+
stdout: string;
267+
stderr: string;
268+
killed: boolean;
269+
270+
constructor(info: EjecaReturn) {
271+
const message =
272+
`Command \`${truncate(info.escapedCommand, 50)}\` ` +
273+
(info.signal != null ? 'was killed' : 'exited with non-zero status') +
274+
(info.signal != null ? ` with signal ${info.signal}` : ` with exit code ${info.exitCode}`);
275+
super(message);
276+
277+
this.exitCode = info.exitCode;
278+
this.signal = info.signal;
279+
this.stdout = info.stdout;
280+
this.stderr = info.stderr;
281+
this.killed = info.killed;
282+
this.escapedCommand = info.escapedCommand;
283+
}
284+
285+
toString() {
286+
return `${this.message}\n${JSON.stringify(this, undefined, 2)}\n`;
287+
}
288+
}
289+
273290
function getStreamPromise(origStream: Stream | null): Promise<string> {
274291
const stream = origStream ?? new Readable({read() {}});
275292
return getStream(stream, {encoding: 'utf8'});

0 commit comments

Comments
 (0)