Compare commits

...

5 Commits

Author SHA1 Message Date
Trek Glowacki
30ada9855e Add dynamic port assignment and FD 3 communication 2023-09-18 12:56:30 -05:00
Trek Glowacki
bfe60ec497 . 2023-09-15 12:26:59 -05:00
Trek Glowacki
3247bee5f4 WIP 2023-09-15 10:51:34 -05:00
Trek Glowacki
b0743a166a . 2023-09-14 12:48:38 -05:00
Trek Glowacki
a27b41e841 . 2023-09-14 12:48:17 -05:00
5 changed files with 207 additions and 10 deletions

View File

@@ -978,6 +978,7 @@ export default class DevServer {
// log address without trailing slash to maintain backwards compatibility
addressFormatted = addressFormatted.replace(/\/$/, '');
}
this.output.ready(`Available at ${link(addressFormatted)}`);
}
@@ -1450,7 +1451,7 @@ export default class DevServer {
);
const middlewareBody = await middlewareRes.buffer();
console.error('Here?');
if (middlewareRes.status === 500 && middlewareBody.byteLength === 0) {
await this.sendError(
req,

View File

@@ -0,0 +1,36 @@
from http.server import HTTPServer
import os
import sys
__HANDLER_CLASS_TEMPLATE
if __name__ == "__main__":
hostName = "localhost"
errorMessage = 'Neither `app` nor `handler` defined in serverless function {}. See: https://vercel.com/docs/functions/serverless-functions/runtimes/python'.format(__file__)
if 'handler' in dir():
appOrHandler = handler
if 'app' in dir():
appOrHandler = app
if not 'appOrHandler' in dir():
raise Exception(errorMessage)
# Port 0 is unix-speak for 'first available port'
httpd = HTTPServer((hostName, 0), appOrHandler)
serverPort = httpd.socket.getsockname()[1]
print("Server started http://%s:%s" % (hostName, serverPort))
fd = os.open("pipe", os.O_RDWR|os.O_CREAT)
with os.fdopen(fd, 'w') as fdfile:
fdfile.write(str(serverPort))
fdfile.close()
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print("Server stopped.")

View File

@@ -20,6 +20,7 @@
"test-e2e": "pnpm test test/integration-*"
},
"devDependencies": {
"@tootallnate/once": "1.1.2",
"@types/execa": "^0.9.0",
"@types/jest": "27.4.1",
"@types/node": "14.18.33",

View File

@@ -1,23 +1,37 @@
import { join, dirname, basename } from 'path';
import { spawn } from 'child_process';
import execa from 'execa';
import fs from 'fs';
import { promisify } from 'util';
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
import { tmpdir } from 'os';
import retry from 'async-retry';
import { Readable } from 'stream';
import once from '@tootallnate/once';
import {
GlobOptions,
BuildOptions,
getWriteableDirectory,
download,
StartDevServerOptions,
StartDevServerResult,
glob,
createLambda,
download,
getWriteableDirectory,
shouldServe,
debug,
NowBuildError,
cloneEnv,
} from '@vercel/build-utils';
import { readFile, writeFile, mkdirp, remove } from 'fs-extra';
import { GlobOptions, createLambda, NowBuildError } from '@vercel/build-utils';
import { installRequirement, installRequirementsFile } from './install';
import { getLatestPythonVersion, getSupportedPythonVersion } from './version';
const TMP = tmpdir();
function isReadable(v: any): v is Readable {
return v && v.readable === true;
}
async function pipenvConvert(cmd: string, srcDir: string) {
debug('Running pipfile2req...');
try {
@@ -204,8 +218,10 @@ export const build = async ({
: 'node_modules/**',
};
const files = await glob('**', globOptions);
console.log(files);
const lambda = await createLambda({
files: await glob('**', globOptions),
files,
handler: `${handlerPyFilename}.vc_handler`,
runtime: pythonVersion.runtime,
environment: {},
@@ -214,6 +230,146 @@ export const build = async ({
return { output: lambda };
};
interface PortInfo {
port: number;
}
function isPortInfo(v: any): v is PortInfo {
return v && typeof v.port === 'number';
}
export interface CancelablePromise<T> extends Promise<T> {
cancel: () => void;
}
function waitForPortFile(portFile: string) {
const opts = { portFile, canceled: false };
const promise = waitForPortFile_(opts) as CancelablePromise<PortInfo | void>;
promise.cancel = () => {
opts.canceled = true;
};
return promise;
}
async function waitForPortFile_(opts: {
portFile: string;
canceled: boolean;
}): Promise<PortInfo | void> {
while (!opts.canceled) {
await new Promise(resolve => setTimeout(resolve, 100));
try {
const port = Number(await readFile(opts.portFile, 'ascii'));
retry(() => remove(opts.portFile)).catch((err: Error) => {
console.error(`Could not delete port file: ${opts.portFile}: ${err}`);
});
return { port };
} catch (err: any) {
if (err.code !== 'ENOENT') {
throw err;
}
}
}
}
async function copyDevServer(
serverlessFunctionString: string,
dest: string
): Promise<void> {
const serverTemplate = await readFile(
join(__dirname, '../dev-server.py'),
'utf8'
);
const patched = serverTemplate.replace(
'__HANDLER_CLASS_TEMPLATE',
serverlessFunctionString
);
console.log('Writing vercel-dev-server-main.py to ', dest);
await writeFile(join(dest, 'vercel-dev-server-main.py'), patched);
}
export async function startDevServer(
opts: StartDevServerOptions
): Promise<StartDevServerResult> {
opts.config;
const { entrypoint, workPath, meta = {} } = opts;
const { devCacheDir = join(workPath, '.vercel', 'cache') } = meta;
const entrypointDir = dirname(entrypoint);
entrypointDir;
const tmp = join(
devCacheDir,
'python',
Math.random().toString(32).substring(2)
);
const tmpPackage = join(tmp, entrypointDir);
await mkdirp(tmpPackage);
const serverlessFunctionBody = await readFile(join(workPath, entrypoint));
await copyDevServer(serverlessFunctionBody, tmpPackage);
const portFile = join(
TMP,
`vercel-dev-port-${Math.random().toString(32).substring(2)}`
);
const env = cloneEnv(process.env, meta.env, {
VERCEL_DEV_PORT_FILE: portFile,
});
const executable = 'python3';
// run the dev server
debug(`SPAWNING ${executable} CWD=${tmp}`);
const child = spawn(executable, ['api/vercel-dev-server-main.py'], {
cwd: tmp,
env,
stdio: ['ignore', 'inherit', 'inherit', 'pipe'],
});
child.on('close', async () => {
try {
await retry(() => remove(tmp));
} catch (err: any) {
console.error(`Could not delete tmp directory: ${tmp}: ${err}`);
}
});
const portPipe = child.stdio[3];
if (!isReadable(portPipe)) {
throw new Error('File descriptor 3 is not readable');
}
// // `dev-server.python` writes the ephemeral port number to FD 3 to be consumed here
const onPort = new Promise<PortInfo>(resolve => {
portPipe.setEncoding('utf8');
portPipe.once('data', d => {
resolve({ port: Number(d) });
});
});
const onPortFile = waitForPortFile(portFile);
const onExit = once.spread<[number, string | null]>(child, 'exit');
const result = await Promise.race([onPort, onPortFile, onExit]);
onExit.cancel();
onPortFile.cancel();
console.log(`Hosting on http://127.0.0.1:${result.port}`);
if (isPortInfo(result)) {
return {
port: result.port,
pid: child.pid,
};
} else if (Array.isArray(result)) {
// Got "exit" event from child process
const [exitCode, signal] = result;
const reason = signal ? `"${signal}" signal` : `exit code ${exitCode}`;
throw new Error(`\`python3 ${entrypoint}\` failed with ${reason}`);
} else {
throw new Error(`Unexpected result type: ${typeof result}`);
}
}
export { shouldServe };
// internal only - expect breaking changes if other packages depend on these exports

3
pnpm-lock.yaml generated
View File

@@ -1337,6 +1337,9 @@ importers:
packages/python:
devDependencies:
'@tootallnate/once':
specifier: 1.1.2
version: 1.1.2
'@types/execa':
specifier: ^0.9.0
version: 0.9.0