Skip to content

Commit

Permalink
Improve error messages when functions fails to load (#5782)
Browse files Browse the repository at this point in the history
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
```
  • Loading branch information
taeold authored May 2, 2023
1 parent 19a8384 commit c48ccfe
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/deploy/functions/runtimes/discovery/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function detectFromPort(
port: number,
project: string,
runtime: runtimes.Runtime,
timeout = 30_000 /* 30s to boot up */
timeout = 10_000 /* 10s to boot up */
): Promise<build.Build> {
// The result type of fetch isn't exported
let res: { text(): Promise<string> };
Expand Down
60 changes: 48 additions & 12 deletions src/deploy/functions/runtimes/python/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import * as runtimes from "..";
import * as backend from "../../backend";
import * as discovery from "../discovery";
import { logger } from "../../../../logger";
import { runWithVirtualEnv } from "../../../../functions/python";
import { DEFAULT_VENV_DIR, runWithVirtualEnv, virtualEnvCmd } from "../../../../functions/python";
import { FirebaseError } from "../../../../error";
import { Build } from "../../build";
import { logLabeledWarning } from "../../../../utils";

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

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

async modulesDir(): Promise<string> {
if (!this._modulesDir) {
let out = "";
let stderr = "";
const child = runWithVirtualEnv(
[
this.bin,
Expand All @@ -84,7 +87,11 @@ export class Delegate implements runtimes.RuntimeDelegate {
this.sourceDir,
{}
);
let out = "";
child.stderr?.on("data", (chunk: Buffer) => {
const chunkString = chunk.toString();
stderr = stderr + chunkString;
logger.debug(`stderr: ${chunkString}`);
});
child.stdout?.on("data", (chunk: Buffer) => {
const chunkString = chunk.toString();
out = out + chunkString;
Expand All @@ -95,6 +102,21 @@ export class Delegate implements runtimes.RuntimeDelegate {
child.on("error", reject);
});
this._modulesDir = out.trim();
if (this._modulesDir === "") {
if (stderr.includes("venv") && stderr.includes("activate")) {
throw new FirebaseError(
"Failed to find location of Firebase Functions SDK: Missing virtual environment at venv directory. " +
`Did you forget to run '${this.bin} -m venv venv'?`
);
}
const { command, args } = virtualEnvCmd(this.sourceDir, DEFAULT_VENV_DIR);
throw new FirebaseError(
"Failed to find location of Firebase Functions SDK. " +
`Did you forget to run '${command} ${args.join(" ")} && ${
this.bin
} -m pip install -r requirements.txt'?`
);
}
}
return this._modulesDir;
}
Expand All @@ -116,13 +138,15 @@ export class Delegate implements runtimes.RuntimeDelegate {
return Promise.resolve();
}

async serveAdmin(port: number, envs: backend.EnvironmentVariables): Promise<() => Promise<void>> {
async serveAdmin(port: number, envs: backend.EnvironmentVariables) {
const modulesDir = await this.modulesDir();
const envWithAdminPort = {
...envs,
ADMIN_PORT: port.toString(),
};
const args = [this.bin, path.join(modulesDir, "private", "serving.py")];
const stdout: string[] = [];
const stderr: string[] = [];
logger.debug(
`Running admin server with args: ${JSON.stringify(args)} and env: ${JSON.stringify(
envWithAdminPort
Expand All @@ -131,20 +155,26 @@ export class Delegate implements runtimes.RuntimeDelegate {
const childProcess = runWithVirtualEnv(args, this.sourceDir, envWithAdminPort);
childProcess.stdout?.on("data", (chunk: Buffer) => {
const chunkString = chunk.toString();
stdout.push(chunkString);
logger.debug(`stdout: ${chunkString}`);
});
childProcess.stderr?.on("data", (chunk: Buffer) => {
const chunkString = chunk.toString();
stderr.push(chunkString);
logger.debug(`stderr: ${chunkString}`);
});
return Promise.resolve(async () => {
await fetch(`http://127.0.0.1:${port}/__/quitquitquit`);
const quitTimeout = setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill("SIGKILL");
}
}, 10_000);
clearTimeout(quitTimeout);
return Promise.resolve({
stderr,
stdout,
killProcess: async () => {
await fetch(`http://127.0.0.1:${port}/__/quitquitquit`);
const quitTimeout = setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill("SIGKILL");
}
}, 10_000);
clearTimeout(quitTimeout);
},
});
}

Expand All @@ -157,9 +187,15 @@ export class Delegate implements runtimes.RuntimeDelegate {
const adminPort = await portfinder.getPortPromise({
port: 8081,
});
const killProcess = await this.serveAdmin(adminPort, envs);
const { killProcess, stderr } = await this.serveAdmin(adminPort, envs);
try {
discovered = await discovery.detectFromPort(adminPort, this.projectId, this.runtime);
} catch (e: any) {
logLabeledWarning(
"functions",
`Failed to detect functions from source ${e}.\nstderr:${stderr.join("\n")}`
);
throw e;
} finally {
await killProcess();
}
Expand Down
25 changes: 19 additions & 6 deletions src/functions/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@ import * as spawn from "cross-spawn";
import * as cp from "child_process";
import { logger } from "../logger";

const DEFAULT_VENV_DIR = "venv";
/**
* Default directory for python virtual environment.
*/
export const DEFAULT_VENV_DIR = "venv";

/**
* Get command for running Python virtual environment for given platform.
*/
export function virtualEnvCmd(cwd: string, venvDir: string): { command: string; args: string[] } {
const activateScriptPath =
process.platform === "win32" ? ["Scripts", "activate.bat"] : ["bin", "activate"];
const venvActivate = path.join(cwd, venvDir, ...activateScriptPath);
return {
command: process.platform === "win32" ? venvActivate : "source",
args: [process.platform === "win32" ? "" : venvActivate],
};
}

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

return spawn(command, args, {
Expand Down

0 comments on commit c48ccfe

Please # to comment.