App checks / changes (push) Canceled after 0s
App checks / tournament (push) Canceled after 0s
App checks / shifts (push) Canceled after 0s
74 lines
3.1 KiB
JavaScript
74 lines
3.1 KiB
JavaScript
// Migration-only transport adapter: runs the unchanged legacy HTTP assertions on FastAPI.
|
|
import { spawn } from 'node:child_process';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import net from 'node:net';
|
|
import { join } from 'node:path';
|
|
|
|
export const SESSION_COOKIE_NAME = 'tournament_admin_session';
|
|
|
|
export async function createApp(options) {
|
|
const temporary = mkdtempSync(join(process.env.TOURNAMENT_WORK, 'api-'));
|
|
const socket = net.createServer();
|
|
await new Promise(resolve => socket.listen(0, '127.0.0.1', resolve));
|
|
const port = socket.address().port;
|
|
await new Promise(resolve => socket.close(resolve));
|
|
const processHandle = spawn(process.env.TOURNAMENT_PYTHON, [
|
|
'-m', 'uvicorn', 'app.main:create_app', '--factory', '--host', '127.0.0.1',
|
|
'--port', String(port), '--no-proxy-headers',
|
|
], { cwd: process.env.TOURNAMENT_BACKEND, env: {
|
|
...process.env, ADMIN_PASSWORD: options.authentication.adminPassword,
|
|
SESSION_SECRET: options.authentication.sessionSecret,
|
|
DATABASE_PATH: join(temporary, 'tournament.sqlite'), APP_BASE_PATH: options.basePath,
|
|
TRUST_PROXY: 'false', PYTHONDONTWRITEBYTECODE: '1',
|
|
}, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
let logs = '';
|
|
processHandle.stdout.on('data', chunk => { logs += chunk; });
|
|
processHandle.stderr.on('data', chunk => { logs += chunk; });
|
|
const base = `http://127.0.0.1:${port}`;
|
|
async function close() {
|
|
if (processHandle.exitCode === null) {
|
|
processHandle.kill('SIGTERM');
|
|
const timer = setTimeout(() => processHandle.kill('SIGKILL'), 3000);
|
|
await new Promise(resolve => processHandle.once('exit', resolve));
|
|
clearTimeout(timer);
|
|
}
|
|
rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
try {
|
|
let ready = false;
|
|
for (let count = 0; count < 100; count++) {
|
|
try {
|
|
const response = await fetch(base + '/api/auth/session');
|
|
if (response.ok) { ready = true; break; }
|
|
} catch { /* Wait for Uvicorn to bind its socket. */ }
|
|
if (processHandle.exitCode !== null) throw new Error(logs);
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
if (!ready) throw new Error('FastAPI startup timed out: ' + logs);
|
|
} catch (error) {
|
|
await close();
|
|
throw error;
|
|
}
|
|
return {
|
|
ready: async () => {}, listen: async () => {}, server: { address: () => ({ port }) }, close,
|
|
async inject(input) {
|
|
const headers = { ...input.headers };
|
|
if (input.payload !== undefined) headers['content-type'] = 'application/json';
|
|
const response = await fetch(base + input.url, {
|
|
method: input.method, headers,
|
|
body: input.payload === undefined ? undefined : JSON.stringify(input.payload),
|
|
});
|
|
const body = await response.text();
|
|
return {
|
|
statusCode: response.status, headers: Object.fromEntries(response.headers), body,
|
|
json: () => JSON.parse(body),
|
|
cookies: response.headers.getSetCookie().map(cookie => {
|
|
const [pair] = cookie.split(';');
|
|
const separator = pair.indexOf('=');
|
|
return { name: pair.slice(0, separator), value: pair.slice(separator + 1) };
|
|
}),
|
|
};
|
|
},
|
|
};
|
|
}
|