From a2c007619180598f95c8030435539e16c7e5eb74 Mon Sep 17 00:00:00 2001 From: fly6516 Date: Fri, 21 Feb 2025 16:35:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E4=BA=86mysql=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=B5=8B=E8=AF=95=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 2 +- src/utils/dockerUtils.ts | 178 ++++++++++++++++++++++++++++++++------- 2 files changed, 148 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7806071..b623f4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1906,7 +1906,7 @@ }, "node_modules/bun": { "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/bun/-/bun-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bun/-/bun-1.2.2.tgz", "integrity": "sha512-RUc8uVVTw8WoASUzXaEQJR1s7mnwoHm3P871qBUIqSaoOpuwcU+bSVX151/xoqDwnyv38SjOX7yQ3oO0IeT73g==", "cpu": [ "arm64", diff --git a/src/utils/dockerUtils.ts b/src/utils/dockerUtils.ts index 508bfd7..b0bc972 100644 --- a/src/utils/dockerUtils.ts +++ b/src/utils/dockerUtils.ts @@ -3,65 +3,181 @@ import Docker from 'dockerode'; const docker = new Docker(); +const CONTAINER_TIMEOUT = 60; // 容器超时时间(秒) -export default async function serverAction(sql: string, databaseType: string): Promise { +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 = `sqlcmd -S localhost -U SA -P YourStrong!Passw0rd -Q "${sql}"`; - env = ['ACCEPT_EULA=Y', 'SA_PASSWORD=YourStrong!Passw0rd']; + command = `sqlcmd -S localhost -U SA -P ${process.env.SQL_SERVER_SA_PASSWORD} -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 -pYourStrong!Passw0rd -e "${sql}"`; - env = ['MYSQL_ROOT_PASSWORD=YourStrong!Passw0rd']; + 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'); } try { - // Start Docker container - const container = await docker.run(dockerImage, [], process.stdout, { - Tty: false, + // 创建并启动容器 + container = await docker.createContainer({ + Image: dockerImage, Env: env, HostConfig: { + AutoRemove: true, PortBindings: { - '3306/tcp': [{ HostPort: '3306' }], // MySQL port binding - '1433/tcp': [{ HostPort: '1433' }], // SQL Server port binding - }, - }, + [databaseType === 'sqlserver' ? '1433/tcp' : '3306/tcp']: [ + { HostPort: databaseType === 'sqlserver' ? '1433' : '3306' } + ] + } + } }); - // Wait for the database to start (this is a simple sleep, you might want a more robust solution) - await new Promise(resolve => setTimeout(resolve, 10000)); + // 对于 SQL Server,安装 mssql-tools + if (databaseType === 'sqlserver') { + 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(); + }); + }); + }); - // Execute SQL command + 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(); + }); + }); + }); + } else { + await container.start(); + } + + // 改进的健康检查逻辑 + let isReady = false; + const startTime = Date.now(); + while (Date.now() - startTime < CONTAINER_TIMEOUT * 1000) { + try { + const healthCheckCmd = databaseType === 'sqlserver' + ? ['/opt/mssql-tools/bin/sqlcmd', '-S', 'localhost', '-U', 'SA', '-P', process.env.SQL_SERVER_SA_PASSWORD, '-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 (!isReady) throw new Error(`Database not ready within ${CONTAINER_TIMEOUT} seconds`); + + // 执行SQL命令 const exec = await container.exec({ - Cmd: ['/bin/sh', '-c', command], + Cmd: [ + databaseType === 'sqlserver' ? '/bin/bash' : '/bin/sh', + '-c', + command + ], AttachStdout: true, - AttachStderr: true, + AttachStderr: true }); - const stream = await exec.start({}); + // 捕获执行结果 + return await new Promise((resolve, reject) => { + let output = ''; + exec.start({}, (err, stream) => { + if (err) return reject(err); - let output = ''; - stream.on('data', (chunk: { toString: () => string; }) => { - output += chunk.toString(); + stream?.on('data', (chunk: Buffer) => { + output += chunk.toString(); + }); + + stream?.on('end', () => { + // 过滤MySQL警告信息 + if (databaseType === 'mysql') { + output = output.replace(/Warning: Using a password on the command line interface can be insecure.\n/g, ''); + } + resolve(output); + }); + + stream?.resume(); + }); }); - - await new Promise((resolve) => stream.on('end', resolve)); - - // Stop and remove Docker container - await container.stop(); - await container.remove(); - - return output; } catch (error) { - throw error; + // 异常清理 + if (container) { + await container.stop().catch(() => {}); + await container.remove().catch(() => {}); + } + throw typeof error === 'string' + ? new Error(error) + : error instanceof Error + ? error + : new Error('Unknown error'); } -} \ No newline at end of file +}