Files
easynode/server/app/socket/docker.js
T

204 lines
6.9 KiB
JavaScript

import path, { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import ssh2Module from 'ssh2'
const { Client: SSHClient } = ssh2Module
import { createTerminal } from './terminal.js'
import decryptAndExecuteAsync from '../utils/decrypt-file.js'
import { createSecureWs } from '../utils/ws-tool.js'
const currentDir = dirname(fileURLToPath(import.meta.url))
let executeCommand = () => {
return new Promise((resolve) => {
resolve('')
})
}
// 专门用于获取Docker日志的函数,Docker logs 输出通常在 stderr 中
function executeDockerLogsCommand(targetSSHClient, command) {
return new Promise((resolve, reject) => {
targetSSHClient.exec(command, (err, stream) => {
if (err) {
logger.error('执行Docker logs命令失败:', err)
return reject(err)
}
let stdoutData = ''
let stderrData = ''
stream.on('close', () => { // code
// Docker logs 的输出主要在 stderr,合并所有输出
const allData = stdoutData + stderrData
// logger.info(`Docker logs 命令完成, 退出码: ${ code }, 输出长度: ${ allData.length }`)
if (allData.trim()) {
resolve(allData)
} else {
logger.warn('Docker logs 无输出:', command)
resolve('')
}
})
stream.on('data', (data) => {
stdoutData += data.toString('utf8')
})
stream.stderr.on('data', (data) => {
stderrData += data.toString('utf8')
})
stream.on('error', (err) => {
logger.error('Docker logs stream 错误:', err)
reject(err)
})
})
})
}
const VALID_CONTAINER_ID = /^[a-zA-Z0-9][a-zA-Z0-9_.\-]{0,127}$/
function validateContainerId(containerId) {
if (typeof containerId !== 'string' || !VALID_CONTAINER_ID.test(containerId)) {
throw new Error('invalid container id')
}
}
function sanitizeTail(tail) {
return Math.min(Math.max(parseInt(tail, 10) || 3000, 1), 100000)
}
async function getDockerLogs(targetSSHClient, containerId, tail = 3000) {
try {
validateContainerId(containerId)
tail = sanitizeTail(tail)
// 使用专门的日志获取函数,确保能获取到所有日志
const logsData = await executeDockerLogsCommand(
targetSSHClient,
`docker logs --tail ${ tail } -t ${ containerId }`
)
// 如果日志为空,返回提示信息
if (!logsData || logsData.trim() === '') {
return '该容器暂无日志输出'
}
return logsData
} catch (error) {
console.error('获取Docker日志失败:', error)
return `获取Docker日志失败: ${ error.message || '未知错误' }`
}
}
async function startDockerContainer(targetSSHClient, containerId) {
try {
validateContainerId(containerId)
await executeCommand(targetSSHClient, `docker start ${ containerId }`)
return { success: true, message: '容器启动成功' }
} catch (error) {
console.error('启动Docker容器失败:', error)
return { success: false, message: error.message || '启动容器失败' }
}
}
async function stopDockerContainer(targetSSHClient, containerId) {
try {
validateContainerId(containerId)
await executeCommand(targetSSHClient, `docker stop ${ containerId }`)
return { success: true, message: '容器停止成功' }
} catch (error) {
console.error('停止Docker容器失败:', error)
return { success: false, message: error.message || '停止容器失败' }
}
}
async function restartDockerContainer(targetSSHClient, containerId) {
try {
validateContainerId(containerId)
await executeCommand(targetSSHClient, `docker restart ${ containerId }`)
return { success: true, message: '容器重启成功' }
} catch (error) {
console.error('重启Docker容器失败:', error)
return { success: false, message: error.message || '重启容器失败' }
}
}
async function deleteDockerContainer(targetSSHClient, containerId) {
try {
validateContainerId(containerId)
await executeCommand(targetSSHClient, `docker rm -f ${ containerId }`)
return { success: true, message: '容器删除成功' }
} catch (error) {
console.error('删除Docker容器失败:', error)
return { success: false, message: error.message || '删除容器失败' }
}
}
export default (httpServer) => {
const serverIo = createSecureWs(httpServer, '/docker')
let connectionCount = 0
serverIo.on('connection', async (socket) => {
connectionCount++
logger.info(`docker websocket 已连接 - 当前连接数: ${ connectionCount }`)
let targetSSHClient = null
let jumpSshClients = []
socket.on('ws_docker', async ({ hostId }) => {
targetSSHClient = new SSHClient()
let { jumpSshClients: dockerJumpSshClients } = await createTerminal(hostId, socket, targetSSHClient, false)
jumpSshClients.push(...dockerJumpSshClients)
let { getDockerContainers = null, executeCommand: dockerExecuteCommand } = (await decryptAndExecuteAsync(path.join(currentDir, 'plus.js'))) || {}
executeCommand = dockerExecuteCommand
if (!getDockerContainers) {
socket.emit('docker_not_plus')
socket.disconnect()
return
}
let containersData = await getDockerContainers(targetSSHClient)
if (!containersData) {
socket.emit('docker_connect_fail')
socket.disconnect()
return
}
socket.emit('docker_containers_data', containersData)
socket.on('docker_get_containers_data', async () => {
socket.emit('docker_containers_data', await getDockerContainers(targetSSHClient))
})
socket.on('docker_get_containers_logs', async ({ containerId, tail = 3000 }) => {
socket.emit('docker_containers_logs', await getDockerLogs(targetSSHClient, containerId, tail))
})
socket.on('docker_start_container', async ({ containerId }) => {
const result = await startDockerContainer(targetSSHClient, containerId)
socket.emit('docker_operation_result', result)
})
socket.on('docker_stop_container', async ({ containerId }) => {
const result = await stopDockerContainer(targetSSHClient, containerId)
socket.emit('docker_operation_result', result)
})
socket.on('docker_restart_container', async ({ containerId }) => {
const result = await restartDockerContainer(targetSSHClient, containerId)
socket.emit('docker_operation_result', result)
})
socket.on('docker_delete_container', async ({ containerId }) => {
const result = await deleteDockerContainer(targetSSHClient, containerId)
socket.emit('docker_operation_result', result)
})
})
socket.on('disconnect', (reason) => {
connectionCount--
targetSSHClient && targetSSHClient.end()
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
targetSSHClient = null
jumpSshClients = null
logger.info(`docker websocket 连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
})
})
}