Repository navigation
Expand file tree
/
Copy pathapp-services.live.ts
More file actions
74 lines (73 loc) · 5.31 KB
/
Copy pathapp-services.live.ts
File metadata and controls
74 lines (73 loc) · 5.31 KB
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
#!/usr/bin/env bun
/** Explicit smoke test. Owns only a random fixture app; never touches existing registrations. */
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { strict as assert } from "node:assert";
import { paths, SCRIPT, MARKER, run } from "./app-services";
if (!process.argv.includes("--run")) {
console.log("Run explicitly with --run to exercise a temporary systemd user service.");
} else {
const root = mkdtempSync(join(tmpdir(), "infomarchy-live-"));
const appFolder = join(root, "app with % spaces $"), registry = join(root, "apps.json");
mkdirSync(appFolder); writeFileSync(registry, "[]");
const env = { ...process.env, INFOMARCHY_APPS_REGISTRY: registry, XDG_STATE_HOME: join(root, "state % $") };
const id = "smoke-" + crypto.randomUUID().slice(0, 8), unit = `infomarchy-app-${id}.service`;
const unitFile = join(paths(env).units, unit);
assert(!existsSync(unitFile));
const probe = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("probe") });
let port = probe.port!; await probe.stop(true);
const serverFile = join(appFolder, "serve.ts");
writeFileSync(serverFile, 'const server = Bun.serve({hostname:"127.0.0.1",port:Number(process.env.PORT),fetch(){console.log("GET / HTTP/1.1 200");return new Response("fixture")}}); console.log("ready",server.port);');
async function cli(args: string[], expected = true): Promise<any> {
const child = Bun.spawn([process.execPath, SCRIPT, ...args, "--json"], { env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
const result = JSON.parse(stdout); assert.equal(code === 0 && result.ok, expected, stderr || stdout); return result;
}
let foreign: ReturnType<typeof Bun.serve> | undefined;
try {
await cli(["register", "--registration", JSON.stringify({ id, name: "Temporary app", path: appFolder, port, command: [process.execPath, serverFile] })]);
assert.equal((await cli(["status", id])).services[0].state, "stopped");
run(["systemd-analyze", "--user", "verify", unitFile]);
const concurrent = await Promise.all(Array.from({ length: 4 }, () => cli(["ensure", id])));
const started = concurrent[0].status; assert(started.ready); assert(concurrent.every(r => r.status.pid === started.pid));
assert.equal(started.autostart, false);
const restarted = (await cli(["restart", id])).status; assert(restarted.ready && restarted.pid !== started.pid);
const beforeLog = (await cli(["logs", id])).log; assert(beforeLog.includes("GET / HTTP/1.1 200"));
await cli(["stop", id]);
const newFolder = join(root, "edited app % $"); mkdirSync(newFolder);
const newFile = join(newFolder, "serve.ts"); writeFileSync(newFile, readFileSync(serverFile));
const available = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response("probe") });
const newPort = available.port!; await available.stop(true);
const edited = await cli(["update", id, "--registration", JSON.stringify({ name: "Edited app", path: newFolder,
port: newPort, command: [process.execPath, newFile], healthPath: "/ready" })]);
assert(edited.message.includes("Updated"));
const saved = (await cli(["registry"])).services.find((app: any) => app.id === id);
assert.equal(saved.path, newFolder); assert.equal(saved.port, newPort); assert.equal(saved.healthPath, "/ready");
assert.equal((await cli(["status", id])).services[0].state, "stopped");
run(["systemd-analyze", "--user", "verify", unitFile]);
port = newPort;
foreign = Bun.serve({ hostname: "127.0.0.1", port, fetch: () => new Response("foreign fixture") });
const conflict = await cli(["ensure", id], false); assert(conflict.error.includes("occupied"));
assert.equal(await (await fetch(`http://127.0.0.1:${port}`)).text(), "foreign fixture");
await foreign.stop(true); foreign = undefined;
const active = (await cli(["ensure", id])).status;
process.kill(active.pid, "SIGKILL"); // only this freshly-created fixture unit's main process
let recovered: any; const deadline = Date.now() + 15000;
while (Date.now() < deadline) {
await Bun.sleep(500); recovered = (await cli(["status", id])).services[0];
if (recovered.ready && recovered.pid !== active.pid) break;
}
assert(recovered.ready && recovered.pid !== active.pid, "systemd did not recover the fixture");
assert((await cli(["logs", id])).log.includes(beforeLog));
console.log(JSON.stringify({ ok: true, checks: ["register stays stopped", "four concurrent ensures reuse one PID", "restart", "HTTP request logs retained", "foreign listener preserved", "automatic crash recovery", "edit stopped service folder and port", "paths with spaces/percent/dollar", "stop and cleanup"], fixture: id }));
} finally {
await foreign?.stop(true);
if (existsSync(unitFile)) {
assert(readFileSync(unitFile, "utf8").startsWith(MARKER));
run(["systemctl", "--user", "stop", unit], { timeout: 30000, check: false });
unlinkSync(unitFile); run(["systemctl", "--user", "daemon-reload"]); run(["systemctl", "--user", "reset-failed", unit], { check: false });
}
rmSync(root, { recursive: true, force: true });
}
}