⚡ 优化会话关闭处理逻辑.
This commit is contained in:
@@ -65,7 +65,7 @@ export function deleteHostSftpLog(idList: Array<number>) {
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
export function downloadWithTransferToken(channelId: string, transferToken: string) {
|
||||
window.open(`${httpBaseUrl}/asset/host-sftp/download?channelId=${channelId}&transferToken=${transferToken}`, 'newWindow');
|
||||
export function getDownloadTransferUrl(channelId: string, transferToken: string) {
|
||||
return `${httpBaseUrl}/asset/host-sftp/download?channelId=${channelId}&transferToken=${transferToken}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,27 @@ export function getParentPath(path: string) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开下载文件
|
||||
*/
|
||||
export function openDownloadFile(url: string) {
|
||||
try {
|
||||
// 创建隐藏的可下载链接
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', url);
|
||||
element.setAttribute('download', '');
|
||||
element.style.display = 'none';
|
||||
// 将其附加到文档中
|
||||
document.body.appendChild(element);
|
||||
// 点击该下载链接
|
||||
element.click();
|
||||
// 移除已下载的链接
|
||||
document.body.removeChild(element);
|
||||
} catch (e) {
|
||||
window.open(url, 'newWindow');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// 执行
|
||||
export const historyCount = 20;
|
||||
// 历史数量
|
||||
export const historyCount = 15;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { ISftpTransferManager, ISftpTransferUploader, SftpTransferItem } from '../types/terminal.type';
|
||||
import { ISftpTransferDownloader, SftpFile, TransferOperatorResponse } from '../types/terminal.type';
|
||||
import { TransferReceiverType, TransferStatus, TransferType } from '../types/terminal.const';
|
||||
import { sessionCloseMsg, TransferReceiverType, TransferStatus, TransferType } from '../types/terminal.const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getTerminalAccessToken, openHostTransferChannel } from '@/api/asset/host-terminal';
|
||||
import { nextId } from '@/utils';
|
||||
import { downloadWithTransferToken } from '@/api/asset/host-sftp';
|
||||
import { getDownloadTransferUrl } from '@/api/asset/host-sftp';
|
||||
import SftpTransferUploader from './sftp-transfer-uploader';
|
||||
import SftpTransferDownloader from './sftp-transfer-downloader';
|
||||
import { openDownloadFile } from '@/utils/file';
|
||||
|
||||
// sftp 传输管理器实现
|
||||
export default class SftpTransferManager implements ISftpTransferManager {
|
||||
@@ -257,7 +258,10 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
|
||||
// 接收开始下载响应
|
||||
private resolveDownloadStart(data: TransferOperatorResponse) {
|
||||
downloadWithTransferToken(data.channelId as string, data.transferToken as string);
|
||||
// 获取下载 url
|
||||
const url = getDownloadTransferUrl(data.channelId as string, data.transferToken as string);
|
||||
// 打开
|
||||
openDownloadFile(url);
|
||||
}
|
||||
|
||||
// 接收下载进度响应
|
||||
@@ -287,6 +291,14 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
this.run = false;
|
||||
// 关闭传输进度
|
||||
clearInterval(this.progressIntervalId);
|
||||
// 进行中和等待中的文件改为失败
|
||||
this.transferList.forEach(s => {
|
||||
if (s.status === TransferStatus.WAITING ||
|
||||
s.status === TransferStatus.TRANSFERRING) {
|
||||
s.status = TransferStatus.ERROR;
|
||||
s.errorMessage = sessionCloseMsg;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager, OutputPayload, Protocol, } from '../types/terminal.type';
|
||||
import { OutputProtocol } from '../types/terminal.protocol';
|
||||
import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager, Protocol, } from '../types/terminal.type';
|
||||
import { format, OutputProtocol, parse } from '../types/terminal.protocol';
|
||||
import { sessionCloseMsg } from '../types/terminal.const';
|
||||
import { getTerminalAccessToken, openHostTerminalChannel } from '@/api/asset/host-terminal';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import TerminalOutputProcessor from './terminal-output-processor';
|
||||
@@ -9,9 +10,12 @@ export default class TerminalChannel implements ITerminalChannel {
|
||||
|
||||
private client?: WebSocket;
|
||||
|
||||
private readonly sessionManager: ITerminalSessionManager;
|
||||
|
||||
private readonly processor: ITerminalOutputProcessor;
|
||||
|
||||
constructor(sessionManager: ITerminalSessionManager) {
|
||||
this.sessionManager = sessionManager;
|
||||
this.processor = new TerminalOutputProcessor(sessionManager, this);
|
||||
}
|
||||
|
||||
@@ -29,6 +33,8 @@ export default class TerminalChannel implements ITerminalChannel {
|
||||
}
|
||||
this.client.onclose = event => {
|
||||
console.warn('terminal close', event);
|
||||
// 关闭回调
|
||||
this.closeCallback();
|
||||
};
|
||||
this.client.onmessage = this.handlerMessage.bind(this);
|
||||
}
|
||||
@@ -66,6 +72,25 @@ export default class TerminalChannel implements ITerminalChannel {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭回调
|
||||
private closeCallback(): void {
|
||||
// 关闭时将手动触发 close 消息, 有可能是其他原因关闭的, 没有接收到 close 消息, 导致已断开是终端还是显示已连接
|
||||
Object.values(this.sessionManager.sessions).forEach(s => {
|
||||
if (!s?.connected) {
|
||||
return;
|
||||
}
|
||||
// close 消息
|
||||
const data = format(OutputProtocol.CLOSE, {
|
||||
type: OutputProtocol.CLOSE.type,
|
||||
sessionId: s.sessionId,
|
||||
forceClose: 0,
|
||||
msg: sessionCloseMsg,
|
||||
});
|
||||
// 触发 close 消息
|
||||
this.handlerMessage({ data } as MessageEvent);
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭
|
||||
close(): void {
|
||||
// 关闭 client
|
||||
@@ -78,55 +103,3 @@ export default class TerminalChannel implements ITerminalChannel {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 分隔符
|
||||
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 => getPayloadValueString(payload[i]))
|
||||
.join(SEPARATOR);
|
||||
};
|
||||
|
||||
// 获取默认值
|
||||
export const getPayloadValueString = (value: unknown): any => {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
}
|
||||
|
||||
// 处理关闭消息
|
||||
processClose({ sessionId, msg, forceClose }: OutputPayload): void {
|
||||
processClose({ sessionId, forceClose, msg }: OutputPayload): void {
|
||||
const session = this.sessionManager.getSession(sessionId);
|
||||
// 无需处理 (直接关闭 tab)
|
||||
if (!session) {
|
||||
|
||||
@@ -19,17 +19,17 @@ import SftpSession from './sftp-session';
|
||||
// 终端会话管理器实现
|
||||
export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||
|
||||
private readonly channel: ITerminalChannel;
|
||||
public sessions: Record<string, ITerminalSession>;
|
||||
|
||||
private sessions: Record<string, ITerminalSession>;
|
||||
private readonly channel: ITerminalChannel;
|
||||
|
||||
private keepAliveTaskId?: any;
|
||||
|
||||
private readonly dispatchResizeFn: () => {};
|
||||
|
||||
constructor() {
|
||||
this.channel = new TerminalChannel(this);
|
||||
this.sessions = {};
|
||||
this.channel = new TerminalChannel(this);
|
||||
this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -352,6 +352,9 @@ export const TransferReceiverType = {
|
||||
DOWNLOAD_ERROR: 'downloadError',
|
||||
};
|
||||
|
||||
// 会话关闭信息
|
||||
export const sessionCloseMsg = 'session closed...';
|
||||
|
||||
// 打开 settingModal key
|
||||
export const openSettingModalKey = Symbol();
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type { InputPayload, OutputPayload, Protocol } from './terminal.type';
|
||||
|
||||
// 分隔符
|
||||
export const SEPARATOR = '|';
|
||||
|
||||
// 输入协议
|
||||
export const InputProtocol = {
|
||||
// 主机连接检查
|
||||
@@ -164,3 +169,52 @@ export const OutputProtocol = {
|
||||
processMethod: 'processSftpSetContent'
|
||||
},
|
||||
};
|
||||
|
||||
// 解析参数
|
||||
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 | OutputPayload) => {
|
||||
payload.type = protocol.type;
|
||||
return protocol.template
|
||||
.map(i => getPayloadValueString(payload[i]))
|
||||
.join(SEPARATOR);
|
||||
};
|
||||
|
||||
// 获取默认值
|
||||
export const getPayloadValueString = (value: unknown): any => {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -153,6 +153,9 @@ export interface ITerminalPanelManager {
|
||||
|
||||
// 终端会话管理器定义
|
||||
export interface ITerminalSessionManager {
|
||||
// 全部会话
|
||||
sessions: Record<string, ITerminalSession>;
|
||||
|
||||
// 打开 ssh 会话
|
||||
openSsh: (tab: TerminalPanelTabItem, domRef: XtermDomRef) => Promise<ISshSession>;
|
||||
// 打开 sftp 会话
|
||||
|
||||
Reference in New Issue
Block a user