import { spawn } from 'node:child_process' import { createWriteStream, mkdirSync, writeFileSync } from 'node:fs' import path from 'node:path' const frontend = process.cwd() const root = path.resolve(frontend, '..') const project = process.env.DODO_E2E_PROJECT if (!project) throw new Error('DODO_E2E_PROJECT is required') const runtimeBase = path.join(frontend, 'playwright-runtime', project) const runtime = path.join(runtimeBase, `run-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`) const attachments = path.join(runtime, 'attachments') const staging = path.join(runtime, 'backup-staging') mkdirSync(attachments, { recursive: true }) mkdirSync(staging, { recursive: true }) writeFileSync(path.join(runtimeBase, 'latest.json'), JSON.stringify({ runtime, database: path.join(runtime, 'dodo.sqlite3'), attachments, staging }, null, 2)) writeFileSync(path.join(runtime, 'runtime.json'), JSON.stringify({ database: path.join(runtime, 'dodo.sqlite3'), attachments, staging }, null, 2)) const logs = { backend: createWriteStream(path.join(runtime, 'backend.log'), { flags: 'a' }), frontend: createWriteStream(path.join(runtime, 'frontend.log'), { flags: 'a' }), } const children = [] function start(command, args, options, log) { const child = spawn(command, args, { ...options, stdio: ['ignore', 'pipe', 'pipe'] }) child.stdout.pipe(log) child.stderr.pipe(log) children.push(child) return child } start(path.join(root, '.venv/bin/uvicorn'), ['backend.main:app', '--host', '127.0.0.1', '--port', '8781'], { cwd: root, env: { ...process.env, DODO_DATABASE_URL: `sqlite+aiosqlite:///${path.join(runtime, 'dodo.sqlite3')}`, DODO_AUTO_CREATE_SCHEMA: 'true', DODO_COOKIE_SECURE: 'false', DODO_ATTACHMENT_DIR: attachments, DODO_BACKUP_STAGING_DIR: staging, }, }, logs.backend) start('pnpm', ['exec', 'vite', '--host', '127.0.0.1', '--port', '5173', '--strictPort'], { cwd: frontend, env: process.env }, logs.frontend) let stopping = false function stop(signal = 'SIGTERM') { if (stopping) return stopping = true for (const child of children) if (!child.killed) child.kill(signal) setTimeout(() => { for (const child of children) if (!child.killed) child.kill('SIGKILL') }, 3000).unref() } process.on('SIGTERM', () => stop()) process.on('SIGINT', () => stop()) process.on('exit', () => stop()) await Promise.all(children.map(child => new Promise((resolve, reject) => { child.once('exit', (code, signal) => stopping ? resolve() : reject(new Error(`server exited code=${code} signal=${signal}`))) })))