diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/manager/TerminalManager.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/manager/TerminalManager.java index e55ad238..37909609 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/manager/TerminalManager.java +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/manager/TerminalManager.java @@ -66,7 +66,7 @@ public class TerminalManager { public void closeAll(String channelId) { // 获取并移除 ConcurrentHashMap session = channelSessions.remove(channelId); - if (Maps.isEmpty(session)) { + if (!Maps.isEmpty(session)) { session.values().forEach(Streams::close); } } diff --git a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/session/TerminalSession.java b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/session/TerminalSession.java index 683cb8b8..36449dca 100644 --- a/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/session/TerminalSession.java +++ b/orion-ops-module-asset/orion-ops-module-asset-service/src/main/java/com/orion/ops/module/asset/handler/host/terminal/session/TerminalSession.java @@ -77,10 +77,14 @@ public class TerminalSession implements ITerminalSession { if (!executor.isConnected()) { executor.connect(); } - config.setCols(cols); - config.setRows(rows); - executor.size(cols, rows); - executor.resize(); + // 大小发生变化 则修改大小 + if (cols != config.getCols() || + rows != config.getRows()) { + config.setCols(cols); + config.setRows(rows); + executor.size(cols, rows); + executor.resize(); + } } @Override diff --git a/orion-ops-ui/src/store/modules/terminal/types.ts b/orion-ops-ui/src/store/modules/terminal/types.ts index f0af3fe4..b7cae650 100644 --- a/orion-ops-ui/src/store/modules/terminal/types.ts +++ b/orion-ops-ui/src/store/modules/terminal/types.ts @@ -1,4 +1,5 @@ import type { Ref } from 'vue'; +import { Terminal } from 'xterm'; export interface TerminalState { isDarkTheme: Ref; @@ -80,9 +81,29 @@ export interface ITerminalDispatcher { openTab: (tab: TerminalTabItem) => void; // 打开终端 openTerminal: (record: any) => void; - // 注册终端钩子 - registerTerminalHook: (tab: TerminalTabItem) => void; + // 注册终端处理器 + registerTerminalHandler: (tab: TerminalTabItem, handler: ITerminalHandler) => void; + // 发送消息 + onMessage: (session: string, value: string) => void; // 重置 reset: () => void; } + +// 终端处理器 +export interface ITerminalHandler { + inst: Terminal; + connected: boolean; + + // 连接 + connect: () => void; + // 设置是否可写 + setCanWrite: (canWrite: boolean) => void; + // 写入数据 + write: (value: string) => void; + // 自适应 + fit: () => void; + + // 关闭 + close: () => void; +} diff --git a/orion-ops-ui/src/views/host/terminal/components/xterm/terminal-view.vue b/orion-ops-ui/src/views/host/terminal/components/xterm/terminal-view.vue index 46977178..80e916e1 100644 --- a/orion-ops-ui/src/views/host/terminal/components/xterm/terminal-view.vue +++ b/orion-ops-ui/src/views/host/terminal/components/xterm/terminal-view.vue @@ -21,9 +21,8 @@ import type { TerminalTabItem } from '@/store/modules/terminal/types'; import { onMounted, ref } from 'vue'; import { useTerminalStore } from '@/store'; - import { FitAddon } from 'xterm-addon-fit'; - import { WebglAddon } from 'xterm-addon-webgl'; - import { Terminal } from 'xterm'; + import TerminalHandler from '@/views/host/terminal/handler/TerminalHandler'; + import { sleep } from '@/utils'; const props = defineProps<{ tab: TerminalTabItem @@ -34,27 +33,13 @@ const terminalRef = ref(); // 初始化 - const init = () => { - // FIXME fontfamily - // 初始化终端 - const term = new Terminal({ - theme: preference.themeSchema, - fastScrollModifier: 'shift', - ...(preference.displaySetting as any), - }); - // 注册插件 - const fitAddon = new FitAddon(); - const webglAddon = new WebglAddon(); - term.loadAddon(fitAddon); - term.loadAddon(webglAddon); - // 打开终端 - term.open(terminalRef.value); - // 自适应 - fitAddon.fit(); - // 注册钩子 - dispatcher.registerTerminalHook(props.tab); - // 初始化终端 - + const init = async () => { + // 创建终端处理器 + const handler = new TerminalHandler(props.tab.key, terminalRef.value); + // 等待前端渲染完成 + await sleep(100); + // 注册处理器 + dispatcher.registerTerminalHandler(props.tab, handler); }; onMounted(init); diff --git a/orion-ops-ui/src/views/host/terminal/handler/TerminalDispatcher.ts b/orion-ops-ui/src/views/host/terminal/handler/TerminalDispatcher.ts index 8a7af115..c47a6925 100644 --- a/orion-ops-ui/src/views/host/terminal/handler/TerminalDispatcher.ts +++ b/orion-ops-ui/src/views/host/terminal/handler/TerminalDispatcher.ts @@ -1,30 +1,44 @@ -import type { ITerminalDispatcher, TerminalTabItem } from '@/store/modules/terminal/types'; +import type { ITerminalDispatcher, ITerminalHandler, TerminalTabItem } from '@/store/modules/terminal/types'; 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 { Message } from '@arco-design/web-vue'; import { sleep } from '@/utils'; -import { InputProtocol, format } from '../types/terminal.protocol'; +import { format, InputProtocol, OutputProtocol, parse } from '../types/terminal.protocol'; +import { useDebounceFn } from '@vueuse/core'; +import { addEventListen, removeEventListen } from '@/utils/event'; export const wsBase = import.meta.env.VITE_WS_BASE_URL; +// 拆分两套逻辑 1. tab处理, 2. terminal处理 +// 太多需要优化的地方了 +// 拆成 event + /** * 终端调度器 */ export default class TerminalDispatcher implements ITerminalDispatcher { - private access?: HostTerminalAccessResponse; - - private client?: WebSocket; - public active: string; public items: Array; + private access?: HostTerminalAccessResponse; + + private client?: WebSocket; + + private handlers: Record; + + private pingTask?: any; + + 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 @@ -75,11 +89,17 @@ export default class TerminalDispatcher implements ITerminalDispatcher { this.client.onclose = event => { console.warn('close', event); }; - this.client.onmessage = this.handlerMessage; - // 等待会话等待完成 + this.client.onmessage = this.handlerMessage.bind(this); + // 注册 ping 事件 + this.pingTask = setInterval(() => { + this.client?.send(format(InputProtocol.PING, {})); + }, 150000); + // 注册 resize 事件 + addEventListen(window, 'resize', this.dispatchResizeFn); + // 等待会话连接成功 for (let i = 0; i < 100; i++) { await sleep(50); - if (this.client.readyState === WebSocket.OPEN) { + if (this.client.readyState !== WebSocket.CONNECTING) { break; } } @@ -87,7 +107,27 @@ export default class TerminalDispatcher implements ITerminalDispatcher { // 处理消息 handlerMessage({ data }: MessageEvent) { - console.log(data); + const payload = parse(data as string); + if (!payload) { + return; + } + // 选取会话 + switch (payload.type) { + case OutputProtocol.CHECK.type: + // 检查信息回调 + this.onTerminalCheckCallback(payload.session, payload.result, payload.errorMessage); + break; + case OutputProtocol.CONNECT.type: + // 连接信息回调 + this.onTerminalConnectCallback(payload.session, payload.result, payload.errorMessage); + break; + case OutputProtocol.OUTPUT.type: + // 输出 + this.onTerminalOutputCallback(payload.session, payload.body); + break; + default: + break; + } } // 打开终端 @@ -105,23 +145,68 @@ export default class TerminalDispatcher implements ITerminalDispatcher { key: session, title: record.alias || (`${record.name} ${record.address}`), hostId: record.id, - address: record.address, - checked: false, - connected: false + address: record.address }); } - // 注册终端钩子 - registerTerminalHook(tab: TerminalTabItem) { - if (!this.client) { + // 注册终端处理器 + registerTerminalHandler(tab: TerminalTabItem, handler: ITerminalHandler) { + this.handlers[tab.key] = handler; + // 发送 check 命令 + this.client?.send(format(InputProtocol.CHECK, { session: tab.key, hostId: tab.hostId })); + } + + // 调度重置大小 + dispatchResize() { + Object.values(this.handlers) + .filter(h => h.connected) + .forEach(h => h.fit()); + } + + // 终端检查回调 + onTerminalCheckCallback(session: string, result: string, errormessage: string) { + const success = !!parseInt(result); + const handler = this.handlers[session]; + // 未成功展示错误信息 + if (!success) { + handler.write('' + errormessage + ''); return; } - // 发送 check 命令 - this.client.send(format(InputProtocol.CHECK, { session: tab.key, hostId: tab.hostId })); + // 发送 connect 命令 + this.client?.send(format(InputProtocol.CONNECT, { session, cols: handler.inst.cols, rows: handler.inst.rows })); + } + + // 终端连接回调 + onTerminalConnectCallback(session: string, result: string, errormessage: string) { + const success = !!parseInt(result); + const handler = this.handlers[session]; + // 未成功展示错误信息 + if (!success) { + handler.write('' + errormessage + ''); + return; + } + // 设置可写 + handler.setCanWrite(true); + handler.connect(); + } + + // 发送消息 + onMessage(session: string, value: string): void { + // 发送命令 + this.client?.send(format(InputProtocol.INPUT, { session, command: value })); + } + + // 终端输出回调 + onTerminalOutputCallback(session: string, body: string) { + this.handlers[session].write(body); } // 关闭终端 - closeTerminal(key: string) { + closeTerminal(session: string) { + // 发送关闭消息 + this.client?.send(format(InputProtocol.CLOSE, { session })); + // 关闭终端 + this.handlers[session].close(); } // 重置 @@ -129,8 +214,21 @@ export default class TerminalDispatcher implements ITerminalDispatcher { this.active = undefined as unknown as string; this.items = []; this.access = undefined; - this.client = undefined; + this.handlers = {}; + // 关闭 client + if (this.client) { + if (this.client.readyState === WebSocket.CONNECTING) { + this.client.close(); + } + this.client = undefined; + } + // 清除 ping 事件 + if (this.pingTask) { + clearInterval(this.pingTask); + this.pingTask = undefined; + } + // 移除 resize 事件 + removeEventListen(window, 'resize', this.dispatchResizeFn); } } - diff --git a/orion-ops-ui/src/views/host/terminal/handler/TerminalHandler.ts b/orion-ops-ui/src/views/host/terminal/handler/TerminalHandler.ts new file mode 100644 index 00000000..8013795e --- /dev/null +++ b/orion-ops-ui/src/views/host/terminal/handler/TerminalHandler.ts @@ -0,0 +1,96 @@ +import { ITerminalHandler } from '@/store/modules/terminal/types'; +import { useTerminalStore } from '@/store'; +import { fontFamilySuffix } from '@/views/host/terminal/types/terminal.const'; +import { ITerminalAddon, Terminal } from 'xterm'; +import { FitAddon } from 'xterm-addon-fit'; +import { WebglAddon } from 'xterm-addon-webgl'; + +/** + * 终端处理器 + */ +export default class TerminalHandler implements ITerminalHandler { + + public connected: boolean = false; + + private canWrite: boolean = false; + + private readonly session: string; + + public inst: Terminal; + + private fitAddon?: FitAddon; + + private addons: ITerminalAddon[] = []; + + constructor(session: string, dom: HTMLElement) { + this.session = session; + const { preference } = useTerminalStore(); + // 初始化实例 + this.inst = new Terminal({ + ...(preference.displaySetting as any), + theme: preference.themeSchema, + fastScrollModifier: 'shift', + fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix, + }); + this.init(dom); + } + + // 初始化 + init(dom: HTMLElement): void { + // 注册插件 + this.addons.push( + this.fitAddon = new FitAddon(), + new WebglAddon() + ); + const inst = this.inst; + this.addons.forEach(s => inst.loadAddon(s)); + // 打开终端 + this.inst.open(dom); + // 自适应 + this.fitAddon.fit(); + } + + // 设置已连接 + connect(): void { + this.connected = true; + // 注册输入事件 + this.inst.onData(s => { + if (!this.canWrite) { + return; + } + // 输入 + useTerminalStore().dispatcher.onMessage(this.session, s); + }); + // 注册 resize 事件 + this.inst.onResize(({ cols, rows }) => { + // 输入 + }); + } + + // 设置是否可写 + setCanWrite(canWrite: boolean): void { + this.canWrite = canWrite; + } + + // 写入数据 + write(value: string): void { + this.inst.write(value); + } + + // 自适应 + fit(): void { + this.fitAddon?.fit(); + } + + // 关闭 + close(): void { + try { + for (let addon of this.addons) { + addon.dispose(); + } + this.inst.dispose(); + } catch (e) { + } + } + +} diff --git a/orion-ops-ui/src/views/host/terminal/index.vue b/orion-ops-ui/src/views/host/terminal/index.vue index ad7e6cb2..716a0304 100644 --- a/orion-ops-ui/src/views/host/terminal/index.vue +++ b/orion-ops-ui/src/views/host/terminal/index.vue @@ -29,7 +29,7 @@ diff --git a/orion-ops-ui/src/views/host/terminal/types/terminal.protocol.ts b/orion-ops-ui/src/views/host/terminal/types/terminal.protocol.ts index 045940db..8e11fb34 100644 --- a/orion-ops-ui/src/views/host/terminal/types/terminal.protocol.ts +++ b/orion-ops-ui/src/views/host/terminal/types/terminal.protocol.ts @@ -7,7 +7,7 @@ export interface Protocol { // 终端内容 export interface Payload { type?: string; - session: string; + session?: string; [key: string]: unknown; } @@ -79,7 +79,7 @@ export const OutputProtocol = { export const SEPARATOR = '|'; // 解析参数 -export const parse: Record = (payload: string) => { +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) {