refactor: 连接终端前端重构.

This commit is contained in:
lijiahang
2024-01-05 19:49:48 +08:00
parent 1174daa09b
commit 98b014bb40
15 changed files with 503 additions and 64 deletions

View File

@@ -0,0 +1,102 @@
import { OutputProtocol } from '@/views/host/terminal/types/terminal.protocol';
import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, OutputPayload, Protocol, } from '@/views/host/terminal/types/terminal.type';
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
// 终端通信处理器 实现
export default class TerminalChannel implements ITerminalChannel {
private readonly processor;
constructor(processor: ITerminalOutputProcessor) {
this.processor = processor;
}
send(protocol: Protocol, payload: InputPayload): void {
}
// 初始化
async init() {
}
// 处理消息
handlerMessage({ data }: MessageEvent) {
// 解析消息
const payload = parse(data as string);
if (!payload) {
return;
}
// 消息调度
switch (payload.type) {
case OutputProtocol.CHECK.type:
// 检查 回调
this.processor.processCheck(payload);
break;
case OutputProtocol.CONNECT.type:
// 连接 回调
this.processor.processConnect(payload);
break;
case OutputProtocol.PONG.type:
// pong 回调
this.processor.processPong(payload);
break;
case OutputProtocol.OUTPUT.type:
// 输出 回调
this.processor.processOutput(payload);
break;
default:
break;
}
}
close(): void {
}
}
// 分隔符
export const SEPARATOR = '|';
// 解析参数
export const parse = (payload: string) => {
const protocols = Object.values(OutputProtocol);
const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload);
if (!useProtocol) {
return undefined;
}
const template = useProtocol.template;
const res = {} as OutputPayload;
let curr = 0;
let len = payload.length;
for (let i = 0, pl = template.length; i < pl; i++) {
if (i == pl - 1) {
// 最后一次
res[template[i]] = payload.substring(curr, len);
} else {
// 非最后一次
let tmp = '';
for (; curr < len; curr++) {
const c = payload.charAt(curr);
if (c == SEPARATOR) {
res[template[i]] = tmp;
curr++;
break;
} else {
tmp += c;
}
}
}
}
return res;
};
// 格式化参数
export const format = (protocol: Protocol, payload: InputPayload) => {
payload.type = protocol.type;
return protocol.template
.map(i => payload[i] || '')
.join(SEPARATOR);
};

View File

