优化终端连接逻辑

This commit is contained in:
chaos-zhu 2024-08-15 08:16:38 +08:00
parent 21a97bf375
commit e0eb1446db
5 changed files with 63 additions and 43 deletions

View File

@ -2,6 +2,7 @@
### Features ### Features
* 终端连接逻辑重写,断线自动重连 ✔
* 终端支持选中复制&右键粘贴 ✔ * 终端支持选中复制&右键粘贴 ✔
* 终端设置支持字体大小 ✔ * 终端设置支持字体大小 ✔
* 终端默认字体样式更改为`Cascadia Code` * 终端默认字体样式更改为`Cascadia Code`

View File

@ -16,7 +16,7 @@ function createInteractiveShell(socket, sshClient) {
consola.info('交互终端已关闭') consola.info('交互终端已关闭')
sshClient.end() sshClient.end()
}) })
socket.emit('connect_terminal') // 已连接终端web端可以执行指令了 socket.emit('connect_shell_success') // 已连接终端web端可以执行指令了
// web端输入 // web端输入
// socket.on('input', key => { // socket.on('input', key => {
// if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入') // if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
@ -80,7 +80,7 @@ async function createTerminal(ip, socket, sshClient) {
sshClient sshClient
.on('ready', async() => { .on('ready', async() => {
consola.success('终端连接成功:', host) consola.success('终端连接成功:', host)
socket.emit('connect_success', `终端连接成功:${ host }`) socket.emit('connect_terminal_success', `终端连接成功:${ host }`)
let stream = await createInteractiveShell(socket, sshClient) let stream = await createInteractiveShell(socket, sshClient)
resolve(stream) resolve(stream)
// execShell(sshClient, 'history', (data) => { // execShell(sshClient, 'history', (data) => {
@ -90,12 +90,12 @@ async function createTerminal(ip, socket, sshClient) {
// }) // })
}) })
.on('close', () => { .on('close', () => {
consola.info('终端连接断开close') consola.info('终端连接断开close: ', host)
socket.emit('connect_close') socket.emit('connect_close')
}) })
.on('error', (err) => { .on('error', (err) => {
consola.log(err) consola.log(err)
consola.error('连接终端失败:', err.level) consola.error('连接终端失败:', host, err.message)
socket.emit('connect_fail', err.message) socket.emit('connect_fail', err.message)
}) })
.connect({ .connect({
@ -103,7 +103,7 @@ async function createTerminal(ip, socket, sshClient) {
// debug: (info) => console.log(info) // debug: (info) => console.log(info)
}) })
} catch (err) { } catch (err) {
consola.error('创建终端失败:', err.message) consola.error('创建终端失败: ', host, err.message)
socket.emit('create_fail', err.message) socket.emit('create_fail', err.message)
} }
}) })
@ -134,7 +134,7 @@ module.exports = (httpServer) => {
// setTimeout(() => { // setTimeout(() => {
// sshClient.end() // sshClient.end()
// }, 3000) // }, 3000)
let stream = await createTerminal(ip, socket, sshClient) let stream = null
function listenerInput(key) { function listenerInput(key) {
if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入') if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
@ -148,6 +148,7 @@ module.exports = (httpServer) => {
socket.on('resize', resizeShell) socket.on('resize', resizeShell)
// 重连 // 重连
socket.on('reconnect_terminal', async () => { socket.on('reconnect_terminal', async () => {
consola.info('重连终端: ', ip)
socket.off('input', listenerInput) // 取消监听,重新注册监听,操作新的stream socket.off('input', listenerInput) // 取消监听,重新注册监听,操作新的stream
socket.off('resize', resizeShell) socket.off('resize', resizeShell)
sshClient?.end() sshClient?.end()
@ -161,6 +162,7 @@ module.exports = (httpServer) => {
socket.on('resize', resizeShell) socket.on('resize', resizeShell)
}, 3000) }, 3000)
}) })
stream = await createTerminal(ip, socket, sshClient)
}) })
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {

View File

@ -3,20 +3,20 @@ html, body, div, ul, section, textarea {
box-sizing: border-box; box-sizing: border-box;
// 滚动条整体部分 // 滚动条整体部分
&::-webkit-scrollbar { &::-webkit-scrollbar {
height: 8px; height: 5px;
width: 2px; width: 5px;
background-color: #ffffff; background-color: #ffffff;
} }
// 底层轨道 // 底层轨道
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background-color: #ffffff; background-color: #ffffff;
border-radius: 10px; border-radius: 3px;
} }
// 滚动滑块 // 滚动滑块
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
border-radius: 10px; border-radius: 3px;
// background-color: #1989fa; // background-color: #1989fa;
background-image: -webkit-gradient(linear, 40% 0%, 75% 84%, from(#a18cd1), to(#fbc2eb), color-stop(.6, #54DE5D)); background-image: -webkit-gradient(linear, 40% 0%, 75% 84%, from(#a18cd1), to(#fbc2eb), color-stop(.6, #54DE5D));
} }

View File

