Skip to content

Functions breakpoint debugging #1798

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

Merged
merged 21 commits into from
Dec 12, 2019
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 +1,2 @@
* Clean up extraneous error messages in extensions commands.
* Clean up extraneous error messages in extensions commands.
* Adds breakpoint debugging for the Cloud Functions emulator using the `--inspect-functions` flag (#1360).
13 changes: 4 additions & 9 deletions src/commands/emulators-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as controller from "../emulator/controller";
import { DatabaseEmulator } from "../emulator/databaseEmulator";
import { EmulatorRegistry } from "../emulator/registry";
import { FirestoreEmulator } from "../emulator/firestoreEmulator";
import { beforeEmulatorCommand } from "../emulator/commandUtils";
import * as commandUtils from "../emulator/commandUtils";

async function runScript(script: string): Promise<number> {
utils.logBullet(`Running script: ${clc.bold(script)}`);
Expand Down Expand Up @@ -77,17 +77,12 @@ async function runScript(script: string): Promise<number> {
}

module.exports = new Command("emulators:exec <script>")
.before(beforeEmulatorCommand)
.before(commandUtils.beforeEmulatorCommand)
.description(
"start the local Firebase emulators, " + "run a test script, then shut down the emulators"
)
.option(
"--only <list>",
"only run specific emulators. " +
"This is a comma separated list of emulators to start. " +
"Valid options are: " +
JSON.stringify(controller.VALID_EMULATOR_STRINGS)
)
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
.action(async (script: string, options: any) => {
let exitCode = 0;
try {
Expand Down
13 changes: 4 additions & 9 deletions src/commands/emulators-start.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { Command } from "../command";
import * as controller from "../emulator/controller";
import { beforeEmulatorCommand } from "../emulator/commandUtils";
import * as commandUtils from "../emulator/commandUtils";
import * as utils from "../utils";

module.exports = new Command("emulators:start")
.before(beforeEmulatorCommand)
.before(commandUtils.beforeEmulatorCommand)
.description("start the local Firebase emulators")
.option(
"--only <list>",
"only run specific emulators. " +
"This is a comma separated list of emulators to start. " +
"Valid options are: " +
JSON.stringify(controller.VALID_EMULATOR_STRINGS)
)
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
.action(async (options: any) => {
try {
await controller.startAll(options);
Expand Down
11 changes: 11 additions & 0 deletions src/emulator/commandUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ import requireAuth = require("../requireAuth");
import requireConfig = require("../requireConfig");
import { Emulators } from "../emulator/types";

export const FLAG_ONLY: string = "--only <emulators>";
export const DESC_ONLY: string =
"only run specific emulators. " +
"This is a comma separated list of emulators to start. " +
"Valid options are: " +
JSON.stringify(controller.VALID_EMULATOR_STRINGS);

export const FLAG_INSPECT_FUNCTIONS = "--inspect-functions [port]";
export const DESC_INSPECT_FUNCTIONS =
"emulate Cloud Functions in debug mode with the node inspector on the given port (9229 if not specified)";

/**
* We want to be able to run the Firestore and Database emulators even in the absence
* of firebase.json. For Functions and Hosting we require the JSON file since the
Expand Down
25 changes: 25 additions & 0 deletions src/emulator/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,36 @@ export async function startAll(options: any): Promise<void> {
options.config.get("functions.source")
);

let inspectFunctions: number | undefined;

// If the flag is provided without a value, use the Node.js default
if (options.inspectFunctions === true) {
options.inspectFunctions = "9229";
}

if (options.inspectFunctions) {
inspectFunctions = Number(options.inspectFunctions);
if (isNaN(inspectFunctions) || inspectFunctions < 1024 || inspectFunctions > 65535) {
throw new FirebaseError(
`"${
options.inspectFunctions
}" is not a valid value for the --inspect-functions flag, please pass an integer between 1024 and 65535.`
);
}

// TODO(samstern): Add a link to documentation
utils.logLabeledWarning(
"functions",
`You are running the functions emulator in debug mode (port=${inspectFunctions}). This means that functions will execute in sequence rather than in parallel.`
);
}

const functionsEmulator = new FunctionsEmulator({
projectId,
functionsDir,
host: functionsAddr.host,
port: functionsAddr.port,
debugPort: inspectFunctions,
});
await startEmulator(functionsEmulator);
}
Expand Down
38 changes: 34 additions & 4 deletions src/emulator/functionsEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import * as http from "http";
import * as logger from "../logger";
import * as track from "../track";
import { Constants } from "./constants";
import { EmulatorInfo, EmulatorInstance, EmulatorLog, Emulators } from "./types";
import {
EmulatorInfo,
EmulatorInstance,
EmulatorLog,
Emulators,
FunctionsExecutionMode,
} from "./types";
import * as chokidar from "chokidar";

import * as spawn from "cross-spawn";
Expand All @@ -29,6 +35,7 @@ import { EmulatorLogger, Verbosity } from "./emulatorLogger";
import { RuntimeWorkerPool, RuntimeWorker } from "./functionsRuntimeWorker";
import { PubsubEmulator } from "./pubsubEmulator";
import { FirebaseError } from "../error";
import { WorkQueue } from "./workQueue";

const EVENT_INVOKE = "functions:invoke";

Expand All @@ -47,6 +54,7 @@ export interface FunctionsEmulatorArgs {
host?: string;
quiet?: boolean;
disabledRuntimeFeatures?: FunctionsRuntimeFeatures;
debugPort?: number;
}

// FunctionsRuntimeInstance is the handler for a running function invocation
Expand Down Expand Up @@ -99,14 +107,26 @@ export class FunctionsEmulator implements EmulatorInstance {
private server?: http.Server;
private triggers: EmulatedTriggerDefinition[] = [];
private knownTriggerIDs: { [triggerId: string]: boolean } = {};
private workerPool: RuntimeWorkerPool = new RuntimeWorkerPool();

private workerPool: RuntimeWorkerPool;
private workQueue: WorkQueue;

constructor(private args: FunctionsEmulatorArgs) {
// TODO: Would prefer not to have static state but here we are!
EmulatorLogger.verbosity = this.args.quiet ? Verbosity.QUIET : Verbosity.DEBUG;

const mode = this.args.debugPort
? FunctionsExecutionMode.SEQUENTIAL
: FunctionsExecutionMode.AUTO;
this.workerPool = new RuntimeWorkerPool(mode);
this.workQueue = new WorkQueue(mode);
}

createHubServer(): express.Application {
// TODO(samstern): Should not need this here but some tests are directly calling this method
// because FunctionsEmulator.start() is not test-safe due to askInstallNodeVersion.
this.workQueue.start();

const hub = express();

hub.use((req, res, next) => {
Expand Down Expand Up @@ -140,14 +160,18 @@ export class FunctionsEmulator implements EmulatorInstance {
req: express.Request,
res: express.Response
) => {
this.handleBackgroundTrigger(req, res);
this.workQueue.submit(() => {
return this.handleBackgroundTrigger(req, res);
});
};

const httpsHandler: express.RequestHandler = async (
req: express.Request,
res: express.Response
) => {
this.handleHttpsTrigger(req, res);
this.workQueue.submit(() => {
return this.handleHttpsTrigger(req, res);
});
};

// The ordering here is important. The longer routes (background)
Expand Down Expand Up @@ -185,6 +209,7 @@ export class FunctionsEmulator implements EmulatorInstance {
async start(): Promise<void> {
this.nodeBinary = await this.askInstallNodeVersion(this.args.functionsDir);
const { host, port } = this.getInfo();
this.workQueue.start();
this.server = this.createHubServer().listen(port, host);
}

Expand Down Expand Up @@ -315,6 +340,7 @@ export class FunctionsEmulator implements EmulatorInstance {
}

async stop(): Promise<void> {
this.workQueue.stop();
this.workerPool.exit();
Promise.resolve(this.server && this.server.close());
}
Expand Down Expand Up @@ -566,6 +592,10 @@ export class FunctionsEmulator implements EmulatorInstance {
args.unshift("--no-warnings");
}

if (this.args.debugPort) {
args.unshift(`--inspect=${this.args.debugPort}`);
}

const childProcess = spawn(opts.nodeBinary, args, {
env: { node: opts.nodeBinary, ...opts.env, ...process.env },
cwd: frb.cwd,
Expand Down
12 changes: 8 additions & 4 deletions src/emulator/functionsEmulatorRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,11 @@ async function flushAndExit(code: number) {
process.exit(code);
}

async function goIdle() {
new EmulatorLog("SYSTEM", "runtime-status", "Runtime is now idle", { state: "idle" }).log();
await EmulatorLog.waitForFlush();
}

async function handleMessage(message: string) {
let runtimeArgs: FunctionsRuntimeArgs;
try {
Expand All @@ -1116,9 +1121,9 @@ async function handleMessage(message: string) {
return;
}

// If there's no trigger id it's just a diagnostic call. We throw away the runtime.
// If there's no trigger id it's just a diagnostic call. We can go idle right away.
if (!runtimeArgs.frb.triggerId) {
await flushAndExit(0);
await goIdle();
return;
}

Expand All @@ -1140,8 +1145,7 @@ async function handleMessage(message: string) {
if (runtimeArgs.opts && runtimeArgs.opts.serializedTriggers) {
await flushAndExit(0);
} else {
new EmulatorLog("SYSTEM", "runtime-status", "Runtime is now idle", { state: "idle" }).log();
await EmulatorLog.waitForFlush();
await goIdle();
}
} catch (err) {
new EmulatorLog("FATAL", "runtime-error", err.stack ? err.stack : err).log();
Expand Down
Loading