@@ -2,12 +2,13 @@ import type { ITerminalDispatcher, ITerminalHandler, TerminalTabItem } from '@/s
import type { HostQueryResponse } from '@/api/asset/host';
import type { HostTerminalAccessResponse } from '@/api/asset/host-terminal';
import { getHostTerminalAccessToken } from '@/api/asset/host-terminal';
import { InnerTabs, TabType } from '@/views/host/terminal/types/terminal.const';
import { TabType } from '@/views/host/terminal/types/terminal.const';
import { Message } from '@arco-design/web-vue';
import { sleep } from '@/utils';
import { format, InputProtocol, OutputProtocol, parse } from '../types/terminal.protocol';
import { format, InputProtocol, OutputProtocol, parse, Payload } from '../types/terminal.protocol';
import { useDebounceFn } from '@vueuse/core';
import { addEventListen, removeEventListen } from '@/utils/event';
import { useTerminalStore } from '@/store';
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
@@ -20,10 +21,6 @@ export const wsBase = import.meta.env.VITE_WS_BASE_URL;
*/
export default class TerminalDispatcher implements ITerminalDispatcher {
public active: string;
public items: Array<TerminalTabItem>;
private access?: HostTerminalAccessResponse;
private client?: WebSocket;
@@ -35,42 +32,10 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
private readonly dispatchResizeFn: () => {};
constructor() {
this.active = InnerTabs.NEW_CONNECTION.key;
this.items = [InnerTabs.NEW_CONNECTION];
this.handlers = {};
this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this);
}
// 点击 tab
clickTab(key: string): void {
this.active = key;
}
// 删除 tab
deleteTab(key: string): void {
// 获取当前 tab
const tabIndex = this.items.findIndex(s => s.key === key);
if (this.items[tabIndex]?.type === TabType.TERMINAL) {
// 如果是 terminal 则需要关闭
this.closeTerminal(key);
}
// 删除 tab
this.items.splice(tabIndex, 1);
if (key === this.active && this.items.length !== 0) {
// 切换为前一个 tab
this.active = this.items[Math.max(tabIndex - 1, 0)].key;
}
// fixme 关闭 socket
}
// 打开 tab
openTab(tab: TerminalTabItem): void {
// 不存在则创建 tab
if (!this.items.find(s => s.key === tab.key)) {
this.items.push(tab);
}
this.active = tab.key;
}
// 初始化客户端
async initClient() {
@@ -92,7 +57,7 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
this.client.onmessage = this.handlerMessage.bind(this);
// 注册 ping 事件
this.pingTask = setInterval(() => {
this.client?.send(format(InputProtocol.PING, {}));
this.client?.send(format(InputProtocol.PING, {} as Payload));
}, 150000);
// 注册 resize 事件
addEventListen(window, 'resize', this.dispatchResizeFn);
@@ -140,7 +105,7 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
}
const session = this.access.sessionInitial = (parseInt(this.access.sessionInitial as string, 32) + 1).toString(32);
// 打开会话
this.openTab({
useTerminalStore().tabs.openTab({
type: TabType.TERMINAL,
key: session,
title: record.alias || (`${record.name} ${record.address}`),
@@ -211,8 +176,6 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
// 重置
reset(): void {
this.active = undefined as unknown as string;
this.items = [];
this.access = undefined;
this.handlers = {};
// 关闭 client

View File

@@ -0,0 +1,49 @@
import { ITerminalChannel, ITerminalOutputProcessor, OutputPayload, } from '@/views/host/terminal/types/terminal.type';
import TerminalChannel from '@/views/host/terminal/handler/terminal-channel';
// 终端调度器实现
export default class TerminalOutputProcessor implements ITerminalOutputProcessor {
private readonly channel: ITerminalChannel;
constructor() {
this.channel = new TerminalChannel(this);
}
// 处理检查消息
processCheck(payload: OutputPayload): void {
// const success = !!Number.parseInt(payload.result);
// const handler = this.handlers[session];
// // 未成功展示错误信息
// if (!success) {
// handler.write('' + errormessage + '');
// return;
// }
// // 发送 connect 命令
// this.channel.send(InputProtocol.CONNECT, { session, cols: handler.inst.cols, rows: handler.inst.rows });
}
// 处理连接消息
processConnect(payload: OutputPayload): void {
const success = !!Number.parseInt(payload.result);
// const handler = this.handlers[session];
// // 未成功展示错误信息
// if (!success) {
// handler.write('' + errormessage + '');
// return;
// }
// // 设置可写
// handler.setCanWrite(true);
// handler.connect();
}
// 处理 pong 消息
processPong(payload: OutputPayload): void {
}
// 处理输出消息
processOutput(payload: OutputPayload): void {
// this.handlers[session].write(body);
}
}

View File

@@ -0,0 +1,58 @@
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
import type { TerminalTabItem } from '@/store/modules/terminal/types';
import { sleep } from '@/utils';
import TerminalSession from './terminal-session';
// 终端会话管理器定义
export interface ITerminalSessionManager {
// 打开终端
openSession: (tab: TerminalTabItem, dom: HTMLElement) => void;
// 获取终端会话
getSession: (sessionId: string) => ITerminalSession;
// 重置
reset: () => void;
}
// FIXME 去除 TOKEN 起始量
// 终端会话管理器实现
export default class TerminalSessionManager implements ITerminalSessionManager {
private readonly channel: ITerminalChannel;
private sessions: Record<string, ITerminalSession>;
constructor(channel: ITerminalChannel) {
this.channel = channel;
this.sessions = {};
}
// 打开终端会话
async openSession(tab: TerminalTabItem, dom: HTMLElement) {
// 初始化客户端
await this.channel.init();
// 新建会话
const session = new TerminalSession(
tab.hostId as number,
tab.key,
this.channel
);
// 初始化
session.init(dom);
// 等待前端渲染完成
await sleep(100);
// 添加会话
this.sessions[tab.key] = session;
}
// 获取终端会话
getSession(sessionId: string): ITerminalSession {
return this.sessions[sessionId];
}
// 重置
reset(): void {
this.sessions = {};
}
}

View File

@@ -0,0 +1,119 @@
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
import { useTerminalStore } from '@/store';
import { fontFamilySuffix } from '@/views/host/terminal/types/terminal.const';
import { InputProtocol } from '@/views/host/terminal/types/terminal.protocol';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl';
// 终端插件
export interface TerminalAddons {
fit: FitAddon;
webgl: WebglAddon;
}
// 终端会话实现
export default class TerminalSession implements ITerminalSession {
public hostId: number;
public inst: Terminal;
public connected: boolean;
private canWrite: boolean;
private readonly sessionId: string;
private readonly channel: ITerminalChannel;
private readonly addons: TerminalAddons;
constructor(hostId: number,
sessionId: string,
channel: ITerminalChannel) {
this.hostId = hostId;
this.sessionId = sessionId;
this.channel = channel;
this.connected = false;
this.canWrite = false;
this.inst = undefined as unknown as Terminal;
this.addons = {} as TerminalAddons;
}
// 初始化
init(dom: HTMLElement): void {
const { preference } = useTerminalStore();
// 初始化实例
this.inst = new Terminal({
...(preference.displaySetting as any),
theme: preference.themeSchema,
fastScrollModifier: 'shift',
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
});
// 注册插件
this.addons.fit = new FitAddon();
this.addons.webgl = new WebglAddon();
// TODO check
const inst = this.inst;
Object.values(this.addons).forEach(s => inst.loadAddon(s));
// 打开终端
this.inst.open(dom);
// 自适应
this.addons.fit.fit();
// TODO sendCheck
}
// 设置已连接
connect(): void {
this.connected = true;
// 注册输入事件
this.inst.onData(s => {
if (!this.canWrite) {
return;
}
// 输入
this.channel.send(InputProtocol.INPUT, {
session: this.sessionId,
command: s
});
});
// 注册 resize 事件
this.inst.onResize(({ cols, rows }) => {
this.channel.send(InputProtocol.RESIZE, {
session: this.sessionId,
cols,
rows
});
});
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
this.canWrite = canWrite;
}
// 写入数据
write(value: string): void {
this.inst.write(value);
}
// 自适应
fit(): void {
this.addons.fit?.fit();
}
// 关闭
close(): void {
try {
// 卸载插件
Object.values(this.addons)
.filter(Boolean)
.forEach(s => s.dispose());
// 卸载实体
this.inst.dispose();
} catch (e) {
}
}
}

View File

@@ -0,0 +1,49 @@
import type { ITerminalTabManager, TerminalTabItem } from '@/store/modules/terminal/types';
import { InnerTabs } from '@/views/host/terminal/types/terminal.const';
// 终端 tab 管理器实现
export default class TerminalTabManager implements ITerminalTabManager {
public active: string;
public items: Array<TerminalTabItem>;
constructor() {
this.active = InnerTabs.NEW_CONNECTION.key;
this.items = [InnerTabs.NEW_CONNECTION];
}
// 点击 tab
clickTab(key: string): void {
this.active = key;
}
// 删除 tab
deleteTab(key: string): void {
// 获取当前 tab
const tabIndex = this.items.findIndex(s => s.key === key);
// 删除 tab
this.items.splice(tabIndex, 1);
if (key === this.active && this.items.length !== 0) {
// 切换为前一个 tab
this.active = this.items[Math.max(tabIndex - 1, 0)].key;
}
// fixme 关闭 ws
}
// 打开 tab
openTab(tab: TerminalTabItem): void {
// 不存在则创建 tab
if (!this.items.find(s => s.key === tab.key)) {
this.items.push(tab);
}
this.active = tab.key;
}
// 清空
clear() {
this.active = undefined as unknown as string;
this.items = [];
}
}