add disconnect reasons to dashboard and fix icons in sideview
This commit is contained in:
@@ -469,6 +469,38 @@ func getPingInterval() time.Duration {
|
|||||||
return time.Duration(ms) * time.Millisecond
|
return time.Duration(ms) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func classifySessionError(err error) (reason, detail string) {
|
||||||
|
if err == nil {
|
||||||
|
return "crash", ""
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
msgLower := strings.ToLower(msg)
|
||||||
|
if strings.Contains(msgLower, "panic") {
|
||||||
|
return "panic", truncateStr(msg, 300)
|
||||||
|
}
|
||||||
|
var closeErr *websocket.CloseError
|
||||||
|
if errors.As(err, &closeErr) {
|
||||||
|
if closeErr.Code == websocket.StatusNormalClosure || closeErr.Code == websocket.StatusGoingAway {
|
||||||
|
return "normal", ""
|
||||||
|
}
|
||||||
|
return "network", fmt.Sprintf("ws close code=%d reason=%s", closeErr.Code, truncateStr(closeErr.Reason, 100))
|
||||||
|
}
|
||||||
|
if strings.Contains(msgLower, "timed out from inactivity") || strings.Contains(msgLower, "pong timeout") {
|
||||||
|
return "timeout", truncateStr(msg, 300)
|
||||||
|
}
|
||||||
|
if strings.Contains(msgLower, "context canceled") || strings.Contains(msgLower, "context deadline exceeded") {
|
||||||
|
return "normal", ""
|
||||||
|
}
|
||||||
|
return "network", truncateStr(msg, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateStr(s string, n int) string {
|
||||||
|
if len(s) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:n] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, cfg config.Config) (err error) {
|
func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.Conn, cfg config.Config) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@@ -480,7 +512,14 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
defer conn.Close(websocket.StatusNormalClosure, "bye")
|
defer func() {
|
||||||
|
reason, detail := classifySessionError(err)
|
||||||
|
di := wire.DisconnectInfo{Type: "disconnect_info", Reason: reason, Detail: detail}
|
||||||
|
sendCtx, sendCancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer sendCancel()
|
||||||
|
_ = wire.WriteMsg(sendCtx, conn, di)
|
||||||
|
conn.Close(websocket.StatusNormalClosure, "bye")
|
||||||
|
}()
|
||||||
|
|
||||||
identity := config.DeriveIdentity()
|
identity := config.DeriveIdentity()
|
||||||
log.Printf("[purgatory] identity fingerprint=%s", identity.Fingerprint)
|
log.Printf("[purgatory] identity fingerprint=%s", identity.Fingerprint)
|
||||||
|
|||||||
@@ -248,3 +248,9 @@ type ProxyClose struct {
|
|||||||
Type string `msgpack:"type"`
|
Type string `msgpack:"type"`
|
||||||
ConnectionID string `msgpack:"connectionId"`
|
ConnectionID string `msgpack:"connectionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DisconnectInfo struct {
|
||||||
|
Type string `msgpack:"type"`
|
||||||
|
Reason string `msgpack:"reason"` // "normal", "panic", "crash", "network", "timeout"
|
||||||
|
Detail string `msgpack:"detail,omitempty"` // error message
|
||||||
|
}
|
||||||
|
|||||||
@@ -2184,6 +2184,7 @@ body.sb-collapsed .sb-collapse-btn i {
|
|||||||
body.sb-collapsed .sb-link {
|
body.sb-collapsed .sb-link {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
|
gap: 0;
|
||||||
}
|
}
|
||||||
.sb-icon {
|
.sb-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -266,6 +266,14 @@ export function createRenderer({
|
|||||||
<i class="fa-solid fa-circle"></i>
|
<i class="fa-solid fa-circle"></i>
|
||||||
${client.online ? "Online" : "Offline"}
|
${client.online ? "Online" : "Offline"}
|
||||||
</span>
|
</span>
|
||||||
|
${!client.online && client.disconnectReason && client.disconnectReason !== "normal" ? (() => {
|
||||||
|
const iconMap = { panic: "fa-skull-crossbones", crash: "fa-skull", timeout: "fa-clock", network: "fa-plug-circle-xmark" };
|
||||||
|
const colorMap = { panic: "text-red-400", crash: "text-red-400", timeout: "text-amber-400", network: "text-slate-400" };
|
||||||
|
const icon = iconMap[client.disconnectReason] || "fa-circle-exclamation";
|
||||||
|
const color = colorMap[client.disconnectReason] || "text-slate-400";
|
||||||
|
const detail = client.disconnectDetail ? escapeHtml(client.disconnectDetail) : "";
|
||||||
|
return `<span class="pill pill-ghost text-xs ${color}" ${detail ? `title="${detail}"` : ""}><i class="fa-solid ${icon}"></i> ${escapeHtml(client.disconnectReason)}</span>`;
|
||||||
|
})() : ""}
|
||||||
</div>
|
</div>
|
||||||
${hasTagNote ? `<div class="client-tag-note rounded-lg border border-amber-900/60 bg-amber-950/20 px-3 py-2 text-sm text-amber-100 whitespace-pre-wrap break-words max-h-48 overflow-auto ${isTagNoteExpanded ? "" : "hidden"}">${escapeHtml(customTagNote)}</div>` : ""}
|
${hasTagNote ? `<div class="client-tag-note rounded-lg border border-amber-900/60 bg-amber-950/20 px-3 py-2 text-sm text-amber-100 whitespace-pre-wrap break-words max-h-48 overflow-auto ${isTagNoteExpanded ? "" : "hidden"}">${escapeHtml(customTagNote)}</div>` : ""}
|
||||||
<div class="flex items-center gap-2 flex-wrap text-sm text-slate-300">
|
<div class="flex items-center gap-2 flex-wrap text-sm text-slate-300">
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ db.run(
|
|||||||
db.run(
|
db.run(
|
||||||
`CREATE INDEX IF NOT EXISTS idx_clients_ping_ms ON clients(ping_ms);`,
|
`CREATE INDEX IF NOT EXISTS idx_clients_ping_ms ON clients(ping_ms);`,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
db.run(`ALTER TABLE clients ADD COLUMN disconnect_reason TEXT`);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
db.run(`ALTER TABLE clients ADD COLUMN disconnect_detail TEXT`);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
db.run(`
|
db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS banned_ips (
|
CREATE TABLE IF NOT EXISTS banned_ips (
|
||||||
@@ -304,13 +310,22 @@ export function upsertClientRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setOnlineState(id: string, online: boolean) {
|
export function setOnlineState(id: string, online: boolean, disconnectReason?: string, disconnectDetail?: string) {
|
||||||
db.run(
|
if (online) {
|
||||||
`UPDATE clients SET online=?, last_seen=? WHERE id=?`,
|
db.run(
|
||||||
online ? 1 : 0,
|
`UPDATE clients SET online=1, last_seen=?, disconnect_reason=NULL, disconnect_detail=NULL WHERE id=?`,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
db.run(
|
||||||
|
`UPDATE clients SET online=0, last_seen=?, disconnect_reason=?, disconnect_detail=? WHERE id=?`,
|
||||||
|
Date.now(),
|
||||||
|
disconnectReason ?? null,
|
||||||
|
disconnectDetail ?? null,
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteClientRow(id: string) {
|
export function deleteClientRow(id: string) {
|
||||||
@@ -541,7 +556,7 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
|
|
||||||
const rows = db
|
const rows = db
|
||||||
.query<any>(
|
.query<any>(
|
||||||
`SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint, cpu, gpu, ram
|
`SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint, cpu, gpu, ram, disconnect_reason as disconnectReason, disconnect_detail as disconnectDetail
|
||||||
FROM clients
|
FROM clients
|
||||||
${whereSql}
|
${whereSql}
|
||||||
${orderBy}
|
${orderBy}
|
||||||
@@ -573,6 +588,8 @@ export function listClients(filters: ListFilters): ListResult {
|
|||||||
cpu: c.cpu || null,
|
cpu: c.cpu || null,
|
||||||
gpu: c.gpu || null,
|
gpu: c.gpu || null,
|
||||||
ram: c.ram || null,
|
ram: c.ram || null,
|
||||||
|
disconnectReason: c.disconnectReason || null,
|
||||||
|
disconnectDetail: c.disconnectDetail || null,
|
||||||
thumbnail: getThumbnail(c.id),
|
thumbnail: getThumbnail(c.id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -555,6 +555,16 @@ export async function handleWebSocketMessage(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "disconnect_info": {
|
||||||
|
const reason = typeof (payload as any).reason === "string" ? (payload as any).reason : "";
|
||||||
|
const detail = typeof (payload as any).detail === "string" ? (payload as any).detail : "";
|
||||||
|
if (reason) {
|
||||||
|
ws.data.disconnectReason = reason;
|
||||||
|
ws.data.disconnectDetail = detail || undefined;
|
||||||
|
logger.debug(`[disconnect_info] ${client.id} reason=${reason}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -709,15 +719,18 @@ export function handleWebSocketClose(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storedDisconnectReason = ws.data.disconnectReason;
|
||||||
|
const storedDisconnectDetail = ws.data.disconnectDetail;
|
||||||
|
|
||||||
clientManager.deleteClient(clientId);
|
clientManager.deleteClient(clientId);
|
||||||
stopAllProxiesForClient(clientId);
|
stopAllProxiesForClient(clientId);
|
||||||
clearClientSyncState(clientId);
|
clearClientSyncState(clientId);
|
||||||
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
deps.notifyConsoleClosed(clientId, "Client disconnected");
|
||||||
setOnlineState(clientId, false);
|
setOnlineState(clientId, false, storedDisconnectReason, storedDisconnectDetail);
|
||||||
deps.clearPendingNotificationScreenshots(clientId);
|
deps.clearPendingNotificationScreenshots(clientId);
|
||||||
deps.clearClientPluginState(clientId);
|
deps.clearClientPluginState(clientId);
|
||||||
deps.notifyDashboard();
|
deps.notifyDashboard();
|
||||||
logger.info(`[close] ${clientId} code=${code} reason=${reason}`);
|
logger.info(`[close] ${clientId} code=${code} reason=${reason} disconnect_reason=${storedDisconnectReason || "unknown"}`);
|
||||||
|
|
||||||
if (role === "client") {
|
if (role === "client") {
|
||||||
deps.notifyRemoteDesktopStatus(clientId, "offline", "Client disconnected");
|
deps.notifyRemoteDesktopStatus(clientId, "offline", "Client disconnected");
|
||||||
@@ -729,7 +742,7 @@ export function handleWebSocketClose(
|
|||||||
action: AuditAction.CLIENT_DISCONNECT,
|
action: AuditAction.CLIENT_DISCONNECT,
|
||||||
targetClientId: clientId,
|
targetClientId: clientId,
|
||||||
success: true,
|
success: true,
|
||||||
details: JSON.stringify({ code, reason: String(reason || "") }),
|
details: JSON.stringify({ code, reason: String(reason || ""), disconnectReason: storedDisconnectReason || null }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export type SocketData = {
|
|||||||
autoTasksRan?: boolean;
|
autoTasksRan?: boolean;
|
||||||
autoDeploysRan?: boolean;
|
autoDeploysRan?: boolean;
|
||||||
enrollmentNonce?: string;
|
enrollmentNonce?: string;
|
||||||
|
disconnectReason?: string;
|
||||||
|
disconnectDetail?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConsoleSession = {
|
export type ConsoleSession = {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export type ClientInfo = {
|
|||||||
enrollmentStatus?: EnrollmentStatus;
|
enrollmentStatus?: EnrollmentStatus;
|
||||||
publicKey?: string;
|
publicKey?: string;
|
||||||
keyFingerprint?: string;
|
keyFingerprint?: string;
|
||||||
|
disconnectReason?: string;
|
||||||
|
disconnectDetail?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListFilters = {
|
export type ListFilters = {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
|||||||
"clipboard_content",
|
"clipboard_content",
|
||||||
"proxy_data",
|
"proxy_data",
|
||||||
"proxy_close",
|
"proxy_close",
|
||||||
|
"disconnect_info",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function isAllowedClientMessageType(type: string): boolean {
|
export function isAllowedClientMessageType(type: string): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user