import { Server } from 'socket.io' import ssh2Module from 'ssh2' const { Client: SSHClient } = ssh2Module import { sendNoticeAsync } from '../utils/notify.js' import { shellThrottle } from '../utils/tools.js' import { createSecureWs } from '../utils/ws-tool.js' import { HostListDB, OnekeyDB } from '../utils/db-class.js' import { getConnectionOptions, handleProxyAndJumpHostConnection } from './terminal.js' const hostListDB = new HostListDB().getInstance() const onekeyDB = new OnekeyDB().getInstance() const execStatusEnum = { connecting: '连接中', connectFail: '连接失败', executing: '执行中', execSuccess: '执行成功', execFail: '执行失败', execTimeout: '执行超时', socketInterrupt: '执行中断' } let isExecuting = false let execResult = [] let execClient = [] let jumpSshClientsPool = [] function disconnectAllExecClient() { execClient.forEach((sshClient) => { if (sshClient) { sshClient.end() sshClient.destroy() sshClient = null } }) jumpSshClientsPool.forEach(jumpSshClients => { jumpSshClients?.forEach(sshClient => sshClient && sshClient.end()) }) jumpSshClientsPool = [] } function execShell(socket, sshClient, curRes, resolve) { const throttledDataHandler = shellThrottle(() => { socket.emit('output', execResult) // const memoryUsage = process.memoryUsage() // const formattedMemoryUsage = { // rss: (memoryUsage.rss / 1024 / 1024).toFixed(2) + ' MB', // Resident Set Size: total memory allocated for the process execution // heapTotal: (memoryUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB', // Total size of the allocated heap // heapUsed: (memoryUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB', // Actual memory used during the execution // external: (memoryUsage.external / 1024 / 1024).toFixed(2) + ' MB', // Memory used by "external" components like V8 external memory // arrayBuffers: (memoryUsage.arrayBuffers / 1024 / 1024).toFixed(2) + ' MB' // Memory allocated for ArrayBuffer and SharedArrayBuffer, including all Node.js Buffers // } // console.log(formattedMemoryUsage) }, 1000) // 防止内存爆破 sshClient.exec(curRes.command, function(err, stream) { if (err) { console.log(curRes.host, '命令执行失败:', err) curRes.status = execStatusEnum.execFail curRes.result += err.toString() socket.emit('output', execResult) return } stream .on('close', async () => { // shell关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失 await throttledDataHandler.last() // 等待最后一次节流函数执行完成,再执行一次数据输出 // console.log('onekey终端执行完成, 关闭连接: ', curRes.host) if (curRes.status === execStatusEnum.executing) { curRes.status = execStatusEnum.execSuccess } socket.emit('output', execResult) resolve(curRes) sshClient.end() }) .on('data', (data) => { // console.log(curRes.host, '执行中: \n' + data) curRes.status = execStatusEnum.executing curRes.result += data.toString() // socket.emit('output', execResult) throttledDataHandler(data) }) .stderr .on('data', (data) => { // console.log(curRes.host, '命令执行过程中产生错误: ' + data) curRes.status = execStatusEnum.executing curRes.result += data.toString() // socket.emit('output', execResult) throttledDataHandler(data) }) }) } export default (httpServer) => { const serverIo = createSecureWs(httpServer, '/onekey') serverIo.on('connection', async (socket) => { logger.info('onekey-terminal websocket 已连接') if (isExecuting) { socket.emit('create_fail', '正在执行中, 请稍后再试') socket.disconnect() return } isExecuting = true socket.on('ws_onekey', async ({ hostIds, command, timeout }) => { console.log('onekey command:', command) const hostList = await hostListDB.findAsync({}) const targetHostsInfo = hostList.filter(item => hostIds.some(id => item._id === id)) || {} if (!targetHostsInfo.length) return socket.emit('create_fail', `未找到【${ hostIds }】服务器信息`) // 查找 hostInfo -> 并发执行 socket.emit('ready') let execPromise = targetHostsInfo.map((hostInfo, index) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { setTimeout(() => reject('执行超时'), timeout * 1000) let { host, port } = hostInfo let curRes = { command, host, port, name: hostInfo.name, result: '', status: execStatusEnum.connecting, startDate: Date.now() + index } execResult.push(curRes) let jumpSshClients = [] try { let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostInfo._id) // 使用通用的代理和跳板机连接处理函数 try { const result = await handleProxyAndJumpHostConnection({ hostInfo, targetConnectionOptions, socket: null, // Onekey不需要发送terminal_print_info事件 logPrefix: 'Onekey ' }) jumpSshClients = result.jumpSshClients if (jumpSshClients.length > 0) { jumpSshClientsPool.push(jumpSshClients) } } catch (proxyError) { curRes.status = execStatusEnum.connectFail curRes.result += `代理连接失败: ${ proxyError.message }` socket.emit('output', execResult) resolve(curRes) return } logger.info('准备连接终端执行一次性指令:', host) logger.info('连接信息', { username: targetConnectionOptions.username, port: targetConnectionOptions.port, authType: hostInfo.authType }) let sshClient = new SSHClient() execClient.push(sshClient) sshClient .on('ready', () => { logger.info('连接终端成功:', host) execShell(socket, sshClient, curRes, resolve) }) .on('error', (err) => { console.log(err) logger.error('onekey终端连接失败:', err.level) curRes.status = execStatusEnum.connectFail curRes.result += err.message socket.emit('output', execResult) // 清理跳板机连接 jumpSshClients?.forEach(sshClient => sshClient && sshClient.end()) resolve(curRes) }) .on('keyboard-interactive', function (name, instructions, instructionsLang, prompts, finish) { finish([targetConnectionOptions[hostInfo.authType]]) }) .connect({ tryKeyboard: true, ...targetConnectionOptions }) } catch (err) { logger.error('创建终端错误:', err.message) curRes.status = execStatusEnum.connectFail curRes.result += err.message socket.emit('output', execResult) // 清理跳板机连接 jumpSshClients?.forEach(sshClient => sshClient && sshClient.end()) resolve(curRes) } }) }) try { await Promise.all(execPromise) logger.info('onekey执行完成') socket.emit('exec_complete') sendNoticeAsync('onekey_complete', '批量指令执行完成', '请登录面板查看执行结果') } catch (error) { logger.error('onekey执行超时:', error) const { connecting, executing } = execStatusEnum execResult.forEach(item => { // 连接中和执行中的状态变更为超时状态 if ([connecting, executing].includes(item.status)) { item.status = execStatusEnum.execTimeout } }) const reason = `执行超时,已强制终止执行 - 超时时间${ timeout }秒` sendNoticeAsync('onekey_complete', '批量指令执行超时', reason) socket.emit('exec_timeout', { reason, result: execResult }) } finally { socket.disconnect() } }) socket.on('disconnect', async (reason) => { logger.info('onekey终端连接断开:', reason) disconnectAllExecClient() const { execSuccess, connectFail, execFail, execTimeout } = execStatusEnum execResult.forEach(item => { // 非服务端手动断开连接且命令执行状态为非完成\失败\超时, 判定为客户端主动中断 if (reason !== 'server namespace disconnect' && ![execSuccess, execFail, execTimeout, connectFail].includes(item.status)) { item.status = execStatusEnum.socketInterrupt } item.endDate = Date.now() }) await onekeyDB.insertAsync(execResult) isExecuting = false execResult = [] execClient = [] jumpSshClientsPool = [] }) }) }