Skip to content

Commit 7870528

Browse files
authored
Functions breakpoint debugging (#1798)
1 parent e6bf37e commit 7870528

12 files changed

+393
-60
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
* Clean up extraneous error messages in extensions commands.
1+
* Clean up extraneous error messages in extensions commands.
2+
* Adds breakpoint debugging for the Cloud Functions emulator using the `--inspect-functions` flag (#1360).

src/commands/emulators-exec.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as controller from "../emulator/controller";
1111
import { DatabaseEmulator } from "../emulator/databaseEmulator";
1212
import { EmulatorRegistry } from "../emulator/registry";
1313
import { FirestoreEmulator } from "../emulator/firestoreEmulator";
14-
import { beforeEmulatorCommand } from "../emulator/commandUtils";
14+
import * as commandUtils from "../emulator/commandUtils";
1515

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

7979
module.exports = new Command("emulators:exec <script>")
80-
.before(beforeEmulatorCommand)
80+
.before(commandUtils.beforeEmulatorCommand)
8181
.description(
8282
"start the local Firebase emulators, " + "run a test script, then shut down the emulators"
8383
)
84-
.option(
85-
"--only <list>",
86-
"only run specific emulators. " +
87-
"This is a comma separated list of emulators to start. " +
88-
"Valid options are: " +
89-
JSON.stringify(controller.VALID_EMULATOR_STRINGS)
90-
)
84+
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
85+
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
9186
.action(async (script: string, options: any) => {
9287
let exitCode = 0;
9388
try {

src/commands/emulators-start.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import { Command } from "../command";
22
import * as controller from "../emulator/controller";
3-
import { beforeEmulatorCommand } from "../emulator/commandUtils";
3+
import * as commandUtils from "../emulator/commandUtils";
44
import * as utils from "../utils";
55

66
module.exports = new Command("emulators:start")
7-
.before(beforeEmulatorCommand)
7+
.before(commandUtils.beforeEmulatorCommand)
88
.description("start the local Firebase emulators")
9-
.option(
10-
"--only <list>",
11-
"only run specific emulators. " +
12-
"This is a comma separated list of emulators to start. " +
13-
"Valid options are: " +
14-
JSON.stringify(controller.VALID_EMULATOR_STRINGS)
15-
)
9+
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
10+
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
1611
.action(async (options: any) => {
1712
try {
1813
await controller.startAll(options);

src/emulator/commandUtils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import requireAuth = require("../requireAuth");
55
import requireConfig = require("../requireConfig");
66
import { Emulators } from "../emulator/types";
77

8+
export const FLAG_ONLY: string = "--only <emulators>";
9+
export const DESC_ONLY: string =
10+
"only run specific emulators. " +
11+
"This is a comma separated list of emulators to start. " +
12+
"Valid options are: " +
13+
JSON.stringify(controller.VALID_EMULATOR_STRINGS);
14+
15+
export const FLAG_INSPECT_FUNCTIONS = "--inspect-functions [port]";
16+
export const DESC_INSPECT_FUNCTIONS =
17+
"emulate Cloud Functions in debug mode with the node inspector on the given port (9229 if not specified)";
18+
819
/**
920
* We want to be able to run the Firestore and Database emulators even in the absence
1021
* of firebase.json. For Functions and Hosting we require the JSON file since the

src/emulator/controller.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,36 @@ export async function startAll(options: any): Promise<void> {
132132
options.config.get("functions.source")
133133
);
134134

135+
let inspectFunctions: number | undefined;
136+
137+
// If the flag is provided without a value, use the Node.js default
138+
if (options.inspectFunctions === true) {
139+
options.inspectFunctions = "9229";
140+
}
141+
142+
if (options.inspectFunctions) {
143+
inspectFunctions = Number(options.inspectFunctions);
144+
if (isNaN(inspectFunctions) || inspectFunctions < 1024 || inspectFunctions > 65535) {
145+
throw new FirebaseError(
146+
`"${
147+
options.inspectFunctions
148+
}" is not a valid value for the --inspect-functions flag, please pass an integer between 1024 and 65535.`
149+
);
150+
}
151+
152+
// TODO(samstern): Add a link to documentation
153+
utils.logLabeledWarning(
154+
"functions",
155+
`You are running the functions emulator in debug mode (port=${inspectFunctions}). This means that functions will execute in sequence rather than in parallel.`
156+
);
157+
}
158+
135159
const functionsEmulator = new FunctionsEmulator({
136160
projectId,
137161
functionsDir,
138162
host: functionsAddr.host,
139163
port: functionsAddr.port,
164+
debugPort: inspectFunctions,
140165
});
141166
await startEmulator(functionsEmulator);
142167
}

src/emulator/functionsEmulator.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import * as http from "http";
88
import * as logger from "../logger";
99
import * as track from "../track";
1010
import { Constants } from "./constants";
11-
import { EmulatorInfo, EmulatorInstance, EmulatorLog, Emulators } from "./types";
11+
import {
12+
EmulatorInfo,
13+
EmulatorInstance,
14+
EmulatorLog,
15+
Emulators,
16+
FunctionsExecutionMode,
17+
} from "./types";
1218
import * as chokidar from "chokidar";
1319

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

3340
const EVENT_INVOKE = "functions:invoke";
3441

@@ -47,6 +54,7 @@ export interface FunctionsEmulatorArgs {
4754
host?: string;
4855
quiet?: boolean;
4956
disabledRuntimeFeatures?: FunctionsRuntimeFeatures;
57+
debugPort?: number;
5058
}
5159

5260
// FunctionsRuntimeInstance is the handler for a running function invocation
@@ -99,14 +107,26 @@ export class FunctionsEmulator implements EmulatorInstance {
99107
private server?: http.Server;
100108
private triggers: EmulatedTriggerDefinition[] = [];
101109
private knownTriggerIDs: { [triggerId: string]: boolean } = {};
102-
private workerPool: RuntimeWorkerPool = new RuntimeWorkerPool();
110+
111+
private workerPool: RuntimeWorkerPool;
112+
private workQueue: WorkQueue;
103113

104114
constructor(private args: FunctionsEmulatorArgs) {
105115
// TODO: Would prefer not to have static state but here we are!
106116
EmulatorLogger.verbosity = this.args.quiet ? Verbosity.QUIET : Verbosity.DEBUG;
117+
118+
const mode = this.args.debugPort
119+
? FunctionsExecutionMode.SEQUENTIAL
120+
: FunctionsExecutionMode.AUTO;
121+
this.workerPool = new RuntimeWorkerPool(mode);
122+
this.workQueue = new WorkQueue(mode);
107123
}
108124

109125
createHubServer(): express.Application {
126+
// TODO(samstern): Should not need this here but some tests are directly calling this method
127+
// because FunctionsEmulator.start() is not test-safe due to askInstallNodeVersion.
128+
this.workQueue.start();
129+
110130
const hub = express();
111131

112132
hub.use((req, res, next) => {
@@ -140,14 +160,18 @@ export class FunctionsEmulator implements EmulatorInstance {
140160
req: express.Request,
141161
res: express.Response
142162
) => {
143-
this.handleBackgroundTrigger(req, res);
163+
this.workQueue.submit(() => {
164+
return this.handleBackgroundTrigger(req, res);
165+
});
144166
};
145167

146168
const httpsHandler: express.RequestHandler = async (
147169
req: express.Request,
148170
res: express.Response
149171
) => {
150-
this.handleHttpsTrigger(req, res);
172+
this.workQueue.submit(() => {
173+
return this.handleHttpsTrigger(req, res);
174+
});
151175
};
152176

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

@@ -315,6 +340,7 @@ export class FunctionsEmulator implements EmulatorInstance {
315340
}
316341

317342
async stop(): Promise<void> {
343+
this.workQueue.stop();
318344
this.workerPool.exit();
319345
Promise.resolve(this.server && this.server.close());
320346
}
@@ -566,6 +592,10 @@ export class FunctionsEmulator implements EmulatorInstance {
566592
args.unshift("--no-warnings");
567593
}
568594

595+
if (this.args.debugPort) {
596+
args.unshift(`--inspect=${this.args.debugPort}`);
597+
}
598+
569599
const childProcess = spawn(opts.nodeBinary, args, {
570600
env: { node: opts.nodeBinary, ...opts.env, ...process.env },
571601
cwd: frb.cwd,

src/emulator/functionsEmulatorRuntime.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,11 @@ async function flushAndExit(code: number) {
10951095
process.exit(code);
10961096
}
10971097

1098+
async function goIdle() {
1099+
new EmulatorLog("SYSTEM", "runtime-status", "Runtime is now idle", { state: "idle" }).log();
1100+
await EmulatorLog.waitForFlush();
1101+
}
1102+
10981103
async function handleMessage(message: string) {
10991104
let runtimeArgs: FunctionsRuntimeArgs;
11001105
try {
@@ -1116,9 +1121,9 @@ async function handleMessage(message: string) {
11161121
return;
11171122
}
11181123

1119-
// If there's no trigger id it's just a diagnostic call. We throw away the runtime.
1124+
// If there's no trigger id it's just a diagnostic call. We can go idle right away.
11201125
if (!runtimeArgs.frb.triggerId) {
1121-
await flushAndExit(0);
1126+
await goIdle();
11221127
return;
11231128
}
11241129

@@ -1140,8 +1145,7 @@ async function handleMessage(message: string) {
11401145
if (runtimeArgs.opts && runtimeArgs.opts.serializedTriggers) {
11411146
await flushAndExit(0);
11421147
} else {
1143-
new EmulatorLog("SYSTEM", "runtime-status", "Runtime is now idle", { state: "idle" }).log();
1144-
await EmulatorLog.waitForFlush();
1148+
await goIdle();
11451149
}
11461150
} catch (err) {
11471151
new EmulatorLog("FATAL", "runtime-error", err.stack ? err.stack : err).log();

0 commit comments

Comments
 (0)