Skip to content
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
- Fixed an issue where the Functions emulator replaced an IPC failure with an unrelated `TypeError` about stream chunk types, hiding why the runtime became unreachable (#10876).
- [Added] Add -f, --force option to `firebase ext:migrate`.
- [Fixed] Fix parameter type preservation and optional system parameter handling during extension updates in `firebase ext:migrate`.
- [Fixed] Fix parameter type preservation and optional system parameter handling during extension updates in `firebase ext:migrate`
73 changes: 73 additions & 0 deletions src/emulator/types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect } from "chai";
import * as sinon from "sinon";

import { EmulatorLog } from "./types";

type SendCallback = (err: unknown) => void;

describe("EmulatorLog", () => {
describe("flush()", () => {
let stderrWrite: sinon.SinonStub;
// eslint-disable-next-line @typescript-eslint/unbound-method
const originalSend = process.send;

beforeEach(() => {
// Enforce the same chunk types the real stream does, so a regression throws here too.
stderrWrite = sinon.stub(process.stderr, "write").callsFake((chunk: unknown): boolean => {
if (typeof chunk !== "string" && !ArrayBuffer.isView(chunk)) {
throw new TypeError(
`The "chunk" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ${typeof chunk}`,
);
}
return true;
});
});

afterEach(() => {
sinon.restore();
process.send = originalSend;
});

// Reports `err` to the send callback, the way Node does when the IPC channel has failed.
function stubSend(err: unknown): void {
process.send = ((
_message: unknown,
_sendHandle: unknown,
_options: unknown,
callback: SendCallback,
): boolean => {
callback(err);
return true;
}) as unknown as typeof process.send;
}

it("writes the stack of an Error reported by process.send()", () => {
stubSend(new Error("channel closed"));

expect(() => new EmulatorLog("INFO", "system", "hello").log()).to.not.throw();

expect(stderrWrite.calledOnce).to.be.true;
const written = stderrWrite.firstCall.args[0] as string;
expect(written).to.be.a("string");
expect(written).to.contain("channel closed");
expect(written).to.match(/\n$/);
});

it("writes a non-Error reported by process.send()", () => {
stubSend("ERR_IPC_CHANNEL_CLOSED");

expect(() => new EmulatorLog("INFO", "system", "hello").log()).to.not.throw();

expect(stderrWrite.calledOnce).to.be.true;
expect(stderrWrite.firstCall.args[0]).to.contain("ERR_IPC_CHANNEL_CLOSED");
});

it("writes nothing when process.send() succeeds", () => {
stubSend(null);

new EmulatorLog("INFO", "system", "hello").log();

expect(stderrWrite.called).to.be.false;
});
});
});
13 changes: 9 additions & 4 deletions src/emulator/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ChildProcess } from "child_process";
import { EventEmitter } from "events";

import { getErrStack } from "../error";

export enum Emulators {
AUTH = "auth",
HUB = "hub",
Expand Down Expand Up @@ -357,12 +359,15 @@ export class EmulatorLog {

EmulatorLog.WAITING_FOR_FLUSH = true;
if (process.send) {
// For some reason our node.d.ts file does not include the version of subprocess.send() with a callback
// but the node docs assert that it has an optional callback.
// https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
(process.send as any)(nextMsg, undefined, {}, (err: any) => {
process.send(nextMsg, undefined, {}, (err: Error | null) => {
if (err) {
process.stderr.write(err);
// process.send() hands the callback an Error object, which stream.write()
// rejects -- writing it directly throws and destroys the original error.
process.stderr.write(`${getErrStack(err)}\n`);
// Clear the buffer to prevent flooding stderr with duplicate stack traces
// for subsequent messages when the IPC channel is permanently broken.
EmulatorLog.LOG_BUFFER = [];
}
Comment thread
mattsears18 marked this conversation as resolved.

EmulatorLog.WAITING_FOR_FLUSH = EmulatorLog.LOG_BUFFER.length > 0;
Expand Down