mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
7d583df980
QA probe: with PLANNOTATOR_DATA_DIR on a read-only mount, `plannotator annotate file.md` crashed before the UI opened — twice. (1) #961's saveToHistory call on annotate open was unguarded, so the new version-diff enhancement gated startup that was stateless at v0.22.0; now wrapped (both the Bun server and the Pi mirror) and degrades to no-version-diff with a stderr notice. (2) registerSession's mkdir/writeFileSync (pre-existing since #795) threw after the server was already listening; the session registry only powers `plannotator sessions` discovery, so it is now best-effort, and unregisterSession/listSessions tolerate an unwritable sessions dir too. Verified: read-only data dir now starts, serves /api/plan with no versionInfo, and prints the session URL. Claude-Session: https://claude.ai/code/session_01SFy9fY27SA8g5BtotWPi1G
119 lines
2.7 KiB
TypeScript
119 lines
2.7 KiB
TypeScript
/**
|
|
* Session Registry
|
|
*
|
|
* Tracks active Plannotator server sessions in ~/.plannotator/sessions/
|
|
* so users can discover and reopen closed browser tabs.
|
|
*/
|
|
|
|
import { join } from "path";
|
|
import {
|
|
mkdirSync,
|
|
writeFileSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
unlinkSync,
|
|
existsSync,
|
|
} from "fs";
|
|
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";
|
|
|
|
export interface SessionInfo {
|
|
pid: number;
|
|
port: number;
|
|
url: string;
|
|
mode: "plan" | "review" | "annotate" | "archive" | "goal-setup";
|
|
project: string;
|
|
startedAt: string;
|
|
label: string;
|
|
}
|
|
|
|
function getSessionsDir(): string {
|
|
const dir = join(getPlannotatorDataDir(), "sessions");
|
|
mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function sessionPath(pid: number): string {
|
|
return join(getSessionsDir(), `${pid}.json`);
|
|
}
|
|
|
|
/**
|
|
* Check if a process is still alive.
|
|
*/
|
|
function isAlive(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register the current server session. Best-effort: the registry only powers
|
|
* `plannotator sessions` discovery, so an unwritable data dir (read-only
|
|
* mount, disk full) must never take the server down with it.
|
|
*/
|
|
export function registerSession(info: SessionInfo): void {
|
|
try {
|
|
writeFileSync(sessionPath(info.pid), JSON.stringify(info, null, 2), "utf-8");
|
|
} catch {
|
|
// Session discovery is unavailable; the session itself is unaffected.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregister the current process's session. No-op if not found.
|
|
*/
|
|
export function unregisterSession(pid: number = process.pid): void {
|
|
try {
|
|
const filePath = sessionPath(pid);
|
|
if (existsSync(filePath)) unlinkSync(filePath);
|
|
} catch {
|
|
// Ignore delete failures (including an unwritable sessions dir).
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all active sessions. Automatically removes stale entries.
|
|
*/
|
|
export function listSessions(): SessionInfo[] {
|
|
const active: SessionInfo[] = [];
|
|
|
|
let entries: string[];
|
|
let dir: string;
|
|
try {
|
|
dir = getSessionsDir();
|
|
entries = readdirSync(dir);
|
|
} catch {
|
|
return [];
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.endsWith(".json")) continue;
|
|
|
|
const filePath = join(dir, entry);
|
|
try {
|
|
const data: SessionInfo = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
|
|
if (isAlive(data.pid)) {
|
|
active.push(data);
|
|
} else {
|
|
// Stale session — clean up
|
|
try {
|
|
unlinkSync(filePath);
|
|
} catch {}
|
|
}
|
|
} catch {
|
|
// Corrupt file — remove it
|
|
try {
|
|
unlinkSync(filePath);
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
// Sort by most recent first
|
|
return active.sort(
|
|
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
);
|
|
}
|