Skip to content

Commit e623f71

Browse files
committed
Get python emulator going.
1 parent e72d11d commit e623f71

File tree

6 files changed

+77
-36
lines changed

6 files changed

+77
-36
lines changed

src/deploy/functions/runtimes/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const RUNTIMES: string[] = ["nodejs10", "nodejs12", "nodejs14", "nodejs16", "nod
1010
// Experimental runtimes are part of the Runtime type, but are in a
1111
// different list to help guard against some day accidentally iterating over
1212
// and printing a hidden runtime to the user.
13-
const EXPERIMENTAL_RUNTIMES: string[] = [];
13+
const EXPERIMENTAL_RUNTIMES: string[] = ["python310", "python311"];
1414
export type Runtime = typeof RUNTIMES[number] | typeof EXPERIMENTAL_RUNTIMES[number];
1515

1616
/** Runtimes that can be found in existing backends but not used for new functions. */

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "fs";
22
import * as path from "path";
3+
import fetch from "node-fetch";
34
import { promisify } from "util";
45

56
import * as portfinder from "portfinder";
@@ -78,7 +79,7 @@ class Delegate implements runtimes.RuntimeDelegate {
7879
child.on("exit", resolve);
7980
child.on("error", reject);
8081
});
81-
this._modulesDir = out;
82+
this._modulesDir = out.trim();
8283
}
8384
return this._modulesDir;
8485
}

src/emulator/functionsEmulator.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,43 @@ export interface FunctionsEmulatorArgs {
124124
projectAlias?: string;
125125
}
126126

127-
// FunctionsRuntimeInstance is the handler for a running function invocation
127+
/**
128+
* IPC connection info of a Function Runtime.
129+
*/
130+
export class IPCConn {
131+
constructor(readonly socketPath: string) {}
132+
133+
httpReqOpts(): http.RequestOptions {
134+
return {
135+
path: `/`,
136+
socketPath: this.socketPath,
137+
};
138+
}
139+
}
140+
141+
/**
142+
* TCP/IP connection info of a Function Runtime.
143+
*/
144+
export class TCPConn {
145+
constructor(readonly host: string, readonly port: number) {}
146+
147+
httpReqOpts(): http.RequestOptions {
148+
return {
149+
path: `/`,
150+
host: this.host,
151+
port: this.port,
152+
};
153+
}
154+
}
155+
128156
export interface FunctionsRuntimeInstance {
129157
process: ChildProcess;
130158
// An emitter which sends our EmulatorLog events from the runtime.
131159
events: EventEmitter;
132160
// A cwd of the process
133161
cwd: string;
134-
// Path to socket file used for HTTP-over-IPC comms.
135-
socketPath: string;
162+
// Communication info for the runtime
163+
conn: IPCConn | TCPConn;
136164
}
137165

