import { request } from "node:http"; import { DatabaseSync } from "node:sqlite"; import { until } from "./net"; const onPc = process.env.E2E_STACK === "pc"; const stateDir = () => { const state = process.env.E2E_PC_STATE; if (!onPc || !state) throw new Error("this needs the pc stack, run it with E2E_STACK=pc"); return state; }; interface Processor { status: string; is_ready: string; } const pc = (method: string, path: string) => new Promise<{ status: number; body: string }>((done, fail) => { request({ socketPath: `${stateDir()}/run/pc.sock`, method, path }, (res) => { let body = ""; res.on("data", (chunk) => (body += chunk)); res.on("end", () => done({ status: res.statusCode ?? 0, body })); }) .on("error", fail) .end(); }); const processOf = async (name: string): Promise => { const { status, body } = await pc("GET", `/process/${name}`); if (status !== 200) throw new Error(`process-compose answered ${status} for ${name}: ${body}`); return JSON.parse(body); }; const command = async (method: string, verb: string, name: string) => { const { status, body } = await pc(method, `/process/${verb}/${name}`); if (status !== 200) throw new Error(`process-compose could not ${verb} ${name}: ${status} ${body}`); }; export const stopProcess = async (name: string) => { await command("PATCH", "stop", name); await until(`${name} should have stopped`, 30_000, async () => { const { status } = await processOf(name); return [ status !== "Running" && status !== "Launching" && status !== "Terminating", `it is ${status}` ]; }); }; // use started when the next step has to kill it mid-startup export const startProcess = async (name: string, upTo: "started" | "ready" = "ready") => { await command("POST", "start", name); await until(`${name} should be ${upTo}`, 90_000, async () => { const { status, is_ready } = await processOf(name); const there = upTo === "ready" ? is_ready === "Ready" : status === "Running"; return [there, `it is ${status}, ${is_ready}`]; }); }; export const isReady = async (name: string) => (await processOf(name)).is_ready === "Ready"; // deliberi has no resend key here, so the token comes from its db instead of a mail export const signupToken = (email: string) => { const db = new DatabaseSync(`${stateDir()}/data/deliberi/deliberi.db`, { readOnly: true }); try { const row = db .prepare("select verification_token as token from signups_inflight where email = ?") .get(email); if (typeof row?.token !== "string") throw new Error(`deliberi has no signup for ${email}`); return row.token; } finally { db.close(); } }; // stop spindle first, it reads this at boot and keeps writing it export const rewindJetstreamCursor = (microseconds: number) => { const db = new DatabaseSync(`${stateDir()}/data/spindle/spindle.db`); try { db.exec("pragma busy_timeout = 10000"); db.prepare("update _jetstream set last_time_us = ? where id = 1").run(microseconds); const moved = db.prepare("select last_time_us as at from _jetstream where id = 1").get(); if (moved?.at !== microseconds) throw new Error(`spindle has no saved cursor to rewind: ${JSON.stringify(moved)}`); } finally { db.close(); } };