✨ 重构终端连接逻辑
This commit is contained in:
parent
5aaad74c57
commit
21a97bf375
@ -2,29 +2,31 @@ const { Server } = require('socket.io')
|
|||||||
const { Client: SSHClient } = require('ssh2')
|
const { Client: SSHClient } = require('ssh2')
|
||||||
const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync } = require('../utils')
|
const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync } = require('../utils')
|
||||||
|
|
||||||
function createTerminal(socket, sshClient) {
|
function createInteractiveShell(socket, sshClient) {
|
||||||
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
return new Promise((resolve) => {
|
||||||
if (err) return socket.emit('output', err.toString())
|
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||||
// 终端输出
|
resolve(stream)
|
||||||
stream
|
if (err) return socket.emit('output', err.toString())
|
||||||
.on('data', (data) => {
|
// 终端输出
|
||||||
socket.emit('output', data.toString())
|
stream
|
||||||
})
|
.on('data', (data) => {
|
||||||
.on('close', () => {
|
socket.emit('output', data.toString())
|
||||||
consola.info('关闭终端')
|
})
|
||||||
sshClient.end()
|
.on('close', () => {
|
||||||
})
|
consola.info('交互终端已关闭')
|
||||||
// web端输入
|
sshClient.end()
|
||||||
socket.on('input', key => {
|
})
|
||||||
if (sshClient._sock.writable === false) return consola.info('终端连接已关闭')
|
socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了
|
||||||
stream.write(key)
|
// web端输入
|
||||||
})
|
// socket.on('input', key => {
|
||||||
socket.emit('connect_terminal') // 已连接终端,web端可以执行指令了
|
// if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
|
||||||
|
// stream.write(key)
|
||||||
// 监听按键重置终端大小
|
// })
|
||||||
socket.on('resize', ({ rows, cols }) => {
|
// 监听按键重置终端大小
|
||||||
// consola.info('更改tty终端行&列: ', { rows, cols })
|
// socket.on('resize', ({ rows, cols }) => {
|
||||||
stream.setWindow(rows, cols)
|
// // consola.info('更改tty终端行&列: ', { rows, cols })
|
||||||
|
// stream.setWindow(rows, cols)
|
||||||
|
// })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -52,6 +54,61 @@ function execShell(sshClient, command = '', callback) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createTerminal(ip, socket, sshClient) {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
const hostList = await readHostList()
|
||||||
|
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
||||||
|
let { authType, host, port, username } = targetHostInfo
|
||||||
|
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
||||||
|
let authInfo = { host, port, username }
|
||||||
|
// 统一使用commonKey解密
|
||||||
|
try {
|
||||||
|
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
||||||
|
if (authType === 'credential') {
|
||||||
|
let credentialId = await AESDecryptSync(targetHostInfo[authType])
|
||||||
|
const sshRecordList = await readSSHRecord()
|
||||||
|
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
||||||
|
authInfo.authType = sshRecord.authType
|
||||||
|
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
||||||
|
} else {
|
||||||
|
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
|
||||||
|
}
|
||||||
|
consola.info('准备连接终端:', host)
|
||||||
|
// targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
||||||
|
consola.log('连接信息', { username, port, authType })
|
||||||
|
sshClient
|
||||||
|
.on('ready', async() => {
|
||||||
|
consola.success('终端连接成功:', host)
|
||||||
|
socket.emit('connect_success', `终端连接成功:${ host }`)
|
||||||
|
let stream = await createInteractiveShell(socket, sshClient)
|
||||||
|
resolve(stream)
|
||||||
|
// execShell(sshClient, 'history', (data) => {
|
||||||
|
// data = data.split('\n').filter(item => item)
|
||||||
|
// console.log(data)
|
||||||
|
// socket.emit('terminal_command_history', data)
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
.on('close', () => {
|
||||||
|
consola.info('终端连接断开close')
|
||||||
|
socket.emit('connect_close')
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
consola.log(err)
|
||||||
|
consola.error('连接终端失败:', err.level)
|
||||||
|
socket.emit('connect_fail', err.message)
|
||||||
|
})
|
||||||
|
.connect({
|
||||||
|
...authInfo
|
||||||
|
// debug: (info) => console.log(info)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
consola.error('创建终端失败:', err.message)
|
||||||
|
socket.emit('create_fail', err.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = (httpServer) => {
|
module.exports = (httpServer) => {
|
||||||
const serverIo = new Server(httpServer, {
|
const serverIo = new Server(httpServer, {
|
||||||
path: '/terminal',
|
path: '/terminal',
|
||||||
@ -62,9 +119,8 @@ module.exports = (httpServer) => {
|
|||||||
serverIo.on('connection', (socket) => {
|
serverIo.on('connection', (socket) => {
|
||||||
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
// 前者兼容nginx反代, 后者兼容nodejs自身服务
|
||||||
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
let clientIp = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
|
||||||
let sshClient = new SSHClient()
|
|
||||||
consola.success('terminal websocket 已连接')
|
consola.success('terminal websocket 已连接')
|
||||||
|
let sshClient = null
|
||||||
socket.on('create', async ({ host: ip, token }) => {
|
socket.on('create', async ({ host: ip, token }) => {
|
||||||
const { code } = await verifyAuthSync(token, clientIp)
|
const { code } = await verifyAuthSync(token, clientIp)
|
||||||
if (code !== 1) {
|
if (code !== 1) {
|
||||||
@ -72,57 +128,43 @@ module.exports = (httpServer) => {
|
|||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const hostList = await readHostList()
|
sshClient = new SSHClient()
|
||||||
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
|
||||||
let { authType, host, port, username } = targetHostInfo
|
// 尝试手动断开调试,再次连接后终端输出内容为4份相同的输出,导致异常
|
||||||
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
// setTimeout(() => {
|
||||||
let authInfo = { host, port, username }
|
// sshClient.end()
|
||||||
// 统一使用commonKey解密
|
// }, 3000)
|
||||||
try {
|
let stream = await createTerminal(ip, socket, sshClient)
|
||||||
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
|
||||||
if (authType === 'credential') {
|
function listenerInput(key) {
|
||||||
let credentialId = await AESDecryptSync(targetHostInfo[authType])
|
if (sshClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
|
||||||
const sshRecordList = await readSSHRecord()
|
stream.write(key)
|
||||||
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
|
||||||
authInfo.authType = sshRecord.authType
|
|
||||||
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
|
||||||
} else {
|
|
||||||
authInfo[authType] = await AESDecryptSync(targetHostInfo[authType])
|
|
||||||
}
|
|
||||||
consola.info('准备连接终端:', host)
|
|
||||||
// targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
|
||||||
consola.log('连接信息', { username, port, authType })
|
|
||||||
sshClient
|
|
||||||
.on('ready', () => {
|
|
||||||
consola.success('连接终端成功:', host)
|
|
||||||
socket.emit('connect_success', `已连接到终端:${ host }`)
|
|
||||||
createTerminal(socket, sshClient)
|
|
||||||
// execShell(sshClient, 'history', (data) => {
|
|
||||||
// data = data.split('\n').filter(item => item)
|
|
||||||
// console.log(data)
|
|
||||||
// socket.emit('terminal_command_history', data)
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
.on('error', (err) => {
|
|
||||||
console.log(err)
|
|
||||||
consola.error('连接终端失败:', err.level)
|
|
||||||
socket.emit('connect_fail', err.message)
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
...authInfo
|
|
||||||
// debug: (info) => console.log(info)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
consola.error('创建终端失败:', err.message)
|
|
||||||
socket.emit('create_fail', err.message)
|
|
||||||
}
|
}
|
||||||
|
function resizeShell({ rows, cols }) {
|
||||||
|
// consola.info('更改tty终端行&列: ', { rows, cols })
|
||||||
|
stream.setWindow(rows, cols)
|
||||||
|
}
|
||||||
|
socket.on('input', listenerInput)
|
||||||
|
socket.on('resize', resizeShell)
|
||||||
|
// 重连
|
||||||
|
socket.on('reconnect_terminal', async () => {
|
||||||
|
socket.off('input', listenerInput) // 取消监听,重新注册监听,操作新的stream
|
||||||
|
socket.off('resize', resizeShell)
|
||||||
|
sshClient?.end()
|
||||||
|
sshClient?.destroy()
|
||||||
|
sshClient = null
|
||||||
|
setTimeout(async () => {
|
||||||
|
// 初始化新的SSH客户端对象
|
||||||
|
sshClient = new SSHClient()
|
||||||
|
stream = await createTerminal(ip, socket, sshClient)
|
||||||
|
socket.on('input', listenerInput)
|
||||||
|
socket.on('resize', resizeShell)
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
consola.info('终端连接断开:', reason)
|
consola.info('终端socket连接断开:', reason)
|
||||||
sshClient.end()
|
|
||||||
sshClient.destroy()
|
|
||||||
sshClient = null
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ 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 isConnectFail = ref(false)
|
||||||
|
const isConnecting = ref(true)
|
||||||
|
const isReConnect = ref(false)
|
||||||
const terminal = ref(null)
|
const terminal = ref(null)
|
||||||
const terminalRef = ref(null)
|
const terminalRef = ref(null)
|
||||||
|
|
||||||
@ -109,6 +112,12 @@ const connectIO = () => {
|
|||||||
console.log('/terminal socket已连接:', socket.value.id)
|
console.log('/terminal socket已连接:', socket.value.id)
|
||||||
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_success', () => {
|
||||||
|
isConnectFail.value = false
|
||||||
|
isConnecting.value = false
|
||||||
|
if (isReConnect.value) {
|
||||||
|
isReConnect.value = false
|
||||||
|
return // 重连不需要再注册监听事件
|
||||||
|
}
|
||||||
onData()
|
onData()
|
||||||
socket.value.on('connect_terminal', () => {
|
socket.value.on('connect_terminal', () => {
|
||||||
onResize()
|
onResize()
|
||||||
@ -121,35 +130,37 @@ const connectIO = () => {
|
|||||||
// commandHistoryList.value = data
|
// commandHistoryList.value = data
|
||||||
// })
|
// })
|
||||||
})
|
})
|
||||||
socket.value.on('create_fail', (message) => {
|
|
||||||
console.error(message)
|
|
||||||
$notification({
|
|
||||||
title: '创建失败',
|
|
||||||
message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
socket.value.on('token_verify_fail', () => {
|
socket.value.on('token_verify_fail', () => {
|
||||||
$notification({
|
$notification({ title: 'Error', message: 'token校验失败,请重新登录', type: 'error' })
|
||||||
title: 'Error',
|
|
||||||
message: 'token校验失败,请重新登录',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
$router.push('/login')
|
$router.push('/login')
|
||||||
})
|
})
|
||||||
socket.value.on('connect_fail', (message) => {
|
socket.value.on('connect_close', () => {
|
||||||
|
if (isConnectFail.value) return
|
||||||
|
isReConnect.value = true // 重连状态标记为true
|
||||||
|
isConnecting.value = true
|
||||||
|
console.warn('连接断开')
|
||||||
|
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)
|
console.error(message)
|
||||||
$notification({
|
term.value.write(`\r\n创建失败: ${ message }\r\n`)
|
||||||
title: '终端连接失败',
|
})
|
||||||
message,
|
socket.value.on('connect_fail', (message) => {
|
||||||
type: 'error'
|
isConnectFail.value = true
|
||||||
})
|
isConnecting.value = false
|
||||||
|
console.error('连接失败:', message)
|
||||||
|
term.value.write(`\r\n连接失败: ${ message }\r\n`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.value.on('disconnect', () => {
|
socket.value.on('disconnect', () => {
|
||||||
console.warn('terminal websocket 连接断开')
|
console.warn('terminal websocket 连接断开')
|
||||||
if (!isManual.value) reConnect()
|
if (!isManual.value) $notification({ title: '与面板socket连接断开', message: `${ props.host }-请检查socket服务是否稳定`, type: 'error' })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.value.on('connect_error', (err) => {
|
socket.value.on('connect_error', (err) => {
|
||||||
@ -162,21 +173,6 @@ const connectIO = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const reConnect = () => {
|
|
||||||
socket.value.close && socket.value.close()
|
|
||||||
$message.warning('终端连接断开')
|
|
||||||
// $messageBox.alert(
|
|
||||||
// '<strong>终端连接断开</strong>',
|
|
||||||
// 'Error',
|
|
||||||
// {
|
|
||||||
// dangerouslyUseHTMLString: true,
|
|
||||||
// confirmButtonText: '刷新页面'
|
|
||||||
// }
|
|
||||||
// ).then(() => {
|
|
||||||
// location.reload()
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
const createLocalTerminal = () => {
|
const createLocalTerminal = () => {
|
||||||
let terminalInstance = new Terminal({
|
let terminalInstance = new Terminal({
|
||||||
rendererType: 'dom',
|
rendererType: 'dom',
|
||||||
@ -300,6 +296,7 @@ const onData = () => {
|
|||||||
terminalText.value += str
|
terminalText.value += str
|
||||||
// console.log(terminalText.value)
|
// console.log(terminalText.value)
|
||||||
})
|
})
|
||||||
|
// term.value.off('data', listenerInput)
|
||||||
term.value.onData((key) => {
|
term.value.onData((key) => {
|
||||||
let acsiiCode = key.codePointAt()
|
let acsiiCode = key.codePointAt()
|
||||||
if (acsiiCode === 22) return handlePaste()
|
if (acsiiCode === 22) return handlePaste()
|
||||||
@ -307,6 +304,11 @@ const onData = () => {
|
|||||||
enterTimer.value = setTimeout(() => {
|
enterTimer.value = setTimeout(() => {
|
||||||
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) { // 连接失败&&未正在连接,按回车可触发重连
|
||||||
|
term.value.write('\r\n连接中...\r\n')
|
||||||
|
socket.value.emit('reconnect_terminal')
|
||||||
|
return
|
||||||
|
}
|
||||||
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
|
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
|
||||||
const lines = cleanText.split('\n')
|
const lines = cleanText.split('\n')
|
||||||
// console.log('lines: ', lines)
|
// console.log('lines: ', lines)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user