Skip to content

Commit c48ccfe

Browse files
authored
Improve error messages when functions fails to load (#5782)
We will proactively try to identify common issues based on error messages to provide better debugging experience. 1. No `venv` directory: ``` ➜ functions firebase emulators:start i emulators: Starting emulators: functions i functions: Watching "/Users/REDACTED/google/cf3-pyinit/functions" for Cloud Functions... ⬢ functions: Failed to load function definition from source: FirebaseError: Failed to find location of Firebase Functions SDK: Missing virtual environment at venv directory. Did you forget to run 'python3.11 -m venv venv'? ``` 2. No `firebase-functions` package ``` ➜ functions firebase emulators:start i emulators: Starting emulators: functions i functions: Watching "/Users/REDACTED/google/cf3-pyinit/functions" for Cloud Functions... ⬢ functions: Failed to load function definition from source: FirebaseError: Failed to find location of Firebase Functions SDK. Did you forget to run 'source /Users/danielylee/google/cf3-pyinit/functions/venv/bin/activate && python3.11 -m pip install -r requirements.txt'? ``` 3. Runtime errors ``` ➜ functions firebase emulators:start i emulators: Starting emulators: functions i functions: Watching "/Users/REDACTED/google/cf3-pyinit/functions" for Cloud Functions... ⚠ functions: Failed to detect functions from source FirebaseError: Failed to parse build specification. stderr:WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:8081 Press CTRL+C to quit [2023-05-02 19:29:18,285] ERROR in app: Exception on /__/functions.yaml [GET] Traceback (most recent call last): ... File "/Users/REDACTED/google/cf3-pyinit/functions/main.py", line 8, in <module> foo += 1 ^^^ NameError: name 'foo' is not defined ```
1 parent 19a8384 commit c48ccfe

File tree

3 files changed

+68
-19
lines changed

3 files changed

+68
-19
lines changed

src/deploy/functions/runtimes/discovery/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export async function detectFromPort(
6060
port: number,
6161
project: string,
6262
runtime: runtimes.Runtime,
63-
timeout = 30_000 /* 30s to boot up */
63+
timeout = 10_000 /* 10s to boot up */
6464
): Promise<build.Build> {
6565
// The result type of fetch isn't exported
6666
let res: { text(): Promise<string> };

src/deploy/functions/runtimes/python/index.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import * as runtimes from "..";
99
import * as backend from "../../backend";
1010
import * as discovery from "../discovery";
1111
import { logger } from "../../../../logger";
12-
import { runWithVirtualEnv } from "../../../../functions/python";
12+
import { DEFAULT_VENV_DIR, runWithVirtualEnv, virtualEnvCmd } from "../../../../functions/python";
1313
import { FirebaseError } from "../../../../error";
1414
import { Build } from "../../build";
15+
import { logLabeledWarning } from "../../../../utils";
1516

1617
export const LATEST_VERSION: runtimes.Runtime = "python311";
1718

@@ -75,6 +76,8 @@ export class Delegate implements runtimes.RuntimeDelegate {
7576

7677
async modulesDir(): Promise<string> {
7778
if (!this._modulesDir) {
79+
let out = "";
80+
let stderr = "";
7881
const child = runWithVirtualEnv(
7982
[
8083
this.bin,
@@ -84,7 +87,11 @@ export class Delegate implements runtimes.RuntimeDelegate {
8487
this.sourceDir,
8588
{}
8689
);
87-
let out = "";
90+
child.stderr?.on("data", (chunk: Buffer) => {
91+
const chunkString = chunk.toString();
92+
stderr = stderr + chunkString;
93+
logger.debug(`stderr: ${chunkString}`);
94+
});
8895
child.stdout?.on("data", (chunk: Buffer) => {
8996
const chunkString = chunk.toString();
9097
out = out + chunkString;
@@ -95,6 +102,21 @@ export class Delegate implements runtimes.RuntimeDelegate {
95102
child.on("error", reject);
96103
});
97104
this._modulesDir = out.trim();
105+
if (this._modulesDir === "") {
106+
if (stderr.includes("venv") && stderr.includes("activate")) {
107+
throw new FirebaseError(
108+
"Failed to find location of Firebase Functions SDK: Missing virtual environment at venv directory. " +
109+
`Did you forget to run '${this.bin} -m venv venv'?`
110+
);
111+
}
112+
const { command, args } = virtualEnvCmd(this.sourceDir, DEFAULT_VENV_DIR);
113+
throw new FirebaseError(
114+
"Failed to find location of Firebase Functions SDK. " +
115+
`Did you forget to run '${command} ${args.join(" ")} && ${
116+
this.bin
117+
} -m pip install -r requirements.txt'?`
118+
);
119+
}
98120
}
99121
return this._modulesDir;
100122
}
@@ -116,13 +138,15 @@ export class Delegate implements runtimes.RuntimeDelegate {
116138
return Promise.resolve();
117139
}
118140

119-
async serveAdmin(port: number, envs: backend.EnvironmentVariables): Promise<() => Promise<void>> {
141+
async serveAdmin(port: number, envs: backend.EnvironmentVariables) {
120142
const modulesDir = await this.modulesDir();
121143
const envWithAdminPort = {
122144
...envs,
123145
ADMIN_PORT: port.toString(),
124146
};
125147
const args = [this.bin, path.join(modulesDir, "private", "serving.py")];
148+
const stdout: string[] = [];
149+
const stderr: string[] = [];
126150
logger.debug(
127151
`Running admin server with args: ${JSON.stringify(args)} and env: ${JSON.stringify(
128152
envWithAdminPort
@@ -131,20 +155,26 @@ export class Delegate implements runtimes.RuntimeDelegate {
131155
const childProcess = runWithVirtualEnv(args, this.sourceDir, envWithAdminPort);
132156
childProcess.stdout?.on("data", (chunk: Buffer) => {
133157
const chunkString = chunk.toString();
158+
stdout.push(chunkString);
134159
logger.debug(`stdout: ${chunkString}`);
135160
});
136161
childProcess.stderr?.on("data", (chunk: Buffer) => {
137162
const chunkString = chunk.toString();
163+
stderr.push(chunkString);
138164
logger.debug(`stderr: ${chunkString}`);
139165
});
140-
return Promise.resolve(async () => {
141-
await fetch(`http://127.0.0.1:${port}/__/quitquitquit`);
142-
const quitTimeout = setTimeout(() => {
143-
if (!childProcess.killed) {
144-
childProcess.kill("SIGKILL");
145-
}
146-
}, 10_000);
147-
clearTimeout(quitTimeout);
166+
return Promise.resolve({
167+
stderr,
168+
stdout,
169+
killProcess: async () => {
170+
await fetch(`http://127.0.0.1:${port}/__/quitquitquit`);
171+
const quitTimeout = setTimeout(() => {
172+
if (!childProcess.killed) {
173+
childProcess.kill("SIGKILL");
174+
}
175+
}, 10_000);
176+
clearTimeout(quitTimeout);
177+
},
148178
});
149179
}
150180

@@ -157,9 +187,15 @@ export class Delegate implements runtimes.RuntimeDelegate {
157187
const adminPort = await portfinder.getPortPromise({
158188
port: 8081,
159189
});
160-
const killProcess = await this.serveAdmin(adminPort, envs);
190+
const { killProcess, stderr } = await this.serveAdmin(adminPort, envs);
161191
try {
162192
discovered = await discovery.detectFromPort(adminPort, this.projectId, this.runtime);
193+
} catch (e: any) {
194+
logLabeledWarning(
195+
"functions",
196+
`Failed to detect functions from source ${e}.\nstderr:${stderr.join("\n")}`
197+
);
198+
throw e;
163199
} finally {
164200
await killProcess();
165201
}

src/functions/python.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,23 @@ import * as spawn from "cross-spawn";
33
import * as cp from "child_process";
44
import { logger } from "../logger";
55

6-
const DEFAULT_VENV_DIR = "venv";
6+
/**
7+
* Default directory for python virtual environment.
8+
*/
9+
export const DEFAULT_VENV_DIR = "venv";
10+
11+
/**
12+
* Get command for running Python virtual environment for given platform.
13+
*/
14+
export function virtualEnvCmd(cwd: string, venvDir: string): { command: string; args: string[] } {
15+
const activateScriptPath =
16+
process.platform === "win32" ? ["Scripts", "activate.bat"] : ["bin", "activate"];
17+
const venvActivate = path.join(cwd, venvDir, ...activateScriptPath);
18+
return {
19+
command: process.platform === "win32" ? venvActivate : "source",
20+
args: [process.platform === "win32" ? "" : venvActivate],
21+
};
22+
}
723

824
/**
925
* Spawn a process inside the Python virtual environment if found.
@@ -15,11 +31,8 @@ export function runWithVirtualEnv(
1531
spawnOpts: cp.SpawnOptions = {},
1632
venvDir = DEFAULT_VENV_DIR
1733
): cp.ChildProcess {
18-
const activateScriptPath =
19-
process.platform === "win32" ? ["Scripts", "activate.bat"] : ["bin", "activate"];
20-
const venvActivate = path.join(cwd, venvDir, ...activateScriptPath);
21-
const command = process.platform === "win32" ? venvActivate : "source";
22-
const args = [process.platform === "win32" ? "" : venvActivate, "&&", ...commandAndArgs];
34+
const { command, args } = virtualEnvCmd(cwd, venvDir);
35+
args.push("&&", ...commandAndArgs);
2336
logger.debug(`Running command with virtualenv: command=${command}, args=${JSON.stringify(args)}`);
2437

2538
return spawn(command, args, {

0 commit comments

Comments
 (0)