feat: 连接终端.
This commit is contained in:
@@ -66,7 +66,7 @@ public class TerminalManager {
|
|||||||
public void closeAll(String channelId) {
|
public void closeAll(String channelId) {
|
||||||
// 获取并移除
|
// 获取并移除
|
||||||
ConcurrentHashMap<String, ITerminalSession> session = channelSessions.remove(channelId);
|
ConcurrentHashMap<String, ITerminalSession> session = channelSessions.remove(channelId);
|
||||||
if (Maps.isEmpty(session)) {
|
if (!Maps.isEmpty(session)) {
|
||||||
session.values().forEach(Streams::close);
|
session.values().forEach(Streams::close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,10 +77,14 @@ public class TerminalSession implements ITerminalSession {
|
|||||||
if (!executor.isConnected()) {
|
if (!executor.isConnected()) {
|
||||||
executor.connect();
|
executor.connect();
|
||||||
}
|
}
|
||||||
config.setCols(cols);
|
// 大小发生变化 则修改大小
|
||||||
config.setRows(rows);
|
if (cols != config.getCols() ||
|
||||||
executor.size(cols, rows);
|
rows != config.getRows()) {
|
||||||
executor.resize();
|
config.setCols(cols);
|
||||||
|
config.setRows(rows);
|
||||||
|
executor.size(cols, rows);
|
||||||
|
executor.resize();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
|
||||||
export interface TerminalState {
|
export interface TerminalState {
|
||||||
isDarkTheme: Ref<boolean>;
|
isDarkTheme: Ref<boolean>;
|
||||||
@@ -80,9 +81,29 @@ export interface ITerminalDispatcher {
|
|||||||
openTab: (tab: TerminalTabItem) => void;
|
openTab: (tab: TerminalTabItem) => void;
|
||||||
// 打开终端
|
// 打开终端
|
||||||
openTerminal: (record: any) => void;
|
openTerminal: (record: any) => void;
|
||||||
// 注册终端钩子
|
// 注册终端处理器
|
||||||
registerTerminalHook: (tab: TerminalTabItem) => void;
|
registerTerminalHandler: (tab: TerminalTabItem, handler: ITerminalHandler) => void;
|
||||||
|
// 发送消息
|
||||||
|
onMessage: (session: string, value: string) => void;
|
||||||
|
|
||||||
// 重置
|
// 重置
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 终端处理器
|
||||||
|
export interface ITerminalHandler {
|
||||||
|
inst: Terminal;
|
||||||
|
connected: boolean;
|
||||||
|
|
||||||
|
// 连接
|
||||||
|
connect: () => void;
|
||||||
|
// 设置是否可写
|
||||||
|
setCanWrite: (canWrite: boolean) => void;
|
||||||
|
// 写入数据
|
||||||
|
write: (value: string) => void;
|
||||||
|
// 自适应
|
||||||
|
fit: () => void;
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,9 +21,8 @@
|
|||||||
import type { TerminalTabItem } from '@/store/modules/terminal/types';
|
import type { TerminalTabItem } from '@/store/modules/terminal/types';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import TerminalHandler from '@/views/host/terminal/handler/TerminalHandler';
|
||||||
import { WebglAddon } from 'xterm-addon-webgl';
|
import { sleep } from '@/utils';
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tab: TerminalTabItem
|
tab: TerminalTabItem
|
||||||
@@ -34,27 +33,13 @@
|
|||||||
const terminalRef = ref();
|
const terminalRef = ref();
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
const init = () => {
|
const init = async () => {
|
||||||
// FIXME fontfamily
|
// 创建终端处理器
|
||||||
// 初始化终端
|
const handler = new TerminalHandler(props.tab.key, terminalRef.value);
|
||||||
const term = new Terminal({
|
// 等待前端渲染完成
|
||||||
theme: preference.themeSchema,
|
await sleep(100);
|
||||||
fastScrollModifier: 'shift',
|
// 注册处理器
|
||||||
...(preference.displaySetting as any),
|
dispatcher.registerTerminalHandler(props.tab, handler);
|
||||||
});
|
|
||||||
// 注册插件
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
const webglAddon = new WebglAddon();
|
|
||||||
term.loadAddon(fitAddon);
|
|
||||||
term.loadAddon(webglAddon);
|
|
||||||
// 打开终端
|
|
||||||
term.open(terminalRef.value);
|
|
||||||
// 自适应
|
|
||||||
fitAddon.fit();
|
|
||||||
// 注册钩子
|
|
||||||
dispatcher.registerTerminalHook(props.tab);
|
|
||||||
// 初始化终端
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(init);
|
onMounted(init);
|
||||||
|
|||||||
@@ -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 { HostQueryResponse } from '@/api/asset/host';
|
||||||
import type { HostTerminalAccessResponse } from '@/api/asset/host-terminal';
|
import type { HostTerminalAccessResponse } from '@/api/asset/host-terminal';
|
||||||
import { getHostTerminalAccessToken } from '@/api/asset/host-terminal';
|
import { getHostTerminalAccessToken } from '@/api/asset/host-terminal';
|
||||||
import { InnerTabs, TabType } from '@/views/host/terminal/types/terminal.const';
|
import { InnerTabs, TabType } from '@/views/host/terminal/types/terminal.const';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { sleep } from '@/utils';
|
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;
|
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
|
||||||
|
|
||||||
|
// 拆分两套逻辑 1. tab处理, 2. terminal处理
|
||||||
|
// 太多需要优化的地方了
|
||||||
|
// 拆成 event
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 终端调度器
|
* 终端调度器
|
||||||
*/
|
*/
|
||||||
export default class TerminalDispatcher implements ITerminalDispatcher {
|
export default class TerminalDispatcher implements ITerminalDispatcher {
|
||||||
|
|
||||||
private access?: HostTerminalAccessResponse;
|
|
||||||
|
|
||||||
private client?: WebSocket;
|
|
||||||
|
|
||||||
public active: string;
|
public active: string;
|
||||||
|
|
||||||
public items: Array<TerminalTabItem>;
|
public items: Array<TerminalTabItem>;
|
||||||
|
|
||||||
|
private access?: HostTerminalAccessResponse;
|
||||||
|
|
||||||
|
private client?: WebSocket;
|
||||||
|
|
||||||
|
private handlers: Record<string, ITerminalHandler>;
|
||||||
|
|
||||||
|
private pingTask?: any;
|
||||||
|
|
||||||
|
private readonly dispatchResizeFn: () => {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.active = InnerTabs.NEW_CONNECTION.key;
|
this.active = InnerTabs.NEW_CONNECTION.key;
|
||||||
this.items = [InnerTabs.NEW_CONNECTION];
|
this.items = [InnerTabs.NEW_CONNECTION];
|
||||||
|
this.handlers = {};
|
||||||
|
this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击 tab
|
// 点击 tab
|
||||||
@@ -75,11 +89,17 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
|
|||||||
this.client.onclose = event => {
|
this.client.onclose = event => {
|
||||||
console.warn('close', 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++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
await sleep(50);
|
await sleep(50);
|
||||||
if (this.client.readyState === WebSocket.OPEN) {
|
if (this.client.readyState !== WebSocket.CONNECTING) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +107,27 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
|
|||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
handlerMessage({ data }: MessageEvent) {
|
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,
|
key: session,
|
||||||
title: record.alias || (`${record.name} ${record.address}`),
|
title: record.alias || (`${record.name} ${record.address}`),
|
||||||
hostId: record.id,
|
hostId: record.id,
|
||||||
address: record.address,
|
address: record.address
|
||||||
checked: false,
|
|
||||||
connected: false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册终端钩子
|
// 注册终端处理器
|
||||||
registerTerminalHook(tab: TerminalTabItem) {
|
registerTerminalHandler(tab: TerminalTabItem, handler: ITerminalHandler) {
|
||||||
if (!this.client) {
|
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('[91m' + errormessage + '[0m');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 发送 check 命令
|
// 发送 connect 命令
|
||||||
this.client.send(format(InputProtocol.CHECK, { session: tab.key, hostId: tab.hostId }));
|
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('[91m' + errormessage + '[0m');
|
||||||
|
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.active = undefined as unknown as string;
|
||||||
this.items = [];
|
this.items = [];
|
||||||
this.access = undefined;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onBeforeMount, onUnmounted } from 'vue';
|
import { ref, onBeforeMount, onUnmounted, onMounted } from 'vue';
|
||||||
import { dictKeys } from './types/terminal.const';
|
import { dictKeys } from './types/terminal.const';
|
||||||
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
|
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
|
||||||
import TerminalHeader from './components/layout/terminal-header.vue';
|
import TerminalHeader from './components/layout/terminal-header.vue';
|
||||||
@@ -45,6 +45,12 @@
|
|||||||
|
|
||||||
const render = ref(false);
|
const render = ref(false);
|
||||||
|
|
||||||
|
// 关闭视口处理
|
||||||
|
const handleBeforeUnload = (event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.returnValue = confirm('系统可能不会保存您所做的更改');
|
||||||
|
};
|
||||||
|
|
||||||
// 加载用户终端偏好
|
// 加载用户终端偏好
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await terminalStore.fetchPreference();
|
await terminalStore.fetchPreference();
|
||||||
@@ -56,9 +62,17 @@
|
|||||||
await dictStore.loadKeys([...dictKeys]);
|
await dictStore.loadKeys([...dictKeys]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 卸载时清除 cache
|
// 注册关闭视口事件
|
||||||
|
onMounted(() => {
|
||||||
|
// TODO 开发阶段
|
||||||
|
// window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
// 卸载时清除 cache
|
||||||
cacheStore.reset('authorizedHostKeys', 'authorizedHostIdentities');
|
cacheStore.reset('authorizedHostKeys', 'authorizedHostIdentities');
|
||||||
|
// 移除关闭视口事件
|
||||||
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface Protocol {
|
|||||||
// 终端内容
|
// 终端内容
|
||||||
export interface Payload {
|
export interface Payload {
|
||||||
type?: string;
|
type?: string;
|
||||||
session: string;
|
session?: string;
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ export const OutputProtocol = {
|
|||||||
export const SEPARATOR = '|';
|
export const SEPARATOR = '|';
|
||||||
|
|
||||||
// 解析参数
|
// 解析参数
|
||||||
export const parse: Record<string, any> = (payload: string) => {
|
export const parse = (payload: string) => {
|
||||||
const protocols = Object.values(OutputProtocol);
|
const protocols = Object.values(OutputProtocol);
|
||||||
const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload);
|
const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload);
|
||||||
if (!useProtocol) {
|
if (!useProtocol) {
|
||||||
|
|||||||
Reference in New Issue
Block a user