import { classifyDistroId, detectVendorFromSshVersion, normalizeDistroId, } from "../../../domain/host"; import { logger } from "../../../lib/logger"; import type { TerminalSessionStartersContext } from "./createTerminalSessionStarters.types"; /** * Per-connection token for stale-timer detection. The renderer reuses the * same sessionId across reconnects within a tab, so comparing sessionIds * cannot distinguish "the current attempt" from "a previous attempt on * the same slot". We assign each startSSH call a fresh token object and * store it in this module-local map, keyed by sessionId. A timer that * was scheduled under an older token will see a different value here and * bail out. The map entry for a sessionId is overwritten on each new * connect and stays around until the app exits — since there is only one * entry per active session, the memory cost is negligible. */ const connectionTokensBySessionId = new Map(); export const isConnectionTokenCurrent = (sessionId: string, token: object): boolean => connectionTokensBySessionId.get(sessionId) === token; export const registerConnectionToken = (sessionId: string): object => { const connectionToken = {}; connectionTokensBySessionId.set(sessionId, connectionToken); return connectionToken; }; export const clearConnectionToken = (sessionId: string): void => { connectionTokensBySessionId.delete(sessionId); }; export const runDistroDetection = async ( ctx: TerminalSessionStartersContext, sessionId: string, connectionToken: object, ) => { // Stale-session guard: the renderer reuses ctx.sessionId across // reconnects in the same tab, so comparing sessionIds is not enough. // We compare against a per-connection token instead; if a newer // connect attempt has run, it will have replaced the token in the // module-level map and this check will fail. Repeated after every // await because the session can change during an async call. const isStillCurrent = () => isConnectionTokenCurrent(sessionId, connectionToken); if (!isStillCurrent()) return; const isKnownNetworkDevice = ctx.host.deviceType === "network" || classifyDistroId(ctx.host.distro) === "network-device"; // Step 1: try to classify from the SSH server identification string // captured at handshake time. This is free (no extra channel) and // reliably identifies most network-device vendors (Cisco IOS, Huawei // VRP, HPE Comware, MikroTik, Fortinet, etc.) so we can skip the // POSIX-shell probe entirely for those hosts — which otherwise fails // and, on devices like Cisco / Juniper with AAA logging, generates an // extra session log entry per connect. try { if (ctx.terminalBackend.getSessionRemoteInfo && sessionId) { const info = await ctx.terminalBackend.getSessionRemoteInfo(sessionId); if (!isStillCurrent()) return; if (!isKnownNetworkDevice && /^(?:SSH-(?:2\.0|1\.99)-)?OpenSSH_for_Windows(?:_|$)/i.test(info?.remoteSshVersion || '')) { ctx.onOsDetected?.(ctx.host.id, 'windows'); return; } const vendor = detectVendorFromSshVersion(info?.remoteSshVersion); if (vendor) { ctx.onOsDetected?.(ctx.host.id, vendor); return; } } } catch (err) { logger.warn("SSH banner vendor detection failed", err); } if (!isStillCurrent()) return; if (isKnownNetworkDevice) return; // Step 2: unknown or generic OpenSSH/Dropbear — fall back to the // /etc/os-release probe to pick a distro-specific icon. We deliberately // use `getSessionDistroInfo` which runs the probe on the *existing* // SSH connection's exec channel instead of spinning up a brand new // SSH client the way `execCommand` would. That saves a full handshake // round-trip on every connect, and on OpenSSH-fronted network devices // that we couldn't identify from the banner (JUNOS, NX-OS, EOS) it // avoids one extra AAA session log entry per connect. try { if (ctx.terminalBackend.getSessionDistroInfo && sessionId) { const res = await ctx.terminalBackend.getSessionDistroInfo(sessionId); if (!isStillCurrent()) return; if (!res?.success) return; const data = (res.stdout || "").trim(); const idMatch = data.match(/^ID="?([\w-]+)"?$/im); const rawDistro = idMatch ? idMatch[1] : (data.match(/^(Linux|Darwin|FreeBSD)\b/i)?.[1] || "").toLowerCase(); // An os-release ID confirms Linux even for distributions without a dedicated icon. const distro = normalizeDistroId(rawDistro) || (idMatch ? 'linux' : ''); if (distro) ctx.onOsDetected?.(ctx.host.id, distro); } } catch (err) { logger.warn("OS probe failed", err); } };