360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
import { invoke as tauriInvoke } from "@/lib/backend/api";
|
|
import { emit } from "@/lib/backend/api";
|
|
import { assertMatchingTemporaryConfig } from "@/lib/appWorkspace";
|
|
import { getErrorMessage } from "@/lib/errors";
|
|
import { invoke } from "@/lib/invoke";
|
|
import { getOwnerMainWindowLabel } from "@/lib/windowManager";
|
|
import { logger } from "@/lib/logger";
|
|
import {
|
|
buildTerminalCommandInput,
|
|
clearSessionCommandHistory,
|
|
sendSessionInput,
|
|
} from "@/lib/sessionInput";
|
|
import type { TemporaryLinkConfig } from "@/lib/temporaryLink";
|
|
import { captureTerminalReconnectContent } from "@/lib/terminalReconnectHistory";
|
|
import type {
|
|
SavedConnection,
|
|
SessionPane,
|
|
SessionType,
|
|
SshRuntimeMode,
|
|
WorkspaceSessionType,
|
|
} from "@/types/global";
|
|
|
|
export type StartupCommandRequest = {
|
|
command: string;
|
|
delayMs: number;
|
|
};
|
|
|
|
const CONNECTION_SESSION_TYPES: Record<
|
|
SavedConnection["type"],
|
|
WorkspaceSessionType
|
|
> = {
|
|
ssh: "SSH",
|
|
local_terminal: "Local",
|
|
telnet: "Telnet",
|
|
serial: "Serial",
|
|
rdp: "RDP",
|
|
vnc: "VNC",
|
|
};
|
|
|
|
export function getConnectionSessionType(
|
|
connection: Pick<SavedConnection, "type"> | null | undefined,
|
|
): WorkspaceSessionType {
|
|
return connection ? CONNECTION_SESSION_TYPES[connection.type] : "SSH";
|
|
}
|
|
|
|
export function getRemoteDesktopPaneDisplay(
|
|
connection: SavedConnection | null | undefined,
|
|
) {
|
|
if (connection?.type === "rdp") {
|
|
return {
|
|
remoteWidth: connection.display?.width ?? 1920,
|
|
remoteHeight: connection.display?.height ?? 1080,
|
|
scaleMode: connection.display?.mode === "fit-window" ? "fit" : "actual",
|
|
} as const;
|
|
}
|
|
if (connection?.type === "vnc") {
|
|
return {
|
|
scaleMode: connection.display?.scale_mode ?? "fit",
|
|
viewOnly: connection.view_only ?? false,
|
|
clipboardEnabled: connection.clipboard?.enabled ?? true,
|
|
} as const;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function isSessionCreationCancelled(error: unknown) {
|
|
return getErrorMessage(error)
|
|
.toLowerCase()
|
|
.includes("session creation cancelled");
|
|
}
|
|
|
|
export async function attachSessionBeforeClose(sessionId: string) {
|
|
await tauriInvoke("attach_session", { sessionId }).catch((error) => {
|
|
logger.debug({
|
|
domain: "session.lifecycle",
|
|
event: "session.attach_before_close_failed",
|
|
message: "Best-effort attach before close failed",
|
|
ids: { session_id: sessionId },
|
|
error,
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function closeStaleCreatedSession(sessionId: string) {
|
|
try {
|
|
await attachSessionBeforeClose(sessionId);
|
|
await invoke("close_session", { sessionId });
|
|
await invoke("finish_recording_scope", { scopeId: sessionId });
|
|
clearSessionCommandHistory(sessionId);
|
|
} catch (error) {
|
|
logger.error({
|
|
domain: "session.lifecycle",
|
|
event: "session.stale_close_failed",
|
|
message: "Failed to close stale created session",
|
|
ids: { session_id: sessionId },
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function createSessionForConnection(
|
|
connection: Pick<SavedConnection, "id" | "type">,
|
|
createRequestId?: string,
|
|
startupCommand?: StartupCommandRequest,
|
|
runtimeModeOverride?: SshRuntimeMode,
|
|
recordingScopeId?: string,
|
|
) {
|
|
switch (connection.type) {
|
|
case "local_terminal":
|
|
return invoke<string>("create_local_session", {
|
|
connectionId: connection.id,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
});
|
|
case "telnet":
|
|
return invoke<string>("create_telnet_session", {
|
|
connectionId: connection.id,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
});
|
|
case "serial":
|
|
return invoke<string>("create_serial_session", {
|
|
connectionId: connection.id,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
});
|
|
case "vnc":
|
|
return invoke<string>("create_vnc_session", {
|
|
ownerWindowLabel: getOwnerMainWindowLabel(),
|
|
connectionId: connection.id,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
});
|
|
case "rdp":
|
|
return invoke<string>("create_rdp_session", {
|
|
connectionId: connection.id,
|
|
createRequestId,
|
|
});
|
|
default:
|
|
return invoke<string>("create_ssh_session", {
|
|
connectionId: connection.id,
|
|
createRequestId,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
runtimeMode: runtimeModeOverride,
|
|
recordingScopeId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function createTemporarySession(
|
|
config: TemporaryLinkConfig,
|
|
createRequestId?: string,
|
|
startupCommand?: StartupCommandRequest,
|
|
recordingScopeId?: string,
|
|
) {
|
|
switch (config.protocol) {
|
|
case "telnet":
|
|
return invoke<string>("create_telnet_session", {
|
|
connectionId: null,
|
|
host: config.host,
|
|
port: config.port,
|
|
name: config.name,
|
|
...(config.network ? { network: config.network } : {}),
|
|
...(config.encoding ? { encoding: config.encoding } : {}),
|
|
createRequestId,
|
|
recordingScopeId,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
});
|
|
case "serial":
|
|
return invoke<string>("create_serial_session", {
|
|
connectionId: null,
|
|
portName: config.portName,
|
|
baudRate: config.baudRate,
|
|
name: config.name,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
});
|
|
default: {
|
|
const { protocol: _protocol, ...sshConfig } = config;
|
|
return invoke<string>("create_temporary_ssh_session", {
|
|
config: sshConfig,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function createExternalLocalSession(
|
|
workingDir: string | null,
|
|
createRequestId?: string,
|
|
recordingScopeId?: string,
|
|
) {
|
|
return invoke<string>("create_local_session", {
|
|
connectionId: null,
|
|
createRequestId,
|
|
recordingScopeId,
|
|
workingDir,
|
|
});
|
|
}
|
|
|
|
export function createSessionForPane(
|
|
pane: Pick<
|
|
SessionPane,
|
|
"id" | "type" | "connectionId" | "temporaryConfig" | "sshRuntimeMode"
|
|
>,
|
|
createRequestId?: string,
|
|
startupCommand?: StartupCommandRequest,
|
|
workingDir?: string,
|
|
) {
|
|
switch (pane.type) {
|
|
case "Local":
|
|
return invoke<string>("create_local_session", {
|
|
connectionId: pane.connectionId || null,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
...(workingDir === undefined ? {} : { workingDir }),
|
|
});
|
|
case "Telnet":
|
|
if (pane.connectionId) {
|
|
return invoke<string>("create_telnet_session", {
|
|
connectionId: pane.connectionId,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
runtimeMode: pane.sshRuntimeMode,
|
|
});
|
|
}
|
|
assertMatchingTemporaryConfig(pane);
|
|
if (pane.temporaryConfig?.protocol === "telnet") {
|
|
return invoke<string>("create_telnet_session", {
|
|
connectionId: null,
|
|
host: pane.temporaryConfig.host,
|
|
port: pane.temporaryConfig.port,
|
|
name: pane.temporaryConfig.name,
|
|
...(pane.temporaryConfig.network ? { network: pane.temporaryConfig.network } : {}),
|
|
...(pane.temporaryConfig.encoding ? { encoding: pane.temporaryConfig.encoding } : {}),
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
});
|
|
}
|
|
throw new Error("Missing Telnet connection id");
|
|
case "Serial":
|
|
if (pane.connectionId) {
|
|
return invoke<string>("create_serial_session", {
|
|
connectionId: pane.connectionId,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
});
|
|
}
|
|
assertMatchingTemporaryConfig(pane);
|
|
if (pane.temporaryConfig?.protocol === "serial") {
|
|
return invoke<string>("create_serial_session", {
|
|
connectionId: null,
|
|
portName: pane.temporaryConfig.portName,
|
|
baudRate: pane.temporaryConfig.baudRate,
|
|
name: pane.temporaryConfig.name,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
});
|
|
}
|
|
throw new Error("Missing Serial connection id");
|
|
case "VNC":
|
|
if (!pane.connectionId) throw new Error("Missing VNC connection id");
|
|
return invoke<string>("create_vnc_session", {
|
|
ownerWindowLabel: getOwnerMainWindowLabel(),
|
|
connectionId: pane.connectionId,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
});
|
|
case "RDP":
|
|
if (!pane.connectionId) throw new Error("Missing RDP connection id");
|
|
return invoke<string>("create_rdp_session", {
|
|
connectionId: pane.connectionId,
|
|
createRequestId,
|
|
});
|
|
default:
|
|
if (pane.connectionId) {
|
|
return invoke<string>("create_ssh_session", {
|
|
connectionId: pane.connectionId,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
runtimeMode: pane.sshRuntimeMode,
|
|
});
|
|
}
|
|
assertMatchingTemporaryConfig(pane);
|
|
if (pane.temporaryConfig?.protocol === "ssh") {
|
|
const { protocol: _protocol, ...sshConfig } = pane.temporaryConfig;
|
|
return invoke<string>("create_temporary_ssh_session", {
|
|
config: sshConfig,
|
|
createRequestId,
|
|
recordingScopeId: pane.id,
|
|
startupCommand: buildStartupCommandPayload(startupCommand),
|
|
});
|
|
}
|
|
throw new Error("Missing SSH connection id");
|
|
}
|
|
}
|
|
|
|
export function getTemporaryLinkSessionType(
|
|
config: TemporaryLinkConfig,
|
|
): SessionType {
|
|
switch (config.protocol) {
|
|
case "telnet":
|
|
return "Telnet";
|
|
case "serial":
|
|
return "Serial";
|
|
default:
|
|
return "SSH";
|
|
}
|
|
}
|
|
|
|
export function buildStartupCommandPayload(
|
|
startupCommand?: StartupCommandRequest,
|
|
) {
|
|
if (!startupCommand || !startupCommand.command.trim()) return null;
|
|
return {
|
|
command: startupCommand.command,
|
|
delayMs: Math.max(0, Math.min(60000, Math.round(startupCommand.delayMs))),
|
|
};
|
|
}
|
|
|
|
export async function sendStartupCommandToSession(
|
|
sessionId: string,
|
|
startupCommand: StartupCommandRequest,
|
|
) {
|
|
const delayMs = Math.max(
|
|
0,
|
|
Math.min(60000, Math.round(startupCommand.delayMs)),
|
|
);
|
|
if (delayMs > 0) {
|
|
await new Promise((resolve) => window.setTimeout(resolve, delayMs));
|
|
}
|
|
await sendSessionInput(
|
|
sessionId,
|
|
buildTerminalCommandInput(startupCommand.command),
|
|
{
|
|
preview: { kind: "reset" },
|
|
registerSubmission: startupCommand.command,
|
|
origin: "startup_command",
|
|
},
|
|
);
|
|
}
|
|
|
|
export function focusTerminalSession(sessionId?: string | null) {
|
|
if (!sessionId) return;
|
|
requestAnimationFrame(() => {
|
|
void emit(`focus-terminal-${sessionId}`);
|
|
});
|
|
}
|
|
|
|
export function capturePaneReconnectContent(pane: SessionPane) {
|
|
if (pane.connecting || pane.connectError) return null;
|
|
return captureTerminalReconnectContent(pane.sessionId);
|
|
}
|