refactor: 重构终端连接流程.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package com.orion.ops.module.asset.controller;
|
package com.orion.ops.module.asset.controller;
|
||||||
|
|
||||||
import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
||||||
import com.orion.ops.module.asset.entity.vo.HostTerminalAccessVO;
|
|
||||||
import com.orion.ops.module.asset.service.HostTerminalService;
|
import com.orion.ops.module.asset.service.HostTerminalService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -36,7 +35,7 @@ public class HostTerminalController {
|
|||||||
@GetMapping("/access")
|
@GetMapping("/access")
|
||||||
@Operation(summary = "获取主机终端 accessToken")
|
@Operation(summary = "获取主机终端 accessToken")
|
||||||
@PreAuthorize("@ss.hasPermission('asset:host-terminal:access')")
|
@PreAuthorize("@ss.hasPermission('asset:host-terminal:access')")
|
||||||
public HostTerminalAccessVO getHostTerminalAccessToken() {
|
public String getHostTerminalAccessToken() {
|
||||||
return hostTerminalService.getHostTerminalAccessToken();
|
return hostTerminalService.getHostTerminalAccessToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.orion.ops.module.asset.entity.vo;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主机终端访问 响应对象
|
|
||||||
*
|
|
||||||
* @author Jiahang Li
|
|
||||||
* @version 1.0.0
|
|
||||||
* @since 2024/1/4 15:42
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(name = "HostTerminalAccessVO", description = "主机终端访问 响应对象")
|
|
||||||
public class HostTerminalAccessVO implements Serializable {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
@Schema(description = "accessToken")
|
|
||||||
private String accessToken;
|
|
||||||
|
|
||||||
@Schema(description = "session 起始量")
|
|
||||||
private String sessionInitial;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import com.orion.net.host.SessionStore;
|
|||||||
import com.orion.ops.module.asset.entity.domain.HostDO;
|
import com.orion.ops.module.asset.entity.domain.HostDO;
|
||||||
import com.orion.ops.module.asset.entity.dto.HostTerminalAccessDTO;
|
import com.orion.ops.module.asset.entity.dto.HostTerminalAccessDTO;
|
||||||
import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
|
import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
|
||||||
import com.orion.ops.module.asset.entity.vo.HostTerminalAccessVO;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主机终端服务
|
* 主机终端服务
|
||||||
@@ -18,9 +17,9 @@ public interface HostTerminalService {
|
|||||||
/**
|
/**
|
||||||
* 获取主机终端访问 accessToken
|
* 获取主机终端访问 accessToken
|
||||||
*
|
*
|
||||||
* @return session
|
* @return accessToken
|
||||||
*/
|
*/
|
||||||
HostTerminalAccessVO getHostTerminalAccessToken();
|
String getHostTerminalAccessToken();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 accessToken 获取主机终端访问信息
|
* 通过 accessToken 获取主机终端访问信息
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import com.orion.ops.module.asset.entity.domain.HostIdentityDO;
|
|||||||
import com.orion.ops.module.asset.entity.domain.HostKeyDO;
|
import com.orion.ops.module.asset.entity.domain.HostKeyDO;
|
||||||
import com.orion.ops.module.asset.entity.dto.HostTerminalAccessDTO;
|
import com.orion.ops.module.asset.entity.dto.HostTerminalAccessDTO;
|
||||||
import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
|
import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
|
||||||
import com.orion.ops.module.asset.entity.vo.HostTerminalAccessVO;
|
|
||||||
import com.orion.ops.module.asset.enums.HostConfigTypeEnum;
|
import com.orion.ops.module.asset.enums.HostConfigTypeEnum;
|
||||||
import com.orion.ops.module.asset.enums.HostExtraItemEnum;
|
import com.orion.ops.module.asset.enums.HostExtraItemEnum;
|
||||||
import com.orion.ops.module.asset.enums.HostExtraSshAuthTypeEnum;
|
import com.orion.ops.module.asset.enums.HostExtraSshAuthTypeEnum;
|
||||||
@@ -83,7 +82,7 @@ public class HostTerminalServiceImpl implements HostTerminalService {
|
|||||||
private SystemUserApi systemUserApi;
|
private SystemUserApi systemUserApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HostTerminalAccessVO getHostTerminalAccessToken() {
|
public String getHostTerminalAccessToken() {
|
||||||
LoginUser user = SecurityUtils.getLoginUser();
|
LoginUser user = SecurityUtils.getLoginUser();
|
||||||
log.info("HostConnectService.getHostAccessToken userId: {}", user.getId());
|
log.info("HostConnectService.getHostAccessToken userId: {}", user.getId());
|
||||||
String accessToken = UUIds.random19();
|
String accessToken = UUIds.random19();
|
||||||
@@ -94,11 +93,7 @@ public class HostTerminalServiceImpl implements HostTerminalService {
|
|||||||
// 设置 access 缓存
|
// 设置 access 缓存
|
||||||
String key = HostTerminalCacheKeyDefine.HOST_TERMINAL_ACCESS.format(accessToken);
|
String key = HostTerminalCacheKeyDefine.HOST_TERMINAL_ACCESS.format(accessToken);
|
||||||
RedisStrings.setJson(key, HostTerminalCacheKeyDefine.HOST_TERMINAL_ACCESS, access);
|
RedisStrings.setJson(key, HostTerminalCacheKeyDefine.HOST_TERMINAL_ACCESS, access);
|
||||||
return HostTerminalAccessVO.builder()
|
return accessToken;
|
||||||
.accessToken(accessToken)
|
|
||||||
// 32 进制的 uuid 作为起始量
|
|
||||||
.sessionInitial(Long.toString(UUIds.random15Long(), 32))
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
/**
|
|
||||||
* 主机终端访问响应
|
|
||||||
*/
|
|
||||||
export interface HostTerminalAccessResponse {
|
|
||||||
accessToken: string;
|
|
||||||
sessionInitial: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主机终端 accessToken
|
* 获取主机终端 accessToken
|
||||||
*/
|
*/
|
||||||
export function getHostTerminalAccessToken() {
|
export function getHostTerminalAccessToken() {
|
||||||
return axios.get<HostTerminalAccessResponse>('/asset/host-terminal/access');
|
return axios.get<string>('/asset/host-terminal/access');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { getPreference, updatePreference } from '@/api/user/preference';
|
|||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useDark } from '@vueuse/core';
|
import { useDark } from '@vueuse/core';
|
||||||
import { DEFAULT_SCHEMA } from '@/views/host/terminal/types/terminal.theme';
|
import { DEFAULT_SCHEMA } from '@/views/host/terminal/types/terminal.theme';
|
||||||
import TerminalDispatcher from '@/views/host/terminal/handler/terminal-dispatcher';
|
|
||||||
import TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
|
import TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
|
||||||
|
import TerminalSessionManager from '@/views/host/terminal/handler/terminal-session-manager';
|
||||||
|
|
||||||
// 暗色主题
|
// 暗色主题
|
||||||
export const DarkTheme = {
|
export const DarkTheme = {
|
||||||
@@ -30,8 +30,8 @@ export default defineStore('terminal', {
|
|||||||
displaySetting: {} as TerminalDisplaySetting,
|
displaySetting: {} as TerminalDisplaySetting,
|
||||||
themeSchema: {} as TerminalThemeSchema
|
themeSchema: {} as TerminalThemeSchema
|
||||||
},
|
},
|
||||||
tabs: new TerminalTabManager(),
|
tabManager: new TerminalTabManager(),
|
||||||
dispatcher: new TerminalDispatcher()
|
sessionManager: new TerminalSessionManager()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { Terminal } from 'xterm';
|
import type { ITerminalTabManager, ITerminalSessionManager } from '@/views/host/terminal/types/terminal.type';
|
||||||
|
|
||||||
export interface TerminalState {
|
export interface TerminalState {
|
||||||
isDarkTheme: Ref<boolean>;
|
isDarkTheme: Ref<boolean>;
|
||||||
preference: TerminalPreference;
|
preference: TerminalPreference;
|
||||||
tabs: ITerminalTabManager;
|
tabManager: ITerminalTabManager;
|
||||||
dispatcher: ITerminalDispatcher;
|
sessionManager: ITerminalSessionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 终端配置
|
// 终端配置
|
||||||
@@ -58,59 +58,3 @@ export interface TerminalThemeSchema {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 终端 tab 元素
|
|
||||||
export interface TerminalTabItem {
|
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 终端 tab 管理器定义
|
|
||||||
export interface ITerminalTabManager {
|
|
||||||
// 当前 tab
|
|
||||||
active: string;
|
|
||||||
// 全部 tab
|
|
||||||
items: Array<TerminalTabItem>;
|
|
||||||
|
|
||||||
// 点击 tab
|
|
||||||
clickTab: (key: string) => void;
|
|
||||||
// 删除 tab
|
|
||||||
deleteTab: (key: string) => void;
|
|
||||||
// 打开 tab
|
|
||||||
openTab: (tab: TerminalTabItem) => void;
|
|
||||||
// 清空
|
|
||||||
clear: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 终端调度器
|
|
||||||
export interface ITerminalDispatcher {
|
|
||||||
// 打开终端
|
|
||||||
openTerminal: (record: any) => 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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-content">
|
<div class="terminal-content">
|
||||||
<!-- 内容 tabs -->
|
<!-- 内容 tabs -->
|
||||||
<a-tabs v-model:active-key="tabs.active">
|
<a-tabs v-model:active-key="tabManager.active">
|
||||||
<a-tab-pane v-for="tab in tabs.items"
|
<a-tab-pane v-for="tab in tabManager.items"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:title="tab.title">
|
:title="tab.title">
|
||||||
<!-- 设置 -->
|
<!-- 设置 -->
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
import NewConnectionView from '../new-connection/new-connection-view.vue';
|
import NewConnectionView from '../new-connection/new-connection-view.vue';
|
||||||
import TerminalView from '../xterm/terminal-view.vue';
|
import TerminalView from '../xterm/terminal-view.vue';
|
||||||
|
|
||||||
const { tabs } = useTerminalStore();
|
const { tabManager } = useTerminalStore();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-header">
|
<div class="terminal-header">
|
||||||
<!-- 左侧 logo -->
|
<!-- 左侧 logo -->
|
||||||
|
<!-- FIXME -->
|
||||||
<div class="terminal-header-left">
|
<div class="terminal-header-left">
|
||||||
<img alt="logo"
|
<img alt="logo"
|
||||||
class="terminal-header-logo-img"
|
class="terminal-header-logo-img"
|
||||||
@@ -10,13 +11,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 左侧 tabs -->
|
<!-- 左侧 tabs -->
|
||||||
<div class="terminal-header-tabs">
|
<div class="terminal-header-tabs">
|
||||||
<a-tabs v-model:active-key="tabs.active"
|
<a-tabs v-model:active-key="tabManager.active"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:hide-content="true"
|
:hide-content="true"
|
||||||
:auto-switch="true"
|
:auto-switch="true"
|
||||||
@tab-click="k => tabs.clickTab(k as string)"
|
@tab-click="k => tabManager.clickTab(k as string)"
|
||||||
@delete="k => tabs.deleteTab(k as string)">
|
@delete="k => tabManager.deleteTab(k as string)">
|
||||||
<a-tab-pane v-for="tab in tabs.items"
|
<a-tab-pane v-for="tab in tabManager.items"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:title="tab.title" />
|
:title="tab.title" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
import IconActions from '../layout/icon-actions.vue';
|
import IconActions from '../layout/icon-actions.vue';
|
||||||
|
|
||||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||||
const { tabs } = useTerminalStore();
|
const { tabManager } = useTerminalStore();
|
||||||
|
|
||||||
// 顶部操作
|
// 顶部操作
|
||||||
const actions = computed<Array<SidebarAction>>(() => [
|
const actions = computed<Array<SidebarAction>>(() => [
|
||||||
|
|||||||
@@ -23,14 +23,14 @@
|
|||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import IconActions from './icon-actions.vue';
|
import IconActions from './icon-actions.vue';
|
||||||
|
|
||||||
const { tabs } = useTerminalStore();
|
const { tabManager } = useTerminalStore();
|
||||||
|
|
||||||
// 顶部操作
|
// 顶部操作
|
||||||
const topActions: Array<SidebarAction> = [
|
const topActions: Array<SidebarAction> = [
|
||||||
{
|
{
|
||||||
icon: 'icon-plus',
|
icon: 'icon-plus',
|
||||||
content: '新建连接',
|
content: '新建连接',
|
||||||
click: () => tabs.openTab(InnerTabs.NEW_CONNECTION)
|
click: () => tabManager.openTab(InnerTabs.NEW_CONNECTION)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
{
|
{
|
||||||
icon: 'icon-command',
|
icon: 'icon-command',
|
||||||
content: '快捷键设置',
|
content: '快捷键设置',
|
||||||
click: () => tabs.openTab(InnerTabs.SHORTCUT_SETTING)
|
click: () => tabManager.openTab(InnerTabs.SHORTCUT_SETTING)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'icon-palette',
|
icon: 'icon-palette',
|
||||||
content: '外观设置',
|
content: '外观设置',
|
||||||
click: () => tabs.openTab(InnerTabs.VIEW_SETTING)
|
click: () => tabManager.openTab(InnerTabs.VIEW_SETTING)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@
|
|||||||
emptyValue: string
|
emptyValue: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { tabs } = useTerminalStore();
|
const { tabManager } = useTerminalStore();
|
||||||
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
||||||
|
|
||||||
const aliasNameInput = ref();
|
const aliasNameInput = ref();
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
|
|
||||||
// 打开终端
|
// 打开终端
|
||||||
const openTerminal = (record: HostQueryResponse) => {
|
const openTerminal = (record: HostQueryResponse) => {
|
||||||
tabs.openTab({
|
tabManager.openTab({
|
||||||
type: TabType.TERMINAL,
|
type: TabType.TERMINAL,
|
||||||
key: nextSessionId(),
|
key: nextSessionId(),
|
||||||
title: record.alias || (`${record.name} ${record.address}`),
|
title: record.alias || (`${record.name} ${record.address}`),
|
||||||
|
|||||||
@@ -18,35 +18,27 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TerminalTabItem } from '@/store/modules/terminal/types';
|
import type { TerminalTabItem } from '../../types/terminal.type';
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import TerminalHandler from '@/views/host/terminal/handler/TerminalHandler';
|
|
||||||
import { sleep } from '@/utils';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
tab: TerminalTabItem
|
tab: TerminalTabItem
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { preference, dispatcher } = useTerminalStore();
|
const { sessionManager } = useTerminalStore();
|
||||||
|
|
||||||
const terminalRef = ref();
|
const terminalRef = ref();
|
||||||
|
|
||||||
// 初始化
|
// 初始化回话
|
||||||
const init = async () => {
|
onMounted(async () => {
|
||||||
// 创建终端处理器
|
// 创建终端处理器
|
||||||
const handler = new TerminalHandler(props.tab.key, terminalRef.value);
|
sessionManager.openSession(props.tab, terminalRef.value);
|
||||||
// 等待前端渲染完成
|
});
|
||||||
await sleep(100);
|
|
||||||
// 注册处理器
|
|
||||||
dispatcher.registerTerminalHandler(props.tab, handler);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(init);
|
|
||||||
|
|
||||||
|
// 关闭回话
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 发送关闭
|
sessionManager.closeSession(props.tab.key);
|
||||||
console.log('12312312');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,68 @@
|
|||||||
import { OutputProtocol } from '@/views/host/terminal/types/terminal.protocol';
|
import type {
|
||||||
import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, OutputPayload, Protocol, } from '@/views/host/terminal/types/terminal.type';
|
InputPayload,
|
||||||
|
ITerminalChannel,
|
||||||
|
ITerminalOutputProcessor,
|
||||||
|
OutputPayload,
|
||||||
|
Protocol,
|
||||||
|
} from '../types/terminal.type';
|
||||||
|
import { OutputProtocol } from '../types/terminal.protocol';
|
||||||
|
import { getHostTerminalAccessToken } from '@/api/asset/host-terminal';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { sleep } from '@/utils';
|
||||||
|
|
||||||
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
|
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
|
||||||
|
|
||||||
// 终端通信处理器 实现
|
// 终端通信处理器 实现
|
||||||
export default class TerminalChannel implements ITerminalChannel {
|
export default class TerminalChannel implements ITerminalChannel {
|
||||||
|
|
||||||
|
private client?: WebSocket;
|
||||||
|
|
||||||
private readonly processor;
|
private readonly processor;
|
||||||
|
|
||||||
constructor(processor: ITerminalOutputProcessor) {
|
constructor(processor: ITerminalOutputProcessor) {
|
||||||
this.processor = processor;
|
this.processor = processor;
|
||||||
}
|
}
|
||||||
|
|
||||||
send(protocol: Protocol, payload: InputPayload): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
async init() {
|
async init() {
|
||||||
|
// 获取 access
|
||||||
|
const { data: accessToken } = await getHostTerminalAccessToken();
|
||||||
|
// 打开会话
|
||||||
|
this.client = new WebSocket(`${wsBase}/host/terminal/${accessToken}`);
|
||||||
|
this.client.onerror = event => {
|
||||||
|
Message.error('无法连接至服务器');
|
||||||
|
console.error('error', event);
|
||||||
|
};
|
||||||
|
this.client.onclose = event => {
|
||||||
|
console.warn('close', event);
|
||||||
|
};
|
||||||
|
this.client.onmessage = this.handlerMessage.bind(this);
|
||||||
|
// 等待会话连接
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
await sleep(50);
|
||||||
|
if (this.client.readyState !== WebSocket.CONNECTING) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否已连接
|
||||||
|
isConnected(): boolean {
|
||||||
|
return !!this.client && this.client.readyState === WebSocket.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
send(protocol: Protocol, payload: InputPayload): void {
|
||||||
|
// 检查是否连接
|
||||||
|
if (!this.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 发送命令
|
||||||
|
this.client?.send(format(protocol, payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理消息
|
// 处理消息
|
||||||
handlerMessage({ data }: MessageEvent) {
|
private handlerMessage({ data }: MessageEvent) {
|
||||||
// 解析消息
|
// 解析消息
|
||||||
const payload = parse(data as string);
|
const payload = parse(data as string);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
@@ -32,31 +72,38 @@ export default class TerminalChannel implements ITerminalChannel {
|
|||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case OutputProtocol.CHECK.type:
|
case OutputProtocol.CHECK.type:
|
||||||
// 检查 回调
|
// 检查 回调
|
||||||
this.processor.processCheck(payload);
|
this.processor.processCheck.call(this.processor, payload);
|
||||||
break;
|
break;
|
||||||
case OutputProtocol.CONNECT.type:
|
case OutputProtocol.CONNECT.type:
|
||||||
// 连接 回调
|
// 连接 回调
|
||||||
this.processor.processConnect(payload);
|
this.processor.processConnect.call(this.processor, payload);
|
||||||
break;
|
break;
|
||||||
case OutputProtocol.PONG.type:
|
case OutputProtocol.PONG.type:
|
||||||
// pong 回调
|
// pong 回调
|
||||||
this.processor.processPong(payload);
|
this.processor.processPong.call(this.processor, payload);
|
||||||
break;
|
break;
|
||||||
case OutputProtocol.OUTPUT.type:
|
case OutputProtocol.OUTPUT.type:
|
||||||
// 输出 回调
|
// 输出 回调
|
||||||
this.processor.processOutput(payload);
|
this.processor.processOutput.call(this.processor, payload);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭
|
||||||
close(): void {
|
close(): void {
|
||||||
|
// 关闭 client
|
||||||
|
if (this.client) {
|
||||||
|
if (this.client.readyState === WebSocket.OPEN) {
|
||||||
|
this.client.close();
|
||||||
|
}
|
||||||
|
this.client = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 分隔符
|
// 分隔符
|
||||||
export const SEPARATOR = '|';
|
export const SEPARATOR = '|';
|
||||||
|
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
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 { TabType } from '@/views/host/terminal/types/terminal.const';
|
|
||||||
import { Message } from '@arco-design/web-vue';
|
|
||||||
import { sleep } from '@/utils';
|
|
||||||
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;
|
|
||||||
|
|
||||||
// 拆分两套逻辑 1. tab处理, 2. terminal处理
|
|
||||||
// 太多需要优化的地方了
|
|
||||||
// 拆成 event
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 终端调度器
|
|
||||||
*/
|
|
||||||
export default class TerminalDispatcher implements ITerminalDispatcher {
|
|
||||||
|
|
||||||
private access?: HostTerminalAccessResponse;
|
|
||||||
|
|
||||||
private client?: WebSocket;
|
|
||||||
|
|
||||||
private handlers: Record<string, ITerminalHandler>;
|
|
||||||
|
|
||||||
private pingTask?: any;
|
|
||||||
|
|
||||||
private readonly dispatchResizeFn: () => {};
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.handlers = {};
|
|
||||||
this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化客户端
|
|
||||||
async initClient() {
|
|
||||||
if (this.client) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 获取 access
|
|
||||||
const { data: accessData } = await getHostTerminalAccessToken();
|
|
||||||
this.access = accessData;
|
|
||||||
// 打开会话
|
|
||||||
this.client = new WebSocket(`${wsBase}/host/terminal/${accessData.accessToken}`);
|
|
||||||
this.client.onerror = event => {
|
|
||||||
Message.error('无法连接至服务器');
|
|
||||||
console.error('error', event);
|
|
||||||
};
|
|
||||||
this.client.onclose = event => {
|
|
||||||
console.warn('close', event);
|
|
||||||
};
|
|
||||||
this.client.onmessage = this.handlerMessage.bind(this);
|
|
||||||
// 注册 ping 事件
|
|
||||||
this.pingTask = setInterval(() => {
|
|
||||||
this.client?.send(format(InputProtocol.PING, {} as Payload));
|
|
||||||
}, 150000);
|
|
||||||
// 注册 resize 事件
|
|
||||||
addEventListen(window, 'resize', this.dispatchResizeFn);
|
|
||||||
// 等待会话连接成功
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
await sleep(50);
|
|
||||||
if (this.client.readyState !== WebSocket.CONNECTING) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理消息
|
|
||||||
handlerMessage({ data }: MessageEvent) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开终端
|
|
||||||
async openTerminal(record: HostQueryResponse) {
|
|
||||||
// 初始化客户端
|
|
||||||
await this.initClient();
|
|
||||||
// uncheck
|
|
||||||
if (!this.access) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const session = this.access.sessionInitial = (parseInt(this.access.sessionInitial as string, 32) + 1).toString(32);
|
|
||||||
// 打开会话
|
|
||||||
useTerminalStore().tabs.openTab({
|
|
||||||
type: TabType.TERMINAL,
|
|
||||||
key: session,
|
|
||||||
title: record.alias || (`${record.name} ${record.address}`),
|
|
||||||
hostId: record.id,
|
|
||||||
address: record.address
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册终端处理器
|
|
||||||
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('[91m' + errormessage + '[0m');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 发送 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('[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(session: string) {
|
|
||||||
// 发送关闭消息
|
|
||||||
this.client?.send(format(InputProtocol.CLOSE, { session }));
|
|
||||||
// 关闭终端
|
|
||||||
this.handlers[session].close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置
|
|
||||||
reset(): void {
|
|
||||||
this.access = 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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('[91m' + errormessage + '[0m');
|
|
||||||
// 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('[91m' + errormessage + '[0m');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// // 设置可写
|
|
||||||
// handler.setCanWrite(true);
|
|
||||||
// handler.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 pong 消息
|
|
||||||
processPong(payload: OutputPayload): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理输出消息
|
|
||||||
processOutput(payload: OutputPayload): void {
|
|
||||||
// this.handlers[session].write(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,45 @@
|
|||||||
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
|
import type {
|
||||||
import type { TerminalTabItem } from '@/store/modules/terminal/types';
|
ITerminalChannel,
|
||||||
|
ITerminalSession,
|
||||||
|
ITerminalSessionManager,
|
||||||
|
ITerminalOutputProcessor,
|
||||||
|
OutputPayload,
|
||||||
|
TerminalTabItem
|
||||||
|
} from '../types/terminal.type';
|
||||||
import { sleep } from '@/utils';
|
import { sleep } from '@/utils';
|
||||||
|
import { InputProtocol } from '../types/terminal.protocol';
|
||||||
import TerminalSession from './terminal-session';
|
import TerminalSession from './terminal-session';
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
// 终端会话管理器定义
|
import TerminalChannel from '@/views/host/terminal/handler/terminal-channel';
|
||||||
export interface ITerminalSessionManager {
|
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||||
// 打开终端
|
|
||||||
openSession: (tab: TerminalTabItem, dom: HTMLElement) => void;
|
|
||||||
// 获取终端会话
|
|
||||||
getSession: (sessionId: string) => ITerminalSession;
|
|
||||||
// 重置
|
|
||||||
reset: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME 去除 TOKEN 起始量
|
|
||||||
|
|
||||||
// 终端会话管理器实现
|
// 终端会话管理器实现
|
||||||
export default class TerminalSessionManager implements ITerminalSessionManager {
|
export default class TerminalSessionManager implements ITerminalSessionManager, ITerminalOutputProcessor {
|
||||||
|
|
||||||
private readonly channel: ITerminalChannel;
|
private readonly channel: ITerminalChannel;
|
||||||
|
|
||||||
private sessions: Record<string, ITerminalSession>;
|
private sessions: Record<string, ITerminalSession>;
|
||||||
|
|
||||||
constructor(channel: ITerminalChannel) {
|
private keepAliveTask?: any;
|
||||||
this.channel = channel;
|
|
||||||
|
private readonly dispatchResizeFn: () => {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.channel = new TerminalChannel(this);
|
||||||
this.sessions = {};
|
this.sessions = {};
|
||||||
|
this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开终端会话
|
// 打开终端会话
|
||||||
async openSession(tab: TerminalTabItem, dom: HTMLElement) {
|
async openSession(tab: TerminalTabItem, dom: HTMLElement) {
|
||||||
|
const sessionId = tab.key;
|
||||||
|
const hostId = tab.hostId as number;
|
||||||
// 初始化客户端
|
// 初始化客户端
|
||||||
await this.channel.init();
|
await this.initChannel();
|
||||||
// 新建会话
|
// 新建会话
|
||||||
const session = new TerminalSession(
|
const session = new TerminalSession(
|
||||||
tab.hostId as number,
|
hostId,
|
||||||
tab.key,
|
sessionId,
|
||||||
this.channel
|
this.channel
|
||||||
);
|
);
|
||||||
// 初始化
|
// 初始化
|
||||||
@@ -42,7 +47,28 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
|||||||
// 等待前端渲染完成
|
// 等待前端渲染完成
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
// 添加会话
|
// 添加会话
|
||||||
this.sessions[tab.key] = session;
|
this.sessions[sessionId] = session;
|
||||||
|
// 发送会话初始化请求
|
||||||
|
this.channel.send(InputProtocol.CHECK, {
|
||||||
|
session: sessionId,
|
||||||
|
hostId: hostId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 channel
|
||||||
|
private async initChannel() {
|
||||||
|
// 检查 channel 是否已经初始化
|
||||||
|
if (this.channel.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 初始化 channel
|
||||||
|
await this.channel.init();
|
||||||
|
// 注册 resize 事件
|
||||||
|
addEventListen(window, 'resize', this.dispatchResizeFn);
|
||||||
|
// 注册 ping 事件
|
||||||
|
this.keepAliveTask = setInterval(() => {
|
||||||
|
this.channel.send(InputProtocol.PING, {});
|
||||||
|
}, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取终端会话
|
// 获取终端会话
|
||||||
@@ -50,9 +76,82 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
|||||||
return this.sessions[sessionId];
|
return this.sessions[sessionId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭终端会话
|
||||||
|
closeSession(sessionId: string): void {
|
||||||
|
// 发送关闭消息
|
||||||
|
this.channel?.send(InputProtocol.CLOSE, { session: sessionId });
|
||||||
|
// 关闭 session
|
||||||
|
const session = this.sessions[sessionId];
|
||||||
|
if (session) {
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
// 移除 session
|
||||||
|
this.sessions[sessionId] = undefined as unknown as ITerminalSession;
|
||||||
|
// session 全部关闭后 关闭 channel
|
||||||
|
if (Object.values(this.sessions).filter(Boolean).every(s => !s?.connected)) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调度重置大小
|
||||||
|
private dispatchResize() {
|
||||||
|
// 对所有已连接的会话重置大小
|
||||||
|
Object.values(this.sessions)
|
||||||
|
.filter(h => h.connected)
|
||||||
|
.forEach(h => h.fit());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理检查消息
|
||||||
|
processCheck({ session: sessionId, result, errorMessage }: OutputPayload): void {
|
||||||
|
const success = !!Number.parseInt(result);
|
||||||
|
const session = this.sessions[sessionId];
|
||||||
|
// 未成功展示错误信息
|
||||||
|
if (!success) {
|
||||||
|
session.write('[91m' + errorMessage + '[0m');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 发送 connect 命令
|
||||||
|
this.channel.send(InputProtocol.CONNECT, { session: sessionId, cols: session.inst.cols, rows: session.inst.rows });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理连接消息
|
||||||
|
processConnect({ session: sessionId, result, errorMessage }: OutputPayload): void {
|
||||||
|
const success = !!Number.parseInt(result);
|
||||||
|
const session = this.sessions[sessionId];
|
||||||
|
// 未成功展示错误信息
|
||||||
|
if (!success) {
|
||||||
|
session.write('[91m' + errorMessage + '[0m');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 设置可写
|
||||||
|
session.setCanWrite(true);
|
||||||
|
// 执行连接逻辑
|
||||||
|
session.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 pong 消息
|
||||||
|
processPong(payload: OutputPayload): void {
|
||||||
|
console.log('pong');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输出消息
|
||||||
|
processOutput({ session: sessionId, body }: OutputPayload): void {
|
||||||
|
const session = this.sessions[sessionId];
|
||||||
|
session && session.write(body);
|
||||||
|
}
|
||||||
|
|
||||||
// 重置
|
// 重置
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.sessions = {};
|
this.sessions = {};
|
||||||
|
// 关闭 channel
|
||||||
|
this.channel.close();
|
||||||
|
// 清除 ping 事件
|
||||||
|
if (this.keepAliveTask) {
|
||||||
|
clearInterval(this.keepAliveTask);
|
||||||
|
this.keepAliveTask = undefined;
|
||||||
|
}
|
||||||
|
// 移除 resize 事件
|
||||||
|
removeEventListen(window, 'resize', this.dispatchResizeFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
|
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
|
||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { fontFamilySuffix } from '@/views/host/terminal/types/terminal.const';
|
import { fontFamilySuffix } from '../types/terminal.const';
|
||||||
import { InputProtocol } from '@/views/host/terminal/types/terminal.protocol';
|
import { InputProtocol } from '../types/terminal.protocol';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { WebglAddon } from 'xterm-addon-webgl';
|
import { WebglAddon } from 'xterm-addon-webgl';
|
||||||
@@ -54,14 +54,13 @@ export default class TerminalSession implements ITerminalSession {
|
|||||||
// 注册插件
|
// 注册插件
|
||||||
this.addons.fit = new FitAddon();
|
this.addons.fit = new FitAddon();
|
||||||
this.addons.webgl = new WebglAddon();
|
this.addons.webgl = new WebglAddon();
|
||||||
// TODO check
|
for (const addon of Object.values(this.addons)) {
|
||||||
const inst = this.inst;
|
this.inst.loadAddon(addon);
|
||||||
Object.values(this.addons).forEach(s => inst.loadAddon(s));
|
}
|
||||||
// 打开终端
|
// 打开终端
|
||||||
this.inst.open(dom);
|
this.inst.open(dom);
|
||||||
// 自适应
|
// 自适应
|
||||||
this.addons.fit.fit();
|
this.addons.fit.fit();
|
||||||
// TODO sendCheck
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置已连接
|
// 设置已连接
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ITerminalTabManager, TerminalTabItem } from '@/store/modules/terminal/types';
|
import type { ITerminalTabManager, TerminalTabItem } from '../types/terminal.type';
|
||||||
import { InnerTabs } from '@/views/host/terminal/types/terminal.const';
|
import { InnerTabs } from '../types/terminal.const';
|
||||||
|
|
||||||
// 终端 tab 管理器实现
|
// 终端 tab 管理器实现
|
||||||
export default class TerminalTabManager implements ITerminalTabManager {
|
export default class TerminalTabManager implements ITerminalTabManager {
|
||||||
@@ -28,7 +28,6 @@ export default class TerminalTabManager implements ITerminalTabManager {
|
|||||||
// 切换为前一个 tab
|
// 切换为前一个 tab
|
||||||
this.active = this.items[Math.max(tabIndex - 1, 0)].key;
|
this.active = this.items[Math.max(tabIndex - 1, 0)].key;
|
||||||
}
|
}
|
||||||
// fixme 关闭 ws
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开 tab
|
// 打开 tab
|
||||||
|
|||||||
@@ -1,17 +1,3 @@
|
|||||||
// 终端协议
|
|
||||||
export interface Protocol {
|
|
||||||
type: string;
|
|
||||||
template: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 终端内容
|
|
||||||
export interface Payload {
|
|
||||||
type?: string;
|
|
||||||
session?: string;
|
|
||||||
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 输入协议
|
// 输入协议
|
||||||
export const InputProtocol = {
|
export const InputProtocol = {
|
||||||
// 主机连接检查
|
// 主机连接检查
|
||||||
@@ -74,47 +60,3 @@ export const OutputProtocol = {
|
|||||||
template: ['type', 'session', 'body']
|
template: ['type', 'session', 'body']
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 分隔符
|
|
||||||
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: Record<string, any> = {};
|
|
||||||
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: Payload) => {
|
|
||||||
payload.type = protocol.type;
|
|
||||||
return protocol.template
|
|
||||||
.map(i => payload[i] || '')
|
|
||||||
.join(SEPARATOR);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
// 终端协议
|
import type { Terminal } from 'xterm';
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
|
|
||||||
|
// 终端 tab 元素
|
||||||
|
export interface TerminalTabItem {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 终端协议
|
||||||
export interface Protocol {
|
export interface Protocol {
|
||||||
type: string;
|
type: string;
|
||||||
template: string[];
|
template: string[];
|
||||||
@@ -22,13 +31,43 @@ export interface OutputPayload {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 终端 tab 管理器定义
|
||||||
|
export interface ITerminalTabManager {
|
||||||
|
// 当前 tab
|
||||||
|
active: string;
|
||||||
|
// 全部 tab
|
||||||
|
items: Array<TerminalTabItem>;
|
||||||
|
|
||||||
|
// 点击 tab
|
||||||
|
clickTab: (key: string) => void;
|
||||||
|
// 删除 tab
|
||||||
|
deleteTab: (key: string) => void;
|
||||||
|
// 打开 tab
|
||||||
|
openTab: (tab: TerminalTabItem) => void;
|
||||||
|
// 清空
|
||||||
|
clear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 终端会话管理器定义
|
||||||
|
export interface ITerminalSessionManager {
|
||||||
|
// 打开终端会话
|
||||||
|
openSession: (tab: TerminalTabItem, dom: HTMLElement) => void;
|
||||||
|
// 获取终端会话
|
||||||
|
getSession: (sessionId: string) => ITerminalSession;
|
||||||
|
// 关闭终端会话
|
||||||
|
closeSession: (sessionId: string) => void;
|
||||||
|
// 重置
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
// 终端通信处理器 定义
|
// 终端通信处理器 定义
|
||||||
export interface ITerminalChannel {
|
export interface ITerminalChannel {
|
||||||
// 初始化
|
// 初始化
|
||||||
init: () => Promise<void>;
|
init: () => Promise<void>;
|
||||||
|
// 是否已连接
|
||||||
|
isConnected: () => boolean;
|
||||||
// 发送消息
|
// 发送消息
|
||||||
send: (protocol: Protocol, payload: InputPayload) => void;
|
send: (protocol: Protocol, payload: InputPayload) => void;
|
||||||
|
|
||||||
// 关闭
|
// 关闭
|
||||||
close: () => void;
|
close: () => void;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user