138166
export interface InvokeRuntimeOpts {
@@ -355,8 +383,8 @@ export class FunctionsEmulator implements EmulatorInstance {
355383
return new Promise((resolve, reject) => {
356384
const req = http.request(
357385
{
386+
...worker.runtime.conn.httpReqOpts(),
358387
path: `/`,
359-
socketPath: worker.runtime.socketPath,
360388
headers: headers,
361389
},
362390
resolve
@@ -1269,14 +1297,14 @@ export class FunctionsEmulator implements EmulatorInstance {
12691297
process: childProcess,
12701298
events: new EventEmitter(),
12711299
cwd: backend.functionsDir,
1272-
socketPath,
1300+
conn: new IPCConn(socketPath),
12731301
});
12741302
}
12751303

12761304
async startPython(
12771305
backend: EmulatableBackend,
12781306
envs: Record<string, string>
1279-
): Promise<ChildProcess> {
1307+
): Promise<FunctionsRuntimeInstance> {
12801308
const args = [path.join(__dirname, "functionsEmulatorRuntime")];
12811309

12821310
if (this.args.debugPort) {
@@ -1294,18 +1322,23 @@ export class FunctionsEmulator implements EmulatorInstance {
12941322
);
12951323
}
12961324

1297-
// Unfortunately, there isn't platform-neutral support for Unix Domain Socket or Nameed Pipe in the python
1298-
// ecosystem. Use regular IP+PORT combination instead.
1325+
// Unfortunately, there isn't platform-neutral support for Unix Domain Socket or Named Pipe in the python
1326+
// ecosystem. Use TCP/IP stack instead.
12991327
const port = await portfinder.getPortPromise({
13001328
port: 8081,
13011329
});
1302-
return Promise.resolve(
1303-
runWithVirtualEnv([bin, ...args], backend.functionsDir, {
1304-
...process.env,
1305-
...envs,
1306-
PORT: port.toString(),
1307-
})
1308-
);
1330+
const childProcess = runWithVirtualEnv([bin, ...args], backend.functionsDir, {
1331+
...process.env,
1332+
...envs,
1333+
PORT: port.toString(),
1334+
});
1335+
1336+
return Promise.resolve({
1337+
process: childProcess,
1338+
events: new EventEmitter(),
1339+
cwd: backend.functionsDir,
1340+
conn: new TCPConn("localhost", port),
1341+
});
13091342
}
13101343

13111344
async startRuntime(
@@ -1315,7 +1348,12 @@ export class FunctionsEmulator implements EmulatorInstance {
13151348
const runtimeEnv = this.getRuntimeEnvs(backend, trigger);
13161349
const secretEnvs = await this.resolveSecretEnvs(backend, trigger);
13171350

1318-
const runtime = await this.startNode(backend, { ...runtimeEnv, ...secretEnvs });
1351+
let runtime;
1352+
if (backend.runtime!.startsWith("python")) {
1353+
runtime = await this.startPython(backend, { ...runtimeEnv, ...secretEnvs });
1354+
} else {
1355+
runtime = await this.startNode(backend, { ...runtimeEnv, ...secretEnvs });
1356+
}
13191357
const extensionLogInfo = {
13201358
instanceId: backend.extensionInstanceId,
13211359
ref: backend.extensionVersion?.ref,

src/emulator/functionsRuntimeWorker.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { EventEmitter } from "events";
88
import { EmulatorLogger, ExtensionLogInfo } from "./emulatorLogger";
99
import { FirebaseError } from "../error";
1010
import { Serializable } from "child_process";
11+
import { IncomingMessage } from "http";
1112

1213
type LogListener = (el: EmulatorLog) => any;
1314

@@ -118,12 +119,12 @@ export class RuntimeWorker {
118119
return new Promise((resolve) => {
119120
const proxy = http.request(
120121
{
122+
...this.runtime.conn.httpReqOpts(),
121123
method: req.method,
122124
path: req.path,
123125
headers: req.headers,
124-
socketPath: this.runtime.socketPath,
125126
},
126-
(_resp) => {
127+
(_resp: IncomingMessage) => {
127128
resp.writeHead(_resp.statusCode || 200, _resp.headers);
128129
const piped = _resp.pipe(resp);
129130
piped.on("finish", () => {
@@ -178,20 +179,19 @@ export class RuntimeWorker {
178179

179180
isSocketReady(): Promise<void> {
180181
return new Promise((resolve, reject) => {
181-
const req = http
182-
.request(
183-
{
184-
method: "GET",
185-
path: "/__/health",
186-
socketPath: this.runtime.socketPath,
187-
},
188-
() => {
189-
// Set the worker state to IDLE for new work
190-
this.readyForWork();
191-
resolve();
192-
}
193-
)
194-
.end();
182+
const req = http.request(
183+
{
184+
...this.runtime.conn.httpReqOpts(),
185+
method: "GET",
186+
path: "/__/health",
187+
},
188+
() => {
189+
// Set the worker state to IDLE for new work
190+
this.readyForWork();
191+
resolve();
192+
}
193+
);
194+
req.end();
195195
req.on("error", (error) => {
196196
reject(error);
197197
});

src/functions/python.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from "path";
22
import * as spawn from "cross-spawn";
33
import * as cp from "child_process";
4+
import { logger } from "../logger";
45

56
const DEFAULT_VENV_DIR = "venv";
67

@@ -18,6 +19,7 @@ export function runWithVirtualEnv(
1819
const venvActivate = path.join(cwd, venvDir, ...activateScriptPath);
1920
const command = process.platform === "win32" ? venvActivate : "source";
2021
const args = [process.platform === "win32" ? "" : venvActivate, "&&", ...commandAndArgs];
22+
logger.debug(`Running command with virtualenv: command=${command}, args=${JSON.stringify(args)}`);
2123

2224
return spawn(command, args, {
2325
shell: true,

src/test/emulators/functionsRuntimeWorker.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as httpMocks from "node-mocks-http";
22
import * as nock from "nock";
33
import { expect } from "chai";
4-
import { FunctionsRuntimeInstance } from "../../emulator/functionsEmulator";
4+
import { FunctionsRuntimeInstance, IPCConn } from "../../emulator/functionsEmulator";
55
import { EventEmitter } from "events";
66
import {
77
RuntimeWorker,
@@ -21,7 +21,7 @@ class MockRuntimeInstance implements FunctionsRuntimeInstance {
2121
events: EventEmitter = new EventEmitter();
2222
exit: Promise<number>;
2323
cwd = "/home/users/dir";
24-
socketPath = "/path/to/socket/foo.sock";
24+
conn = new IPCConn("/path/to/socket/foo.sock");
2525

2626
constructor() {
2727
this.exit = new Promise((resolve) => {

0 commit comments

Comments
 (0)