@ -55,6 +55,7 @@ const timer = ref(null)
const fitAddon = ref(null) const fitAddon = ref(null)
const searchBar = ref(null) const searchBar = ref(null)
const isManual = ref(false) const isManual = ref(false)
const isConnectSuccess = ref(false)
const isConnectFail = ref(false) const isConnectFail = ref(false)
const isConnecting = ref(true) const isConnecting = ref(true)
const isReConnect = ref(false) const isReConnect = ref(false)
@ -107,59 +108,75 @@ const connectIO = () => {
forceNew: false, forceNew: false,
reconnectionAttempts: 1 reconnectionAttempts: 1
}) })
socket.value.on('connect', () => { 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.emit('create', { host, token: token.value })
socket.value.on('connect_success', () => { socket.value.on('connect_terminal_success', () => {
isConnectFail.value = false isConnectFail.value = false
isConnecting.value = false isConnecting.value = false
if (isReConnect.value) { if (isReConnect.value) {
isReConnect.value = false isReConnect.value = false
return // 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() onResize()
onFindText() onFindText()
onWebLinks() onWebLinks()
if (command.value) socket.value.emit('input', command.value + '\n') if (command.value) socket.value.emit('input', command.value + '\n')
}) })
// socket.value.on('terminal_command_history', (data) => { // socket.value.on('terminal_command_history', (data) => {
// console.log(data) // console.log(data)
// commandHistoryList.value = data // commandHistoryList.value = data
// }) // })
}) })
socket.value.on('token_verify_fail', () => { socket.value.on('token_verify_fail', () => {
$notification({ title: 'Error', message: 'token校验失败请重新登录', type: 'error' }) $notification({ title: 'Error', message: 'token校验失败请重新登录', type: 'error' })
$router.push('/login') $router.push('/login')
}) })
socket.value.on('connect_close', () => { socket.value.on('connect_close', () => {
if (isConnectFail.value) return if (isConnectFail.value) return
isReConnect.value = true // true isReConnect.value = true // true
isConnecting.value = true isConnecting.value = true
console.warn('连接断开') isConnectSuccess.value = false
console.warn('连接断开,3秒后重连: ', host)
term.value.write('\r\n连接断开,3秒后自动重连...\r\n') term.value.write('\r\n连接断开,3秒后自动重连...\r\n')
// socket.value.removeAllListeners()
// socket.value.off('output') // output,onData
socket.value.emit('reconnect_terminal') socket.value.emit('reconnect_terminal')
}) })
socket.value.on('create_fail', (message) => { socket.value.on('create_fail', (message) => {
isConnectFail.value = true isConnectFail.value = true
isConnecting.value = false isConnecting.value = false
console.error(message) isConnectSuccess.value = false
console.error('n创建失败:', host, message)
term.value.write(`\r\n创建失败: ${ message }\r\n`) term.value.write(`\r\n创建失败: ${ message }\r\n`)
}) })
socket.value.on('connect_fail', (message) => { socket.value.on('connect_fail', (message) => {
isConnectFail.value = true isConnectFail.value = true
isConnecting.value = false isConnecting.value = false
console.error('连接失败:', message) isConnectSuccess.value = false
console.error('连接失败:', host, message)
term.value.write(`\r\n连接失败: ${ message }\r\n`) term.value.write(`\r\n连接失败: ${ message }\r\n`)
}) })
}) })
socket.value.on('disconnect', () => { socket.value.on('disconnect', () => {
console.warn('terminal websocket 连接断开') 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' }) if (!isManual.value) $notification({ title: '与面板socket连接断开', message: `${ props.host }-请检查socket服务是否稳定`, type: 'error' })
}) })
@ -291,11 +308,6 @@ function extractLastCdPath(text) {
} }
const onData = () => { 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.off('data', listenerInput)
term.value.onData((key) => { term.value.onData((key) => {
let acsiiCode = key.codePointAt() let acsiiCode = key.codePointAt()
@ -305,31 +317,35 @@ const onData = () => {
if (enterTimer.value) clearTimeout(enterTimer.value) if (enterTimer.value) clearTimeout(enterTimer.value)
if (key === '\r') { // Enter if (key === '\r') { // Enter
if (isConnectFail.value && !isConnecting.value) { // && if (isConnectFail.value && !isConnecting.value) { // &&
isConnecting.value = true
term.value.write('\r\n连接中...\r\n') term.value.write('\r\n连接中...\r\n')
socket.value.emit('reconnect_terminal') socket.value.emit('reconnect_terminal')
return return
} }
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value)) if (isConnectSuccess.value) {
const lines = cleanText.split('\n') let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
// console.log('lines: ', lines) const lines = cleanText.split('\n')
const lastLine = lines[lines.length - 1].trim() // console.log('lines: ', lines)
// console.log('lastLine: ', lastLine) const lastLine = lines[lines.length - 1].trim()
// '$''#' // console.log('lastLine: ', lastLine)
const commandStartIndex = lastLine.lastIndexOf('#') + 1 // '$''#'
const commandText = lastLine.substring(commandStartIndex).trim() const commandStartIndex = lastLine.lastIndexOf('#') + 1
// console.log('Processed command: ', commandText) const commandText = lastLine.substring(commandStartIndex).trim()
// eslint-disable-next-line // console.log('Processed command: ', commandText)
const cdPath = extractLastCdPath(commandText) // eslint-disable-next-line
const cdPath = extractLastCdPath(commandText)
if (cdPath) { if (cdPath) {
console.log('cd command path:', cdPath) console.log('cd command path:', cdPath)
let firstChar = cdPath.charAt(0) let firstChar = cdPath.charAt(0)
if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // '~' if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // '~'
emit('cdCommand', 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) emit('inputCommand', key)
socket.value.emit('input', key) socket.value.emit('input', key)
}) })
@ -377,6 +393,7 @@ onMounted(async () => {
createLocalTerminal() createLocalTerminal()
await getCommand() await getCommand()
connectIO() connectIO()
onData()
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -372,7 +372,7 @@ const handleInputCommand = async (command) => {
:deep(.el-tabs__content) { :deep(.el-tabs__content) {
flex: 1; flex: 1;
width: 100%; width: 100%;
padding: 0 5px 5px 0; padding: 0 0 5px 0;
} }
:deep(.el-tabs--border-card) { :deep(.el-tabs--border-card) {