~james/core

core/e2e/harness/processes.ts -rw-r--r-- 3.2 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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<Processor> => {
	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();
	}
};