From e0eb1446db2537beb641b0c4dbea13fe9c1839be Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Thu, 15 Aug 2024 08:16:38 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E4=BC=98=E5=8C=96=E7=BB=88?= =?UTF-8?q?=E7=AB=AF=E8=BF=9E=E6=8E=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + server/app/socket/terminal.js | 14 ++-- web/src/assets/scss/global.scss | 8 +- .../terminal/components/terminal-tab.vue | 81 +++++++++++-------- .../views/terminal/components/terminal.vue | 2 +- 5 files changed, 63 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b38289..dc8d4a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +* 终端连接逻辑重写,断线自动重连 ✔ * 终端支持选中复制&右键粘贴 ✔ * 终端设置支持字体大小 ✔ * 终端默认字体样式更改为`Cascadia Code` ✔ diff --git a/server/app/socket/terminal.js b/server/app/socket/terminal.js index 9c3006d..146a003 100644 --- a/server/app/socket/terminal.js +++ b/server/app/socket/terminal.js @@ -16,7 +16,7 @@ function createInteractiveShell(socket, sshClient) { consola.info('交互终端已关闭') sshClient.end() }) - socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了 + socket.emit('connect_shell_success') // 已连接终端,web端可以执行指令了 // web端输入 // socket.on('input', key => { // if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入') @@ -80,7 +80,7 @@ async function createTerminal(ip, socket, sshClient) { sshClient .on('ready', async() => { consola.success('终端连接成功:', host) - socket.emit('connect_success', `终端连接成功:${ host }`) + socket.emit('connect_terminal_success', `终端连接成功:${ host }`) let stream = await createInteractiveShell(socket, sshClient) resolve(stream) // execShell(sshClient, 'history', (data) => { @@ -90,12 +90,12 @@ async function createTerminal(ip, socket, sshClient) { // }) }) .on('close', () => { - consola.info('终端连接断开close') + consola.info('终端连接断开close: ', host) socket.emit('connect_close') }) .on('error', (err) => { consola.log(err) - consola.error('连接终端失败:', err.level) + consola.error('连接终端失败:', host, err.message) socket.emit('connect_fail', err.message) }) .connect({ @@ -103,7 +103,7 @@ async function createTerminal(ip, socket, sshClient) { // debug: (info) => console.log(info) }) } catch (err) { - consola.error('创建终端失败:', err.message) + consola.error('创建终端失败: ', host, err.message) socket.emit('create_fail', err.message) } }) @@ -134,7 +134,7 @@ module.exports = (httpServer) => { // setTimeout(() => { // sshClient.end() // }, 3000) - let stream = await createTerminal(ip, socket, sshClient) + let stream = null function listenerInput(key) { if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入') @@ -148,6 +148,7 @@ module.exports = (httpServer) => { socket.on('resize', resizeShell) // 重连 socket.on('reconnect_terminal', async () => { + consola.info('重连终端: ', ip) socket.off('input', listenerInput) // 取消监听,重新注册监听,操作新的stream socket.off('resize', resizeShell) sshClient?.end() @@ -161,6 +162,7 @@ module.exports = (httpServer) => { socket.on('resize', resizeShell) }, 3000) }) + stream = await createTerminal(ip, socket, sshClient) }) socket.on('disconnect', (reason) => { diff --git a/web/src/assets/scss/global.scss b/web/src/assets/scss/global.scss index 0de524a..c0cd9b5 100644 --- a/web/src/assets/scss/global.scss +++ b/web/src/assets/scss/global.scss @@ -3,20 +3,20 @@ html, body, div, ul, section, textarea { box-sizing: border-box; // 滚动条整体部分 &::-webkit-scrollbar { - height: 8px; - width: 2px; + height: 5px; + width: 5px; background-color: #ffffff; } // 底层轨道 &::-webkit-scrollbar-track { background-color: #ffffff; - border-radius: 10px; + border-radius: 3px; } // 滚动滑块 &::-webkit-scrollbar-thumb { - border-radius: 10px; + border-radius: 3px; // background-color: #1989fa; background-image: -webkit-gradient(linear, 40% 0%, 75% 84%, from(#a18cd1), to(#fbc2eb), color-stop(.6, #54DE5D)); } diff --git a/web/src/views/terminal/components/terminal-tab.vue b/web/src/views/terminal/components/terminal-tab.vue index feb1e41..e63e274 100644 --- a/web/src/views/terminal/components/terminal-tab.vue +++ b/web/src/views/terminal/components/terminal-tab.vue @@ -55,6 +55,7 @@ const timer = ref(null) const fitAddon = ref(null) const searchBar = ref(null) const isManual = ref(false) +const isConnectSuccess = ref(false) const isConnectFail = ref(false) const isConnecting = ref(true) const isReConnect = ref(false) @@ -107,59 +108,75 @@ const connectIO = () => { forceNew: false, reconnectionAttempts: 1 }) - socket.value.on('connect', () => { - console.log('/terminal socket已连接:', socket.value.id) + console.log('/terminal socket已连接:', host) socket.value.emit('create', { host, token: token.value }) - socket.value.on('connect_success', () => { + socket.value.on('connect_terminal_success', () => { isConnectFail.value = false isConnecting.value = false if (isReConnect.value) { isReConnect.value = false return // 重连不需要再注册监听事件 } - onData() - socket.value.on('connect_terminal', () => { + + socket.value.on('output', (str) => { + term.value.write(str) + terminalText.value += str + }) + + socket.value.on('connect_shell_success', () => { + isConnectSuccess.value = true onResize() onFindText() onWebLinks() if (command.value) socket.value.emit('input', command.value + '\n') }) + // socket.value.on('terminal_command_history', (data) => { // console.log(data) // commandHistoryList.value = data // }) }) + socket.value.on('token_verify_fail', () => { $notification({ title: 'Error', message: 'token校验失败,请重新登录', type: 'error' }) $router.push('/login') }) + socket.value.on('connect_close', () => { if (isConnectFail.value) return isReConnect.value = true // 重连状态标记为true isConnecting.value = true - console.warn('连接断开') + isConnectSuccess.value = false + console.warn('连接断开,3秒后重连: ', host) term.value.write('\r\n连接断开,3秒后自动重连...\r\n') - // socket.value.removeAllListeners() - // socket.value.off('output') // 取消output监听,取消onData输入监听,重新注册 socket.value.emit('reconnect_terminal') }) + socket.value.on('create_fail', (message) => { isConnectFail.value = true isConnecting.value = false - console.error(message) + isConnectSuccess.value = false + console.error('n创建失败:', host, message) term.value.write(`\r\n创建失败: ${ message }\r\n`) }) + socket.value.on('connect_fail', (message) => { isConnectFail.value = true isConnecting.value = false - console.error('连接失败:', message) + isConnectSuccess.value = false + console.error('连接失败:', host, message) term.value.write(`\r\n连接失败: ${ message }\r\n`) }) }) socket.value.on('disconnect', () => { console.warn('terminal websocket 连接断开') + socket.value.removeAllListeners() // 取消所有监听 + // socket.value.off('output') // 取消output监听,取消onData输入监听,重新注册 + isConnectFail.value = true + isConnecting.value = true + isConnectSuccess.value = false if (!isManual.value) $notification({ title: '与面板socket连接断开', message: `${ props.host }-请检查socket服务是否稳定`, type: 'error' }) }) @@ -291,11 +308,6 @@ function extractLastCdPath(text) { } const onData = () => { - socket.value.on('output', (str) => { - term.value.write(str) - terminalText.value += str - // console.log(terminalText.value) - }) // term.value.off('data', listenerInput) term.value.onData((key) => { let acsiiCode = key.codePointAt() @@ -305,31 +317,35 @@ const onData = () => { if (enterTimer.value) clearTimeout(enterTimer.value) if (key === '\r') { // Enter if (isConnectFail.value && !isConnecting.value) { // 连接失败&&未正在连接,按回车可触发重连 + isConnecting.value = true term.value.write('\r\n连接中...\r\n') socket.value.emit('reconnect_terminal') return } - let cleanText = applyBackspace(filterAnsiSequences(terminalText.value)) - const lines = cleanText.split('\n') - // console.log('lines: ', lines) - const lastLine = lines[lines.length - 1].trim() - // console.log('lastLine: ', lastLine) - // 截取最后一个提示符后的内容('$'或'#'后的内容) - const commandStartIndex = lastLine.lastIndexOf('#') + 1 - const commandText = lastLine.substring(commandStartIndex).trim() - // console.log('Processed command: ', commandText) - // eslint-disable-next-line - const cdPath = extractLastCdPath(commandText) + if (isConnectSuccess.value) { + let cleanText = applyBackspace(filterAnsiSequences(terminalText.value)) + const lines = cleanText.split('\n') + // console.log('lines: ', lines) + const lastLine = lines[lines.length - 1].trim() + // console.log('lastLine: ', lastLine) + // 截取最后一个提示符后的内容('$'或'#'后的内容) + const commandStartIndex = lastLine.lastIndexOf('#') + 1 + const commandText = lastLine.substring(commandStartIndex).trim() + // console.log('Processed command: ', commandText) + // eslint-disable-next-line + const cdPath = extractLastCdPath(commandText) - if (cdPath) { - console.log('cd command path:', cdPath) - let firstChar = cdPath.charAt(0) - if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // 后端依赖不支持 '~' - emit('cdCommand', cdPath) + if (cdPath) { + console.log('cd command path:', cdPath) + let firstChar = cdPath.charAt(0) + if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // 后端依赖不支持 '~' + emit('cdCommand', cdPath) + } + terminalText.value = '' } - terminalText.value = '' } }) + if (isConnectFail.value || isConnecting.value) return console.warn(`isConnectFail: ${ isConnectFail.value }, isConnecting: ${ isConnecting.value }`) emit('inputCommand', key) socket.value.emit('input', key) }) @@ -377,6 +393,7 @@ onMounted(async () => { createLocalTerminal() await getCommand() connectIO() + onData() }) onBeforeUnmount(() => { diff --git a/web/src/views/terminal/components/terminal.vue b/web/src/views/terminal/components/terminal.vue index 5f09842..6b52ae5 100644 --- a/web/src/views/terminal/components/terminal.vue +++ b/web/src/views/terminal/components/terminal.vue @@ -372,7 +372,7 @@ const handleInputCommand = async (command) => { :deep(.el-tabs__content) { flex: 1; width: 100%; - padding: 0 5px 5px 0; + padding: 0 0 5px 0; } :deep(.el-tabs--border-card) {