104 lines
2.9 KiB
JavaScript
104 lines
2.9 KiB
JavaScript
import fs from "node:fs";
|
|
import fsp from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
|
|
export class SessionStore {
|
|
constructor(directory) {
|
|
this.directory = directory;
|
|
this.sessions = new Map();
|
|
}
|
|
|
|
async init() {
|
|
await fsp.mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
const files = await fsp.readdir(this.directory);
|
|
for (const file of files.filter((name) => name.endsWith(".json"))) {
|
|
try {
|
|
const record = JSON.parse(await fsp.readFile(path.join(this.directory, file), "utf8"));
|
|
if (record.status === "running") {
|
|
record.status = "interrupted";
|
|
record.endedAt = new Date().toISOString();
|
|
}
|
|
this.sessions.set(record.id, { ...record, archived: true, clients: new Set(), buffer: "" });
|
|
} catch {
|
|
// A partial metadata file must not prevent the runner from starting.
|
|
}
|
|
}
|
|
}
|
|
|
|
add(record) {
|
|
const session = { ...record, clients: new Set(), buffer: "", errorBuffer: "", archived: false };
|
|
this.sessions.set(session.id, session);
|
|
this.persist(session);
|
|
return session;
|
|
}
|
|
|
|
get(id) {
|
|
return this.sessions.get(id);
|
|
}
|
|
|
|
publicRecord(session) {
|
|
const {
|
|
clients,
|
|
pty,
|
|
childProcess,
|
|
buffer,
|
|
errorBuffer,
|
|
logStream,
|
|
environment,
|
|
archived,
|
|
...record
|
|
} = session;
|
|
return { ...record, connectedClients: clients?.size || 0, archived: Boolean(archived) };
|
|
}
|
|
|
|
list() {
|
|
return [...this.sessions.values()]
|
|
.map((session) => this.publicRecord(session))
|
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
}
|
|
|
|
append(session, data) {
|
|
session.buffer += data;
|
|
if (Buffer.byteLength(session.buffer) > MAX_BUFFER_BYTES) {
|
|
session.buffer = session.buffer.slice(-MAX_BUFFER_BYTES);
|
|
}
|
|
if (!session.logStream) {
|
|
session.logStream = fs.createWriteStream(path.join(this.directory, `${session.id}.log`), {
|
|
flags: "a",
|
|
mode: 0o600,
|
|
});
|
|
}
|
|
session.logStream.write(data);
|
|
}
|
|
|
|
async readOutput(session) {
|
|
if (session.buffer) return session.buffer;
|
|
try {
|
|
return await fsp.readFile(path.join(this.directory, `${session.id}.log`), "utf8");
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
finish(session, status, exitCode = null, signal = null) {
|
|
session.status = status;
|
|
session.exitCode = exitCode;
|
|
session.signal = signal;
|
|
session.endedAt = new Date().toISOString();
|
|
session.logStream?.end();
|
|
session.logStream = undefined;
|
|
this.persist(session);
|
|
}
|
|
|
|
persist(session) {
|
|
const target = path.join(this.directory, `${session.id}.json`);
|
|
const temp = `${target}.tmp`;
|
|
const body = `${JSON.stringify(this.publicRecord(session), null, 2)}\n`;
|
|
fsp.writeFile(temp, body, { mode: 0o600 })
|
|
.then(() => fsp.rename(temp, target))
|
|
.catch(() => {});
|
|
}
|
|
}
|