From f6e0a21d4e16bc6f4af2411734abf2e52c1cdcfc Mon Sep 17 00:00:00 2001 From: fly6516 Date: Mon, 24 Feb 2025 20:00:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E5=A4=84=E7=90=86=E5=92=8C=E6=8F=90=E5=89=8D=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=AE=B9=E5=99=A8=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/dockerUtils.ts | 442 +++++++++++++++++++++++---------------- 1 file changed, 260 insertions(+), 182 deletions(-) diff --git a/src/utils/dockerUtils.ts b/src/utils/dockerUtils.ts index f00901c..15f4bd2 100644 --- a/src/utils/dockerUtils.ts +++ b/src/utils/dockerUtils.ts @@ -3,231 +3,309 @@ import Docker from 'dockerode'; const docker = new Docker(); -const CONTAINER_TIMEOUT = 60; // 容器超时时间(秒) +const CONTAINER_TIMEOUT = 60; // 容器操作超时时间(秒) +const PREWARM_COUNT = 2; // 预热容器数量配置 +const SQL_SERVER_STARTUP_DELAY = 15000; // SQL Server启动等待时间(毫秒) -// 安全容器操作函数 -async function safeContainerStop(container: Docker.Container) { +// 端口管理配置 +const PORT_CONFIG = { + min: 5000, + max: 6000, + current: 5000 +}; + +// 容器池数据结构 +type ContainerPool = { + [key: string]: { + free: Docker.ContainerInfo[]; + used: Map; + } +}; + +const containerPool: ContainerPool = { + sqlserver: { free: [], used: new Map() }, + mysql: { free: [], used: new Map() } +}; + +// 初始化预热容器 +(async () => { + await Promise.all([ + ...Array(PREWARM_COUNT).fill(null).map(() => createContainer('sqlserver')), + ...Array(PREWARM_COUNT).fill(null).map(() => createContainer('mysql')) + ]); + console.log('预热容器初始化完成'); +})(); + +// 安全容器操作方法 +async function safeStopContainer(container: Docker.Container) { try { await container.stop(); } catch (error: any) { - if (![409, 404].includes(error.statusCode)) { + if (![404, 409].includes(error.statusCode)) { + console.error('停止容器失败:', error); throw error; } } } -async function safeContainerRemove(container: Docker.Container) { +async function safeRemoveContainer(container: Docker.Container) { try { await container.remove(); } catch (error: any) { if (error.statusCode !== 404) { + console.error('删除容器失败:', error); throw error; } } } -export default async function serverAction( - sql: string, - databaseType: string -): Promise { - let dockerImage: string; - let command: string; - let env: string[] = []; - let container: Docker.Container; - - // 配置数据库参数 - switch (databaseType) { - case 'sqlserver': - process.env.SQL_SERVER_SA_PASSWORD = 'YourStrong!Passw0rd'; - dockerImage = 'mcr.microsoft.com/mssql/server:2019-latest'; - command = `/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P ${process.env.SQL_SERVER_SA_PASSWORD} -C -Q "${sql}"`; - env = ['ACCEPT_EULA=Y', `SA_PASSWORD=${process.env.SQL_SERVER_SA_PASSWORD}`]; - break; - case 'mysql': - process.env.MYSQL_ROOT_PASSWORD = 'YourStrong!Passw0rd'; - dockerImage = 'mysql:latest'; - command = `mysql -u root -p${process.env.MYSQL_ROOT_PASSWORD} -e "${sql}"`; - env = [ - `MYSQL_ROOT_PASSWORD=${process.env.MYSQL_ROOT_PASSWORD}`, - 'MYSQL_TCP_PORT=3306', - 'MYSQL_ROOT_HOST=%', - ]; - break; - default: - throw new Error('Unsupported database type'); - } +// 容器创建逻辑 +async function createContainer(dbType: string) { + const port = getAvailablePort(); + const config = getDatabaseConfig(dbType); try { - // 创建并启动容器 - container = await docker.createContainer({ - Image: dockerImage, - Env: databaseType === 'sqlserver' - ? [...env, 'MSSQL_AGENT_ENABLED=True', 'MSSQL_TCP_PROTOCOL=1'] - : env, + const container = await docker.createContainer({ + Image: config.image, + Env: config.env, HostConfig: { AutoRemove: true, PortBindings: { - [databaseType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [ - { HostPort: databaseType === 'sqlserver' ? '1433' : '3306' } + [dbType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [ + { HostPort: port.toString() } ] }, - ...(databaseType === 'sqlserver' && { - Memory: 2147483648 + ...(dbType === 'sqlserver' && { + Memory: 2147483648, + NetworkMode: 'bridge' }) } }); - if (databaseType === 'sqlserver') { - await container.start(); + await container.start(); - // 安装依赖工具链 - await new Promise((resolve, reject) => { - const exec = container.exec({ - Cmd: [ - '/bin/bash', - '-c', - 'echo "YourStrong!Passw0rd" | su -c "apt-get update && apt-get install -y curl && curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list"' - ], - AttachStdout: true, - AttachStderr: true - }, (err, exec) => { - if (err) return reject(err); - exec?.start({}, (err, stream) => { - if (err) return reject(err); - stream?.on('end', () => resolve(null)); - stream?.resume(); - }); - }); - }); - - // 安装核心组件 - await new Promise((resolve, reject) => { - const exec = container.exec({ - Cmd: [ - '/bin/bash', - '-c', - 'ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools && echo "export PATH=$PATH:/opt/mssql-tools/bin" >> ~/.bashrc && source ~/.bashrc' - ], - AttachStdout: true, - AttachStderr: true - }, (err, exec) => { - if (err) return reject(err); - exec?.start({}, (err, stream) => { - if (err) return reject(err); - stream?.on('end', () => resolve(null)); - stream?.resume(); - }); - }); - }); - - await new Promise(resolve => setTimeout(resolve, 5000)); - } else { - await container.start(); + // SQL Server需要额外启动等待时间 + if (dbType === 'sqlserver') { + await new Promise(resolve => setTimeout(resolve, SQL_SERVER_STARTUP_DELAY)); } - // 健康检查逻辑 - let isReady = false; - const startTime = Date.now(); - while (Date.now() - startTime < CONTAINER_TIMEOUT * 1000) { - try { - const healthCheckCmd = databaseType === 'sqlserver' - ? [ - '/opt/mssql-tools18/bin/sqlcmd', - '-S', 'localhost', - '-U', 'SA', - '-P', process.env.SQL_SERVER_SA_PASSWORD, - '-C', - '-Q', 'SELECT 1' - ] - : [ - 'sh', - '-c', - `mysql -h 127.0.0.1 -u root -p${process.env.MYSQL_ROOT_PASSWORD} -e "SELECT 1" 2>&1 | grep -v "Using a password"` - ]; - - const exec = await container.exec({ - Cmd: healthCheckCmd, - AttachStdout: true, - AttachStderr: true - }); - - const output = await new Promise((resolve, reject) => { - exec.start({}, (err, stream) => { - if (err) return reject(err); - let data = ''; - stream?.on('data', (chunk: Buffer) => data += chunk.toString()); - stream?.on('end', () => resolve(data)); - stream?.resume(); - }); - }); - - if (output.includes("ERROR")) throw new Error(output); - isReady = true; - break; - } catch { - await new Promise(resolve => setTimeout(resolve, 2000)); - } + // 执行健康检查 + if (await healthCheck(container, dbType)) { + const info = await container.inspect(); + containerPool[dbType].free.push({ + ...info, + Port: port + }); + console.log(`已创建 ${dbType} 容器:${info.Id}`); } + } catch (error) { + console.error(`创建 ${dbType} 容器失败:`, error); + throw error; + } +} - if (!isReady) throw new Error(`Database not ready within ${CONTAINER_TIMEOUT} seconds`); +// 健康检查逻辑(增强版) +async function healthCheck(container: Docker.Container, dbType: string) { + const config = getDatabaseConfig(dbType); + const containerInfo = await container.inspect(); + const containerIp = containerInfo.NetworkSettings.Networks.bridge?.IPAddress; - // 执行SQL命令 - const execResult = await new Promise((resolve, reject) => { - container.exec({ - Cmd: [ - databaseType === 'sqlserver' ? '/bin/bash' : '/bin/sh', - '-c', - command - ], - AttachStdout: true, - AttachStderr: true - }, (err, exec) => { - if (err) return reject(err); + if (!containerIp) { + console.error('无法获取容器IP地址'); + await safeRemoveContainer(container); + return false; + } - exec?.start({}, (err, stream) => { + const testCommand = dbType === 'sqlserver' + ? `/opt/mssql-tools18/bin/sqlcmd -S ${containerIp},1433 -U SA -P ${config.password} -Q "SELECT 1" -C -N -l 30` + : `mysql -h ${containerIp} -u root -p${config.password} -e "SELECT 1"`; + + try { + const exec = await container.exec({ + Cmd: ['sh', '-c', testCommand], + AttachStdout: true, + AttachStderr: true + }); + + // 设置30秒超时 + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('健康检查超时')), 30000) + ); + + await Promise.race([ + new Promise((resolve, reject) => { + exec.start((err, stream) => { if (err) return reject(err); - let output = ''; - - stream?.on('data', (chunk: Buffer) => { - output += chunk.toString(); - }); - - stream?.on('end', () => { - if (databaseType === 'mysql') { - output = output.replace(/Warning: Using a password on the command line interface can be insecure.\n/g, ''); - } - resolve(output); - }); - + stream?.on('end', resolve); + stream?.on('error', reject); stream?.resume(); }); + }), + timeoutPromise + ]); + + return true; + } catch (error) { + console.error(`健康检查失败: ${error}`); + await safeRemoveContainer(container); + return false; + } +} + +// 数据库配置(增加SSL参数) +function getDatabaseConfig(dbType: string) { + const password = 'YourStrong!Passw0rd'; + return { + password, + image: dbType === 'sqlserver' + ? 'mcr.microsoft.com/mssql/server:2019-latest' + : 'mysql:latest', + env: dbType === 'sqlserver' ? [ + 'ACCEPT_EULA=Y', + `SA_PASSWORD=${password}`, + 'MSSQL_AGENT_ENABLED=True', + 'MSSQL_TCP_PORT=1433', + 'MSSQL_LCID=1033', + 'MSSQL_MEMORY_LIMIT_MB=2048', + 'MSSQL_OPTION_FLAGS=-C' // 启用信任服务器证书 + ] : [ + `MYSQL_ROOT_PASSWORD=${password}`, + 'MYSQL_TCP_PORT=3306', + 'MYSQL_ROOT_HOST=%' + ] + }; +} + +// 端口分配 +function getAvailablePort() { + PORT_CONFIG.current = + PORT_CONFIG.current >= PORT_CONFIG.max + ? PORT_CONFIG.min + : PORT_CONFIG.current + 1; + return PORT_CONFIG.current; +} + +// 主服务函数(关键修复点) +export default async function serverAction( + sql: string, + dbType: string +): Promise { + let containerInfo: Docker.ContainerInfo | undefined; + + try { + // 获取或创建容器 + if (containerPool[dbType].free.length === 0) { + await createContainer(dbType); + } + + containerInfo = containerPool[dbType].free.pop()!; + containerPool[dbType].used.set(containerInfo.Id, containerInfo); + + const container = docker.getContainer(containerInfo.Id); + const containerIp = containerInfo.NetworkSettings.Networks.bridge?.IPAddress; + + if (!containerIp) { + throw new Error('无法获取容器IP地址'); + } + + // 构建执行命令(增加SSL参数) + const command = dbType === 'sqlserver' + ? `/opt/mssql-tools18/bin/sqlcmd -S ${containerIp},1433 -U SA -P ${getDatabaseConfig(dbType).password} -Q "${sql}" -C -N -l 30` + : `mysql -h ${containerIp} -u root -p${getDatabaseConfig(dbType).password} -e "${sql}"`; + + // 执行SQL命令 + const exec = await container.exec({ + Cmd: ['sh', '-c', command], + AttachStdout: true, + AttachStderr: true + }); + + // 增加执行状态跟踪 + let isCompleted = false; + const cleanup = async () => { + if (!isCompleted && containerInfo) { + console.log('执行未完成,执行清理...'); + await safeRemoveContainer(container); + containerPool[dbType].used.delete(containerInfo.Id); + await createContainer(dbType); + } + }; + + const result = await new Promise((resolve, reject) => { + let output = ''; + exec.start({}, async (err, stream) => { + if (err) { + await cleanup(); + return reject(err); + } + + stream?.on('data', (chunk: Buffer) => { + output += chunk.toString(); + }); + + stream?.on('end', async () => { + try { + if (dbType === 'mysql') { + output = output.replace(/Warning: Using a password on the command line interface can be insecure.\n/g, ''); + } + isCompleted = true; + + // 安全归还容器 + await safeStopContainer(container); + containerPool[dbType].free.push(containerInfo!); + containerPool[dbType].used.delete(containerInfo!.Id); + + resolve(output); + } catch (cleanupError) { + await cleanup(); + reject(cleanupError); + } + }); + + stream?.on('error', async (error) => { + await cleanup(); + reject(error); + }); + + stream?.resume(); }); }); - // 安全停止容器 - await safeContainerStop(container); - - // 兜底状态检查 - setTimeout(async () => { - try { - const info = await container.inspect(); - if (info.State.Running) { - await safeContainerStop(container); - } - } catch (error: any) { - if (error.statusCode !== 404) { - console.error('Post-execution cleanup check failed:', error.message); - } - } - }, 2000); - - return execResult; + return result; } catch (error) { - // 错误处理流程 - if (container) { - await safeContainerStop(container); - await safeContainerRemove(container); + if (containerInfo) { + await safeRemoveContainer(docker.getContainer(containerInfo.Id)); + containerPool[dbType].used.delete(containerInfo.Id); + await createContainer(dbType); } - throw error instanceof Error ? error : new Error('Unknown error'); + console.error('执行错误:', error); + throw error instanceof Error ? error : new Error('未知错误'); } } + +// 后台维护任务 +setInterval(async () => { + try { + for (const dbType of ['sqlserver', 'mysql']) { + // 维持预热容器数量 + while (containerPool[dbType].free.length < PREWARM_COUNT) { + await createContainer(dbType); + } + + // 清理无效容器 + containerPool[dbType].free = (await Promise.all( + containerPool[dbType].free.map(async (info) => { + try { + const container = docker.getContainer(info.Id); + await container.inspect(); + return info; + } catch { + return null; + } + }) + )).filter(Boolean) as Docker.ContainerInfo[]; + } + } catch (error) { + console.error('维护任务出错:', error); + } +}, 30000); // 每30秒执行一次维护