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
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