@icyfenix-dmla/cli 2026.4.19-954 → 2026.4.21-2145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/manage.js +6 -70
- package/src/commands/server.js +196 -37
- package/src/index.js +78 -21
- package/src/server/index.js +72 -5
- package/src/server/routes/sandbox.js +61 -8
- package/src/server/sandbox.js +324 -27
package/package.json
CHANGED
package/src/commands/manage.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function installImages(types, registry = 'dockerhub') {
|
|
|
66
66
|
|
|
67
67
|
console.log()
|
|
68
68
|
console.log(chalk.green('🎉 镜像安装完成'))
|
|
69
|
-
console.log(chalk.yellow('
|
|
69
|
+
console.log(chalk.yellow('提示: 运行 dmla start 启动服务'))
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -105,70 +105,6 @@ async function pullImageWithProgress(imageName) {
|
|
|
105
105
|
})
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
/**
|
|
109
|
-
* 更新所有组件
|
|
110
|
-
*/
|
|
111
|
-
export async function updateAll(registry = 'dockerhub') {
|
|
112
|
-
console.log()
|
|
113
|
-
|
|
114
|
-
// 更新 npm 包
|
|
115
|
-
console.log(chalk.bold('📦 更新 npm 包'))
|
|
116
|
-
try {
|
|
117
|
-
console.log(chalk.gray(' 执行 npm update -g @icyfenix-dmla/cli...'))
|
|
118
|
-
execSync('npm update -g @icyfenix-dmla/cli', { stdio: 'inherit' })
|
|
119
|
-
console.log(chalk.green('✅ npm 包已更新'))
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.log(chalk.yellow('⚠️ npm 包更新失败或已是最新版本'))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log()
|
|
125
|
-
|
|
126
|
-
// 检查并更新镜像
|
|
127
|
-
console.log(chalk.bold('🖼️ 检查 Docker 镜像更新'))
|
|
128
|
-
const registryUrl = getRegistryUrl(registry)
|
|
129
|
-
|
|
130
|
-
for (const type of ['cpu', 'gpu']) {
|
|
131
|
-
const remoteImage = `${registryUrl}:${type}`
|
|
132
|
-
const localImage = type === 'gpu' ? CONFIG.imageGpu : CONFIG.imageCpu
|
|
133
|
-
|
|
134
|
-
console.log(chalk.gray(` 检查 ${type.toUpperCase()} 版本...`))
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
// 检查本地镜像是否存在
|
|
138
|
-
let localImageInfo = null
|
|
139
|
-
try {
|
|
140
|
-
localImageInfo = await docker.getImage(localImage).inspect()
|
|
141
|
-
} catch {
|
|
142
|
-
// 本地镜像不存在,需要拉取
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 拉取最新镜像
|
|
146
|
-
console.log(chalk.gray(` 拉取最新 ${type.toUpperCase()} 镜像...`))
|
|
147
|
-
await pullImageWithProgress(remoteImage)
|
|
148
|
-
|
|
149
|
-
// 获取拉取的镜像信息
|
|
150
|
-
const remoteImageInfo = await docker.getImage(remoteImage).inspect()
|
|
151
|
-
|
|
152
|
-
// 比较镜像 ID
|
|
153
|
-
if (localImageInfo && localImageInfo.Id === remoteImageInfo.Id) {
|
|
154
|
-
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像已是最新版本`))
|
|
155
|
-
} else {
|
|
156
|
-
// Tag 为本地名称
|
|
157
|
-
console.log(chalk.gray(` 重命名为 ${localImage}...`))
|
|
158
|
-
const image = docker.getImage(remoteImage)
|
|
159
|
-
await image.tag({ repo: CONFIG.imageName, tag: type })
|
|
160
|
-
|
|
161
|
-
console.log(chalk.green(`✅ ${type.toUpperCase()} 镜像已更新`))
|
|
162
|
-
}
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.log(chalk.yellow(`⚠️ ${type.toUpperCase()} 镜像更新失败: ${error.message}`))
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log()
|
|
169
|
-
console.log(chalk.green('🎉 更新完成'))
|
|
170
|
-
}
|
|
171
|
-
|
|
172
108
|
/**
|
|
173
109
|
* 环境诊断
|
|
174
110
|
*/
|
|
@@ -201,7 +137,7 @@ export async function runDoctor() {
|
|
|
201
137
|
// ───────────────────────────────────────────────────────────
|
|
202
138
|
// 镜像检查
|
|
203
139
|
// ───────────────────────────────────────────────────────────
|
|
204
|
-
console.log(chalk.bold('
|
|
140
|
+
console.log(chalk.bold('Docker 镜像'))
|
|
205
141
|
|
|
206
142
|
const cpuImage = CONFIG.imageCpu
|
|
207
143
|
const gpuImage = CONFIG.imageGpu
|
|
@@ -234,7 +170,7 @@ export async function runDoctor() {
|
|
|
234
170
|
// ───────────────────────────────────────────────────────────
|
|
235
171
|
// GPU 检查
|
|
236
172
|
// ───────────────────────────────────────────────────────────
|
|
237
|
-
console.log(chalk.bold('
|
|
173
|
+
console.log(chalk.bold('GPU 驱动'))
|
|
238
174
|
|
|
239
175
|
try {
|
|
240
176
|
const output = execSync('nvidia-smi -L', { timeout: 5000, encoding: 'utf8' })
|
|
@@ -261,7 +197,7 @@ export async function runDoctor() {
|
|
|
261
197
|
// ───────────────────────────────────────────────────────────
|
|
262
198
|
// 端口检查
|
|
263
199
|
// ───────────────────────────────────────────────────────────
|
|
264
|
-
console.log(chalk.bold('
|
|
200
|
+
console.log(chalk.bold('端口可用性'))
|
|
265
201
|
|
|
266
202
|
const port = CONFIG.defaultPort
|
|
267
203
|
const portAvailable = await checkPortAvailable(port)
|
|
@@ -310,11 +246,11 @@ export async function runDoctor() {
|
|
|
310
246
|
console.log(chalk.red(` ${i + 1}. ${issue}`))
|
|
311
247
|
})
|
|
312
248
|
console.log()
|
|
313
|
-
console.log(chalk.yellow('
|
|
249
|
+
console.log(chalk.yellow('请根据上述提示解决问题后再次运行 dmla doctor'))
|
|
314
250
|
} else {
|
|
315
251
|
console.log(chalk.bold.green('✅ 所有检查通过,环境正常'))
|
|
316
252
|
console.log()
|
|
317
|
-
console.log(chalk.gray('
|
|
253
|
+
console.log(chalk.gray('运行 dmla start 启动服务'))
|
|
318
254
|
}
|
|
319
255
|
}
|
|
320
256
|
|
package/src/commands/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import Docker from 'dockerode'
|
|
|
6
6
|
import { spawn } from 'child_process'
|
|
7
7
|
import http from 'http'
|
|
8
8
|
import path from 'path'
|
|
9
|
-
import { fileURLToPath } from 'url'
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
10
10
|
import fs from 'fs'
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url)
|
|
@@ -49,6 +49,41 @@ async function checkImageExists(type) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 检查可用镜像并自动选择
|
|
54
|
+
* @returns {Object} { imageType: 'cpu'|'gpu', message: string }
|
|
55
|
+
*/
|
|
56
|
+
async function resolveImageType(useGpu) {
|
|
57
|
+
const cpuExists = await checkImageExists('cpu')
|
|
58
|
+
const gpuExists = await checkImageExists('gpu')
|
|
59
|
+
|
|
60
|
+
// 用户明确指定了 GPU
|
|
61
|
+
if (useGpu) {
|
|
62
|
+
if (gpuExists) {
|
|
63
|
+
return { imageType: 'gpu', message: 'GPU' }
|
|
64
|
+
}
|
|
65
|
+
// 用户想要 GPU 但 GPU 镜像不存在
|
|
66
|
+
if (cpuExists) {
|
|
67
|
+
console.log(chalk.yellow('⚠️ GPU 镜像不存在,将使用 CPU 镜像'))
|
|
68
|
+
return { imageType: 'cpu', message: 'CPU (降级)' }
|
|
69
|
+
}
|
|
70
|
+
return { imageType: null, message: '无可用镜像' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 用户未指定,自动选择
|
|
74
|
+
if (cpuExists) {
|
|
75
|
+
return { imageType: 'cpu', message: 'CPU' }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// CPU 镜像不存在
|
|
79
|
+
if (gpuExists) {
|
|
80
|
+
console.log(chalk.yellow('⚠️ CPU 镜像不存在,自动使用 GPU 镜像'))
|
|
81
|
+
return { imageType: 'gpu', message: 'GPU (自动)' }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { imageType: null, message: '无可用镜像' }
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
/**
|
|
53
88
|
* 检查 GPU 是否可用
|
|
54
89
|
*/
|
|
@@ -117,35 +152,64 @@ async function findServiceContainer() {
|
|
|
117
152
|
* 查找服务器入口文件
|
|
118
153
|
*/
|
|
119
154
|
function findServerPath() {
|
|
155
|
+
// 开发环境路径:packages/cli/src/commands -> ../../../local-server/src/index.js
|
|
120
156
|
const serverPath = path.resolve(__dirname, '../../../local-server/src/index.js')
|
|
157
|
+
// npm 包路径:packages/cli/src/commands -> ../server/index.js
|
|
121
158
|
const standaloneServerPath = path.resolve(__dirname, '../server/index.js')
|
|
122
159
|
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
// 检查 __dirname 是否正确(调试用)
|
|
161
|
+
const cliPackageRoot = path.resolve(__dirname, '../..')
|
|
162
|
+
const expectedServerDir = path.resolve(cliPackageRoot, 'src/server')
|
|
163
|
+
|
|
164
|
+
if (fs.existsSync(serverPath)) {
|
|
165
|
+
return serverPath
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (fs.existsSync(standaloneServerPath)) {
|
|
169
|
+
return standaloneServerPath
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 调试输出:显示路径信息帮助诊断
|
|
173
|
+
console.log(chalk.yellow('⚠️ 服务入口文件查找失败'))
|
|
174
|
+
console.log(chalk.gray(` __dirname: ${__dirname}`))
|
|
175
|
+
console.log(chalk.gray(` 开发路径: ${serverPath} (${fs.existsSync(serverPath) ? '存在' : '不存在'})`))
|
|
176
|
+
console.log(chalk.gray(` npm路径: ${standaloneServerPath} (${fs.existsSync(standaloneServerPath) ? '存在' : '不存在'})`))
|
|
177
|
+
console.log(chalk.gray(` CLI包根目录: ${cliPackageRoot}`))
|
|
178
|
+
|
|
179
|
+
// 检查 CLI 包根目录下的文件结构
|
|
180
|
+
const srcDir = path.resolve(cliPackageRoot, 'src')
|
|
181
|
+
if (fs.existsSync(srcDir)) {
|
|
182
|
+
console.log(chalk.gray(` src目录内容: ${fs.readdirSync(srcDir).join(', ')}`))
|
|
183
|
+
if (fs.existsSync(expectedServerDir)) {
|
|
184
|
+
console.log(chalk.gray(` server目录内容: ${fs.readdirSync(expectedServerDir).join(', ')}`))
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
/**
|
|
128
192
|
* 同步启动服务(在当前进程运行,用于调试)
|
|
129
193
|
* @param {number} port - 服务端口
|
|
130
|
-
* @param {boolean} useGpu - 是否使用 GPU
|
|
194
|
+
* @param {boolean} useGpu - 是否使用 GPU(可选,自动检测)
|
|
131
195
|
*/
|
|
132
196
|
export async function startServerSync(port, useGpu = false) {
|
|
133
197
|
// 检查端口
|
|
134
198
|
const portAvailable = await checkPortAvailable(port)
|
|
135
199
|
if (!portAvailable) {
|
|
136
200
|
console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
|
|
137
|
-
console.log(chalk.yellow('
|
|
201
|
+
console.log(chalk.yellow('提示: 使用 --port 选项指定其他端口'))
|
|
138
202
|
return
|
|
139
203
|
}
|
|
140
204
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(chalk.
|
|
146
|
-
console.log(chalk.yellow('💡 提示: 运行 dmla install 安装镜像'))
|
|
205
|
+
// 智能选择镜像
|
|
206
|
+
const imageResolution = await resolveImageType(useGpu)
|
|
207
|
+
if (!imageResolution.imageType) {
|
|
208
|
+
console.log(chalk.red('❌ 无可用镜像'))
|
|
209
|
+
console.log(chalk.yellow('提示: 运行 dmla install 安装镜像'))
|
|
147
210
|
return
|
|
148
211
|
}
|
|
212
|
+
const resolvedUseGpu = imageResolution.imageType === 'gpu'
|
|
149
213
|
|
|
150
214
|
// 检查服务是否已运行
|
|
151
215
|
const alreadyRunning = await checkServiceRunning(port)
|
|
@@ -158,23 +222,26 @@ export async function startServerSync(port, useGpu = false) {
|
|
|
158
222
|
const actualServerPath = findServerPath()
|
|
159
223
|
if (!actualServerPath) {
|
|
160
224
|
console.log(chalk.red('❌ 找不到服务入口文件'))
|
|
161
|
-
console.log(chalk.yellow('
|
|
225
|
+
console.log(chalk.yellow('提示: 确保正确安装了 @icyfenix-dmla/cli'))
|
|
162
226
|
return
|
|
163
227
|
}
|
|
164
228
|
|
|
229
|
+
console.log(chalk.gray(` 镜像类型: ${imageResolution.message}`))
|
|
165
230
|
console.log(chalk.gray(' 同步模式启动...'))
|
|
166
231
|
console.log(chalk.gray(` 服务入口: ${actualServerPath}`))
|
|
167
232
|
console.log()
|
|
168
233
|
|
|
169
234
|
// 设置环境变量
|
|
170
235
|
process.env.PORT = port.toString()
|
|
171
|
-
process.env.USE_GPU =
|
|
236
|
+
process.env.USE_GPU = resolvedUseGpu ? 'true' : 'false'
|
|
172
237
|
process.env.DMLA_SYNC_MODE = 'true' // 标记同步模式,让服务器在 import 时启动
|
|
173
238
|
|
|
174
239
|
// 动态 import 服务器模块并直接运行
|
|
175
240
|
// 服务器模块会在 import 时自动启动(因为入口点检测逻辑)
|
|
241
|
+
// Windows 需要将路径转换为 file:// URL 格式
|
|
176
242
|
try {
|
|
177
|
-
|
|
243
|
+
const serverURL = pathToFileURL(actualServerPath).href
|
|
244
|
+
await import(serverURL)
|
|
178
245
|
} catch (error) {
|
|
179
246
|
console.log(chalk.red(`❌ 服务启动失败: ${error.message}`))
|
|
180
247
|
console.log(chalk.gray(error.stack))
|
|
@@ -189,18 +256,18 @@ export async function startServer(port, useGpu = false) {
|
|
|
189
256
|
const portAvailable = await checkPortAvailable(port)
|
|
190
257
|
if (!portAvailable) {
|
|
191
258
|
console.log(chalk.red(`❌ 端口 ${port} 已被占用`))
|
|
192
|
-
console.log(chalk.yellow('
|
|
259
|
+
console.log(chalk.yellow('提示: 使用 --port 选项指定其他端口'))
|
|
193
260
|
return
|
|
194
261
|
}
|
|
195
262
|
|
|
196
|
-
//
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.log(chalk.
|
|
201
|
-
console.log(chalk.yellow('💡 提示: 运行 dmla install 安装镜像'))
|
|
263
|
+
// 智能选择镜像
|
|
264
|
+
const imageResolution = await resolveImageType(useGpu)
|
|
265
|
+
if (!imageResolution.imageType) {
|
|
266
|
+
console.log(chalk.red('❌ 无可用镜像'))
|
|
267
|
+
console.log(chalk.yellow('提示: 运行 dmla install 安装镜像'))
|
|
202
268
|
return
|
|
203
269
|
}
|
|
270
|
+
const resolvedUseGpu = imageResolution.imageType === 'gpu'
|
|
204
271
|
|
|
205
272
|
// 检查服务是否已运行
|
|
206
273
|
const alreadyRunning = await checkServiceRunning(port)
|
|
@@ -210,6 +277,7 @@ export async function startServer(port, useGpu = false) {
|
|
|
210
277
|
}
|
|
211
278
|
|
|
212
279
|
// 启动服务
|
|
280
|
+
console.log(chalk.gray(` 镜像类型: ${imageResolution.message}`))
|
|
213
281
|
console.log(chalk.gray(' 正在启动...'))
|
|
214
282
|
|
|
215
283
|
try {
|
|
@@ -217,24 +285,65 @@ export async function startServer(port, useGpu = false) {
|
|
|
217
285
|
|
|
218
286
|
if (!actualServerPath) {
|
|
219
287
|
console.log(chalk.red('❌ 找不到服务入口文件'))
|
|
220
|
-
console.log(chalk.yellow('
|
|
288
|
+
console.log(chalk.yellow('提示: 确保正确安装了 @icyfenix-dmla/cli'))
|
|
221
289
|
return
|
|
222
290
|
}
|
|
223
291
|
|
|
292
|
+
// 日志文件路径
|
|
293
|
+
const logDir = path.resolve(__dirname, '../../logs')
|
|
294
|
+
if (!fs.existsSync(logDir)) {
|
|
295
|
+
fs.mkdirSync(logDir, { recursive: true })
|
|
296
|
+
}
|
|
297
|
+
const logFile = path.join(logDir, 'server.log')
|
|
298
|
+
const errorLogFile = path.join(logDir, 'server-error.log')
|
|
299
|
+
|
|
300
|
+
console.log(chalk.gray(` 日志文件: ${logFile}`))
|
|
301
|
+
|
|
302
|
+
// 创建日志文件流
|
|
303
|
+
const logStream = fs.openSync(logFile, 'a')
|
|
304
|
+
const errorLogStream = fs.openSync(errorLogFile, 'a')
|
|
305
|
+
|
|
224
306
|
const env = {
|
|
225
307
|
...process.env,
|
|
226
308
|
PORT: port.toString(),
|
|
227
|
-
USE_GPU:
|
|
309
|
+
USE_GPU: resolvedUseGpu ? 'true' : 'false',
|
|
310
|
+
DMLA_LOG_FILE: logFile // 传递日志文件路径给服务端
|
|
228
311
|
}
|
|
229
312
|
|
|
313
|
+
// 写入启动日志
|
|
314
|
+
const timestamp = new Date().toISOString()
|
|
315
|
+
fs.writeSync(logStream, `[${timestamp}] Server starting...\n`)
|
|
316
|
+
fs.writeSync(logStream, `[${timestamp}] Server path: ${actualServerPath}\n`)
|
|
317
|
+
fs.writeSync(logStream, `[${timestamp}] Port: ${port}\n`)
|
|
318
|
+
fs.writeSync(logStream, `[${timestamp}] GPU: ${resolvedUseGpu} (${imageResolution.message})\n`)
|
|
319
|
+
|
|
320
|
+
// 使用 spawn 启动 server 进程
|
|
321
|
+
// 重要:stdio 必须是 'ignore' 或管道,不能是 'inherit'
|
|
322
|
+
// 因为 'inherit' 会让子进程依赖父进程的 stdout,父进程退出后子进程也会退出
|
|
230
323
|
const serverProcess = spawn('node', [actualServerPath], {
|
|
231
324
|
env,
|
|
232
|
-
stdio: '
|
|
233
|
-
detached: true
|
|
325
|
+
stdio: ['ignore', logStream, errorLogStream], // stdin: ignore, stdout: log file, stderr: error log
|
|
326
|
+
detached: true,
|
|
327
|
+
windowsHide: true // Windows 下隐藏窗口
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// 监听子进程事件(调试用)
|
|
331
|
+
serverProcess.on('error', (err) => {
|
|
332
|
+
fs.writeSync(errorLogStream, `[${new Date().toISOString()}] Spawn error: ${err.message}\n`)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
serverProcess.on('exit', (code, signal) => {
|
|
336
|
+
const msg = `[${new Date().toISOString()}] Process exited: code=${code}, signal=${signal}\n`
|
|
337
|
+
fs.writeSync(logStream, msg)
|
|
338
|
+
fs.writeSync(errorLogStream, msg)
|
|
234
339
|
})
|
|
235
340
|
|
|
236
341
|
serverProcess.unref()
|
|
237
342
|
|
|
343
|
+
// 关闭父进程中的文件描述符(子进程会保留自己的副本)
|
|
344
|
+
fs.closeSync(logStream)
|
|
345
|
+
fs.closeSync(errorLogStream)
|
|
346
|
+
|
|
238
347
|
// 等待服务启动
|
|
239
348
|
console.log(chalk.gray(' 等待服务就绪...'))
|
|
240
349
|
let attempts = 0
|
|
@@ -245,15 +354,19 @@ export async function startServer(port, useGpu = false) {
|
|
|
245
354
|
if (running) {
|
|
246
355
|
console.log(chalk.green(`✅ 服务已启动: http://localhost:${port}`))
|
|
247
356
|
console.log(chalk.gray(` 健康检查: http://localhost:${port}/api/health`))
|
|
357
|
+
console.log(chalk.gray(` 日志查看: ${logFile}`))
|
|
248
358
|
return
|
|
249
359
|
}
|
|
250
360
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
251
361
|
attempts++
|
|
252
362
|
}
|
|
253
363
|
|
|
254
|
-
console.log(chalk.yellow('⚠️
|
|
364
|
+
console.log(chalk.yellow('⚠️ 服务启动超时'))
|
|
365
|
+
console.log(chalk.gray(` 请查看日志: ${logFile}`))
|
|
366
|
+
console.log(chalk.gray(` 或使用 --sync 模式调试`))
|
|
255
367
|
} catch (error) {
|
|
256
368
|
console.log(chalk.red(`❌ 启动失败: ${error.message}`))
|
|
369
|
+
console.log(chalk.gray(error.stack))
|
|
257
370
|
}
|
|
258
371
|
}
|
|
259
372
|
|
|
@@ -261,7 +374,51 @@ export async function startServer(port, useGpu = false) {
|
|
|
261
374
|
* 停止服务
|
|
262
375
|
*/
|
|
263
376
|
export async function stopServer() {
|
|
264
|
-
//
|
|
377
|
+
// 首先尝试通过 API 停止服务
|
|
378
|
+
const port = CONFIG.defaultPort
|
|
379
|
+
const running = await checkServiceRunning(port)
|
|
380
|
+
|
|
381
|
+
if (running) {
|
|
382
|
+
try {
|
|
383
|
+
// 调用 shutdown API
|
|
384
|
+
await new Promise((resolve, reject) => {
|
|
385
|
+
const req = http.request({
|
|
386
|
+
hostname: 'localhost',
|
|
387
|
+
port: port,
|
|
388
|
+
path: '/api/shutdown',
|
|
389
|
+
method: 'POST',
|
|
390
|
+
timeout: 5000
|
|
391
|
+
}, (res) => {
|
|
392
|
+
if (res.statusCode === 200) {
|
|
393
|
+
console.log(chalk.green('✅ 服务已停止'))
|
|
394
|
+
resolve()
|
|
395
|
+
} else {
|
|
396
|
+
reject(new Error(`HTTP ${res.statusCode}`))
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
req.on('error', (e) => reject(e))
|
|
400
|
+
req.on('timeout', () => {
|
|
401
|
+
req.destroy()
|
|
402
|
+
reject(new Error('Timeout'))
|
|
403
|
+
})
|
|
404
|
+
req.end()
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
// 等待服务完全关闭
|
|
408
|
+
let attempts = 0
|
|
409
|
+
while (attempts < 10) {
|
|
410
|
+
const stillRunning = await checkServiceRunning(port)
|
|
411
|
+
if (!stillRunning) break
|
|
412
|
+
await new Promise(r => setTimeout(r, 200))
|
|
413
|
+
attempts++
|
|
414
|
+
}
|
|
415
|
+
return
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.log(chalk.yellow(`⚠️ 通过 API 停止失败: ${error.message}`))
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 尝试查找并停止 Docker 容器
|
|
265
422
|
const container = await findServiceContainer()
|
|
266
423
|
|
|
267
424
|
if (container) {
|
|
@@ -269,14 +426,15 @@ export async function stopServer() {
|
|
|
269
426
|
const containerObj = docker.getContainer(container.Id)
|
|
270
427
|
await containerObj.stop()
|
|
271
428
|
await containerObj.remove()
|
|
272
|
-
console.log(chalk.green('✅
|
|
429
|
+
console.log(chalk.green('✅ 服务容器已停止'))
|
|
273
430
|
} catch (error) {
|
|
274
|
-
console.log(chalk.red(`❌
|
|
431
|
+
console.log(chalk.red(`❌ 停止容器失败: ${error.message}`))
|
|
275
432
|
}
|
|
433
|
+
} else if (!running) {
|
|
434
|
+
console.log(chalk.gray(' 服务未运行'))
|
|
276
435
|
} else {
|
|
277
|
-
|
|
278
|
-
console.log(chalk.
|
|
279
|
-
console.log(chalk.gray(' 提示: 服务可能以非容器模式运行'))
|
|
436
|
+
console.log(chalk.yellow('⚠️ 无法停止服务'))
|
|
437
|
+
console.log(chalk.gray(' 提示: 手动终止端口 3001 上的进程'))
|
|
280
438
|
}
|
|
281
439
|
}
|
|
282
440
|
|
|
@@ -287,9 +445,10 @@ export async function getStatus() {
|
|
|
287
445
|
console.log()
|
|
288
446
|
|
|
289
447
|
// 检查 npm 包版本
|
|
290
|
-
console.log(chalk.bold('
|
|
448
|
+
console.log(chalk.bold('npm 包版本'))
|
|
291
449
|
try {
|
|
292
|
-
|
|
450
|
+
// __dirname 是 src/commands,需要向上两级到包根目录
|
|
451
|
+
const pkgPath = path.resolve(__dirname, '../../package.json')
|
|
293
452
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
294
453
|
console.log(chalk.gray(` @icyfenix-dmla/cli: ${pkg.version}`))
|
|
295
454
|
} catch {
|
|
@@ -299,7 +458,7 @@ export async function getStatus() {
|
|
|
299
458
|
console.log()
|
|
300
459
|
|
|
301
460
|
// 检查镜像
|
|
302
|
-
console.log(chalk.bold('
|
|
461
|
+
console.log(chalk.bold('Docker 镜像'))
|
|
303
462
|
const cpuExists = await checkImageExists('cpu')
|
|
304
463
|
const gpuExists = await checkImageExists('gpu')
|
|
305
464
|
console.log(chalk.gray(` CPU: ${cpuExists ? chalk.green('已安装') : chalk.red('未安装')}`))
|
|
@@ -308,7 +467,7 @@ export async function getStatus() {
|
|
|
308
467
|
console.log()
|
|
309
468
|
|
|
310
469
|
// 检查 GPU
|
|
311
|
-
console.log(chalk.bold('
|
|
470
|
+
console.log(chalk.bold('GPU 状态'))
|
|
312
471
|
const gpuAvailable = await checkGPUAvailable()
|
|
313
472
|
if (gpuAvailable) {
|
|
314
473
|
console.log(chalk.green(' GPU 可用'))
|
|
@@ -326,7 +485,7 @@ export async function getStatus() {
|
|
|
326
485
|
console.log()
|
|
327
486
|
|
|
328
487
|
// 检查服务
|
|
329
|
-
console.log(chalk.bold('
|
|
488
|
+
console.log(chalk.bold('服务状态'))
|
|
330
489
|
const running = await checkServiceRunning(CONFIG.defaultPort)
|
|
331
490
|
if (running) {
|
|
332
491
|
console.log(chalk.green(` 服务运行中 (端口 ${CONFIG.defaultPort})`))
|
package/src/index.js
CHANGED
|
@@ -2,18 +2,87 @@
|
|
|
2
2
|
* DMLA CLI 入口
|
|
3
3
|
* 沙箱服务命令行管理工具
|
|
4
4
|
*/
|
|
5
|
-
import { program } from 'commander'
|
|
5
|
+
import { program, Help } from 'commander'
|
|
6
6
|
import chalk from 'chalk'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import fs from 'fs'
|
|
7
10
|
import { startServer, startServerSync, stopServer, getStatus } from './commands/server.js'
|
|
8
|
-
import {
|
|
11
|
+
import { runDoctor } from './commands/manage.js'
|
|
9
12
|
import { runInstallTUI } from '@icyfenix-dmla/install'
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
// 从 package.json 读取版本号
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = path.dirname(__filename)
|
|
17
|
+
const pkgPath = path.resolve(__dirname, '../package.json')
|
|
18
|
+
const VERSION = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version
|
|
19
|
+
|
|
20
|
+
// 重写 Help 类的方法以输出中文标题
|
|
21
|
+
Help.prototype.padWidth = function(cmd, helper) {
|
|
22
|
+
return 20
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 重写帮助信息格式化方法
|
|
26
|
+
Help.prototype.formatHelp = function(cmd, helper) {
|
|
27
|
+
const indent = ' '
|
|
28
|
+
const itemIndent = ' '
|
|
29
|
+
|
|
30
|
+
let output = []
|
|
31
|
+
|
|
32
|
+
// 用法(中文)
|
|
33
|
+
output.push('用法:')
|
|
34
|
+
output.push(indent + helper.commandUsage(cmd))
|
|
35
|
+
|
|
36
|
+
// 说明(中文)
|
|
37
|
+
if (cmd.description()) {
|
|
38
|
+
output.push('')
|
|
39
|
+
output.push('说明:')
|
|
40
|
+
output.push(indent + cmd.description())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 参数(中文)
|
|
44
|
+
const args = helper.visibleArguments(cmd)
|
|
45
|
+
if (args.length > 0) {
|
|
46
|
+
output.push('')
|
|
47
|
+
output.push('参数:')
|
|
48
|
+
args.forEach(arg => {
|
|
49
|
+
output.push(itemIndent + arg.name())
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 选项(中文)
|
|
54
|
+
const options = helper.visibleOptions(cmd)
|
|
55
|
+
if (options.length > 0) {
|
|
56
|
+
output.push('')
|
|
57
|
+
output.push('选项:')
|
|
58
|
+
options.forEach(opt => {
|
|
59
|
+
const term = helper.optionTerm(opt)
|
|
60
|
+
const description = helper.optionDescription(opt)
|
|
61
|
+
output.push(itemIndent + term.padEnd(20) + description)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 命令(中文)
|
|
66
|
+
const commands = helper.visibleCommands(cmd)
|
|
67
|
+
if (commands.length > 0) {
|
|
68
|
+
output.push('')
|
|
69
|
+
output.push('命令:')
|
|
70
|
+
commands.forEach(subcmd => {
|
|
71
|
+
const term = helper.subcommandTerm(subcmd)
|
|
72
|
+
const description = helper.subcommandDescription(subcmd)
|
|
73
|
+
output.push(itemIndent + term.padEnd(20) + description)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return output.join('\n')
|
|
78
|
+
}
|
|
12
79
|
|
|
13
80
|
program
|
|
14
81
|
.name('dmla')
|
|
15
82
|
.description('DMLA 沙箱服务命令行管理工具')
|
|
16
|
-
.version(VERSION)
|
|
83
|
+
.version(VERSION, '-v, --version', '显示版本号')
|
|
84
|
+
.helpOption('-h, --help', '显示帮助信息')
|
|
85
|
+
.addHelpCommand('help [command]', '显示命令帮助信息')
|
|
17
86
|
|
|
18
87
|
// ─────────────────────────────────────────────────────────────
|
|
19
88
|
// start 命令
|
|
@@ -29,9 +98,9 @@ program
|
|
|
29
98
|
const useGpu = options.gpu
|
|
30
99
|
const sync = options.sync
|
|
31
100
|
|
|
32
|
-
console.log(chalk.blue('
|
|
101
|
+
console.log(chalk.blue('启动 DMLA 沙箱服务...'))
|
|
33
102
|
console.log(chalk.gray(` 端口: ${port}`))
|
|
34
|
-
console.log(chalk.gray(`
|
|
103
|
+
console.log(chalk.gray(` 请求类型: ${useGpu ? 'GPU' : '自动选择'}`))
|
|
35
104
|
if (sync) {
|
|
36
105
|
console.log(chalk.yellow(` 模式: 同步(调试模式)`))
|
|
37
106
|
}
|
|
@@ -50,7 +119,7 @@ program
|
|
|
50
119
|
.command('stop')
|
|
51
120
|
.description('停止运行中的沙箱服务')
|
|
52
121
|
.action(async () => {
|
|
53
|
-
console.log(chalk.blue('
|
|
122
|
+
console.log(chalk.blue('停止 DMLA 沙箱服务...'))
|
|
54
123
|
await stopServer()
|
|
55
124
|
})
|
|
56
125
|
|
|
@@ -61,7 +130,7 @@ program
|
|
|
61
130
|
.command('status')
|
|
62
131
|
.description('查看服务状态')
|
|
63
132
|
.action(async () => {
|
|
64
|
-
console.log(chalk.blue('
|
|
133
|
+
console.log(chalk.blue('DMLA 沙箱服务状态'))
|
|
65
134
|
await getStatus()
|
|
66
135
|
})
|
|
67
136
|
|
|
@@ -75,18 +144,6 @@ program
|
|
|
75
144
|
await runInstallTUI()
|
|
76
145
|
})
|
|
77
146
|
|
|
78
|
-
// ─────────────────────────────────────────────────────────────
|
|
79
|
-
// update 命令
|
|
80
|
-
// ─────────────────────────────────────────────────────────────
|
|
81
|
-
program
|
|
82
|
-
.command('update')
|
|
83
|
-
.description('更新 npm 包和 Docker 镜像')
|
|
84
|
-
.option('-r, --registry <type>', '镜像仓库 (dockerhub/acr)', 'dockerhub')
|
|
85
|
-
.action(async (options) => {
|
|
86
|
-
console.log(chalk.blue('🔄 更新 DMLA...'))
|
|
87
|
-
await updateAll(options.registry)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
147
|
// ─────────────────────────────────────────────────────────────
|
|
91
148
|
// doctor 命令
|
|
92
149
|
// ─────────────────────────────────────────────────────────────
|
|
@@ -94,7 +151,7 @@ program
|
|
|
94
151
|
.command('doctor')
|
|
95
152
|
.description('诊断安装环境')
|
|
96
153
|
.action(async () => {
|
|
97
|
-
console.log(chalk.blue('
|
|
154
|
+
console.log(chalk.blue('DMLA 环境诊断'))
|
|
98
155
|
await runDoctor()
|
|
99
156
|
})
|
|
100
157
|
|
package/src/server/index.js
CHANGED
|
@@ -11,27 +11,82 @@ import sandboxRouter from './routes/sandbox.js'
|
|
|
11
11
|
export const app = express()
|
|
12
12
|
const PORT = process.env.PORT || 3001
|
|
13
13
|
|
|
14
|
+
// 日志函数
|
|
15
|
+
function log(message) {
|
|
16
|
+
const timestamp = new Date().toISOString()
|
|
17
|
+
console.log(`[${timestamp}] ${message}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 启动日志
|
|
21
|
+
log('Server initializing...')
|
|
22
|
+
log(`PORT: ${PORT}`)
|
|
23
|
+
log(`NODE_VERSION: ${process.version}`)
|
|
24
|
+
log(`PLATFORM: ${process.platform}`)
|
|
25
|
+
log(`DMLA_SYNC_MODE: ${process.env.DMLA_SYNC_MODE || 'false'}`)
|
|
26
|
+
|
|
14
27
|
// 中间件
|
|
15
28
|
app.use(cors())
|
|
16
29
|
app.use(express.json())
|
|
17
30
|
|
|
31
|
+
// 请求日志中间件
|
|
32
|
+
app.use((req, res, next) => {
|
|
33
|
+
log(`Request: ${req.method} ${req.path}`)
|
|
34
|
+
next()
|
|
35
|
+
})
|
|
36
|
+
|
|
18
37
|
// 健康检查
|
|
19
38
|
app.get('/api/health', (req, res) => {
|
|
39
|
+
log('Health check request')
|
|
20
40
|
res.json({ status: 'ok', timestamp: new Date().toISOString() })
|
|
21
41
|
})
|
|
22
42
|
|
|
43
|
+
// 停止服务(用于 CLI stop 命令)
|
|
44
|
+
app.post('/api/shutdown', (req, res) => {
|
|
45
|
+
log('Shutdown request received')
|
|
46
|
+
res.json({ status: 'shutting_down', timestamp: new Date().toISOString() })
|
|
47
|
+
// 延迟关闭,确保响应发送完成
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
log('Exiting due to shutdown request')
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}, 100)
|
|
52
|
+
})
|
|
53
|
+
|
|
23
54
|
// 沙箱 API
|
|
24
55
|
app.use('/api/sandbox', sandboxRouter)
|
|
25
56
|
|
|
26
57
|
// 错误处理
|
|
27
58
|
app.use((err, req, res, next) => {
|
|
28
|
-
|
|
59
|
+
log(`Error: ${err.message}`)
|
|
60
|
+
log(`Stack: ${err.stack}`)
|
|
29
61
|
res.status(500).json({
|
|
30
62
|
success: false,
|
|
31
63
|
error: err.message || 'Internal Server Error'
|
|
32
64
|
})
|
|
33
65
|
})
|
|
34
66
|
|
|
67
|
+
// 捕获未处理的异常
|
|
68
|
+
process.on('uncaughtException', (err) => {
|
|
69
|
+
log(`UNCAUGHT EXCEPTION: ${err.message}`)
|
|
70
|
+
log(`Stack: ${err.stack}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// 捕获未处理的 Promise 拒绝
|
|
75
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
76
|
+
log(`UNHANDLED REJECTION: ${reason}`)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// 捕获进程信号
|
|
80
|
+
process.on('SIGTERM', () => {
|
|
81
|
+
log('Received SIGTERM')
|
|
82
|
+
process.exit(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
process.on('SIGINT', () => {
|
|
86
|
+
log('Received SIGINT')
|
|
87
|
+
process.exit(0)
|
|
88
|
+
})
|
|
89
|
+
|
|
35
90
|
// 启动服务器
|
|
36
91
|
// 条件1: 直接运行(入口点匹配)
|
|
37
92
|
// 条件2: 同步模式(DMLA_SYNC_MODE 环境变量)
|
|
@@ -39,12 +94,24 @@ const __filename = fileURLToPath(import.meta.url)
|
|
|
39
94
|
const entryPoint = resolve(process.argv[1] || '')
|
|
40
95
|
const shouldStart = __filename === entryPoint || process.env.DMLA_SYNC_MODE === 'true'
|
|
41
96
|
|
|
97
|
+
log(`Entry point check: __filename=${__filename}, entryPoint=${entryPoint}, match=${__filename === entryPoint}`)
|
|
98
|
+
|
|
42
99
|
if (shouldStart) {
|
|
43
|
-
app.listen(PORT, () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
100
|
+
const server = app.listen(PORT, () => {
|
|
101
|
+
log('Server started successfully')
|
|
102
|
+
log(`API: http://localhost:${PORT}`)
|
|
103
|
+
log(`Health: http://localhost:${PORT}/api/health`)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
server.on('error', (err) => {
|
|
107
|
+
log(`Server error: ${err.message}`)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
server.on('close', () => {
|
|
111
|
+
log('Server closed')
|
|
47
112
|
})
|
|
113
|
+
} else {
|
|
114
|
+
log('Skipping server start (imported as module)')
|
|
48
115
|
}
|
|
49
116
|
|
|
50
117
|
export default app
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* 沙箱 API 路由
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express'
|
|
5
|
-
import { runPythonCode, checkImageExists, checkGPUAvailable } from '../sandbox.js'
|
|
5
|
+
import sandbox, { runPythonCode, checkImageExists, checkGPUAvailable, checkCUDACompatibility } from '../sandbox.js'
|
|
6
|
+
|
|
7
|
+
const { SANDBOX_CONFIG } = sandbox
|
|
6
8
|
|
|
7
9
|
const router = Router()
|
|
8
10
|
|
|
@@ -56,31 +58,45 @@ router.post('/run', async (req, res) => {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
try {
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
+
// 检查镜像是否存在,智能降级
|
|
62
|
+
// GPU镜像包含CPU的全部功能,如果CPU镜像不存在但GPU镜像存在,可以使用GPU镜像执行CPU代码
|
|
63
|
+
let actualUseGpu = useGpu
|
|
64
|
+
let actualImage = null // 指定使用的镜像
|
|
65
|
+
let imageExists = await checkImageExists(useGpu)
|
|
66
|
+
|
|
67
|
+
if (!imageExists && !useGpu) {
|
|
68
|
+
// CPU镜像不存在,检查是否可以用GPU镜像替代
|
|
69
|
+
const gpuImageExists = await checkImageExists(true)
|
|
70
|
+
if (gpuImageExists) {
|
|
71
|
+
imageExists = true
|
|
72
|
+
actualUseGpu = false // 不启用GPU设备
|
|
73
|
+
actualImage = SANDBOX_CONFIG.imageGpu // 使用GPU镜像
|
|
74
|
+
console.log('[Sandbox] CPU镜像不存在,使用GPU镜像执行(不启用GPU设备)')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
61
77
|
|
|
62
78
|
if (!imageExists) {
|
|
63
79
|
return res.status(503).json({
|
|
64
80
|
success: false,
|
|
65
81
|
error: useGpu
|
|
66
82
|
? 'GPU 镜像未安装。请运行以下命令安装:\n\nnpm run build:sandbox:gpu\n\n或使用 dmla CLI:\n\ndmla install --gpu'
|
|
67
|
-
: '沙箱镜像未安装。请运行以下命令安装:\n\nnpm run build:sandbox:cpu\n\n或使用 dmla CLI:\n\ndmla install --cpu'
|
|
83
|
+
: '沙箱镜像未安装。请运行以下命令安装:\n\nnpm run build:sandbox:cpu\n\n或使用 dmla CLI:\n\ndmla install --cpu\n\n注意:如果您已安装 GPU 镜像,它也支持 CPU 执行'
|
|
68
84
|
})
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
// 如果请求 GPU,检查 GPU 是否可用
|
|
72
|
-
if (
|
|
88
|
+
if (actualUseGpu) {
|
|
73
89
|
const gpuAvailable = await checkGPUAvailable()
|
|
74
90
|
if (!gpuAvailable) {
|
|
75
91
|
return res.status(503).json({
|
|
76
92
|
success: false,
|
|
77
|
-
error:
|
|
93
|
+
error: `GPU 硬件不可用。请确保系统安装了 NVIDIA GPU 驱动和 nvidia-container-toolkit。\n\n诊断步骤:\n1. 运行 nvidia-smi 检查 GPU 状态\n2. 运行 docker run --rm --gpus all ${SANDBOX_CONFIG.imageGpu} nvidia-smi 测试 Docker GPU 支持\n\n或使用 dmla doctor 进行环境诊断`
|
|
78
94
|
})
|
|
79
95
|
}
|
|
80
96
|
}
|
|
81
97
|
|
|
82
|
-
//
|
|
83
|
-
const result = await runPythonCode(code,
|
|
98
|
+
// 执行代码(使用确定后的镜像)
|
|
99
|
+
const result = await runPythonCode(code, actualUseGpu, actualImage)
|
|
84
100
|
|
|
85
101
|
res.json(result)
|
|
86
102
|
|
|
@@ -112,4 +128,41 @@ router.get('/gpu', async (req, res) => {
|
|
|
112
128
|
}
|
|
113
129
|
})
|
|
114
130
|
|
|
131
|
+
/**
|
|
132
|
+
* CUDA 兼容性检查
|
|
133
|
+
* 返回详细的 CUDA 环境诊断信息
|
|
134
|
+
*/
|
|
135
|
+
router.get('/cuda-compat', async (req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const imageGpuExists = await checkImageExists(true)
|
|
138
|
+
|
|
139
|
+
if (!imageGpuExists) {
|
|
140
|
+
return res.json({
|
|
141
|
+
status: 'error',
|
|
142
|
+
message: 'GPU 镜像未安装',
|
|
143
|
+
compatible: false,
|
|
144
|
+
suggestion: '请运行 npm run build:sandbox:gpu 或 dmla install --gpu'
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const compatResult = await checkCUDACompatibility()
|
|
149
|
+
|
|
150
|
+
res.json({
|
|
151
|
+
status: compatResult.compatible ? 'ok' : 'error',
|
|
152
|
+
compatible: compatResult.compatible,
|
|
153
|
+
details: compatResult.details,
|
|
154
|
+
issues: compatResult.issues,
|
|
155
|
+
message: compatResult.compatible
|
|
156
|
+
? 'CUDA 环境完全兼容,GPU 加速可用'
|
|
157
|
+
: 'CUDA 环境不兼容,请使用 CPU 模式或重新构建镜像'
|
|
158
|
+
})
|
|
159
|
+
} catch (error) {
|
|
160
|
+
res.status(500).json({
|
|
161
|
+
status: 'error',
|
|
162
|
+
compatible: false,
|
|
163
|
+
error: error.message
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
115
168
|
export default router
|
package/src/server/sandbox.js
CHANGED
|
@@ -10,6 +10,15 @@ import fs from 'fs'
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
11
11
|
const __dirname = path.dirname(__filename)
|
|
12
12
|
|
|
13
|
+
// 日志函数
|
|
14
|
+
function log(message) {
|
|
15
|
+
const timestamp = new Date().toISOString()
|
|
16
|
+
console.log(`[${timestamp}] [Sandbox] ${message}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 启动时记录
|
|
20
|
+
log('Sandbox module initialized')
|
|
21
|
+
|
|
13
22
|
// 检测运行模式并计算正确的路径
|
|
14
23
|
// 开发模式: 从 local-server/src 运行,项目根目录在上两级
|
|
15
24
|
// 独立模式: 从 packages/cli/src/server 运行,无 shared_modules 目录
|
|
@@ -39,6 +48,11 @@ const DEFAULT_SHARED_MODULES_PATH = PROJECT_ROOT
|
|
|
39
48
|
? path.join(PROJECT_ROOT, 'local-server', 'shared_modules')
|
|
40
49
|
: null
|
|
41
50
|
|
|
51
|
+
// kernel_runner.py 路径(开发模式下可用)
|
|
52
|
+
const DEFAULT_KERNEL_RUNNER_PATH = PROJECT_ROOT
|
|
53
|
+
? path.join(PROJECT_ROOT, 'local-server', 'src', 'kernel_runner.py')
|
|
54
|
+
: null
|
|
55
|
+
|
|
42
56
|
const docker = new Docker()
|
|
43
57
|
|
|
44
58
|
// 沙箱配置
|
|
@@ -61,6 +75,18 @@ function getSharedModulesPath() {
|
|
|
61
75
|
return DEFAULT_SHARED_MODULES_PATH
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
/**
|
|
79
|
+
* 获取 kernel_runner.py 路径
|
|
80
|
+
*/
|
|
81
|
+
function getKernelRunnerPath() {
|
|
82
|
+
// 优先使用环境变量指定的路径
|
|
83
|
+
if (process.env.KERNEL_RUNNER_PATH) {
|
|
84
|
+
return process.env.KERNEL_RUNNER_PATH
|
|
85
|
+
}
|
|
86
|
+
// 开发模式下的默认路径
|
|
87
|
+
return DEFAULT_KERNEL_RUNNER_PATH
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
/**
|
|
65
91
|
* 检查是否启用 Volume Mount
|
|
66
92
|
*/
|
|
@@ -68,17 +94,24 @@ function shouldMountSharedModules() {
|
|
|
68
94
|
return process.env.MOUNT_SHARED_MODULES !== 'false'
|
|
69
95
|
}
|
|
70
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 检查是否挂载本地 kernel_runner.py(开发模式)
|
|
99
|
+
*/
|
|
100
|
+
function shouldMountKernelRunner() {
|
|
101
|
+
return process.env.MOUNT_KERNEL_RUNNER !== 'false' && PROJECT_ROOT !== null
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
/**
|
|
72
105
|
* 检查 GPU 是否可用
|
|
73
|
-
*
|
|
106
|
+
* 使用已安装的 GPU 镜像运行 nvidia-smi 命令检测 GPU 状态
|
|
74
107
|
*/
|
|
75
108
|
export async function checkGPUAvailable() {
|
|
76
109
|
let container = null
|
|
77
110
|
|
|
78
111
|
try {
|
|
79
|
-
//
|
|
112
|
+
// 使用已配置的 GPU 镜像检测,而非硬编码的 nvidia/cuda 镜像
|
|
80
113
|
container = await docker.createContainer({
|
|
81
|
-
Image:
|
|
114
|
+
Image: SANDBOX_CONFIG.imageGpu,
|
|
82
115
|
Cmd: ['nvidia-smi', '-L'],
|
|
83
116
|
HostConfig: {
|
|
84
117
|
DeviceRequests: [{
|
|
@@ -121,18 +154,179 @@ export async function checkGPUAvailable() {
|
|
|
121
154
|
}
|
|
122
155
|
}
|
|
123
156
|
|
|
157
|
+
/**
|
|
158
|
+
* 检查 CUDA 兼容性
|
|
159
|
+
* 在 GPU 镜像中运行简单的 CUDA 操作测试,验证 PyTorch 与 GPU 兼容
|
|
160
|
+
* @returns {Promise<{compatible: boolean, issues: string[], details: object}>}
|
|
161
|
+
*/
|
|
162
|
+
export async function checkCUDACompatibility() {
|
|
163
|
+
let container = null
|
|
164
|
+
|
|
165
|
+
const testCode = `
|
|
166
|
+
import torch
|
|
167
|
+
import json
|
|
168
|
+
|
|
169
|
+
result = {
|
|
170
|
+
'pytorch_version': torch.__version__,
|
|
171
|
+
'cuda_available': torch.cuda.is_available(),
|
|
172
|
+
'cuda_version': str(torch.version.cuda) if torch.cuda.is_available() else None,
|
|
173
|
+
'device_name': torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
|
|
174
|
+
'compatible': True,
|
|
175
|
+
'test_passed': False,
|
|
176
|
+
'error': None
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if torch.cuda.is_available():
|
|
180
|
+
try:
|
|
181
|
+
x = torch.randn(100, 100, device='cuda')
|
|
182
|
+
y = x + x
|
|
183
|
+
torch.cuda.synchronize()
|
|
184
|
+
result['test_passed'] = True
|
|
185
|
+
except RuntimeError as e:
|
|
186
|
+
result['compatible'] = False
|
|
187
|
+
result['error'] = str(e)
|
|
188
|
+
if 'no kernel image' in str(e) or 'CUDA error' in str(e):
|
|
189
|
+
result['error_type'] = 'compatibility'
|
|
190
|
+
|
|
191
|
+
print(json.dumps(result))
|
|
192
|
+
`
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
container = await docker.createContainer({
|
|
196
|
+
Image: SANDBOX_CONFIG.imageGpu,
|
|
197
|
+
Cmd: ['python3', '-c', testCode],
|
|
198
|
+
HostConfig: {
|
|
199
|
+
DeviceRequests: [{
|
|
200
|
+
Driver: 'nvidia',
|
|
201
|
+
Count: -1,
|
|
202
|
+
Capabilities: [['gpu']]
|
|
203
|
+
}],
|
|
204
|
+
AutoRemove: false
|
|
205
|
+
},
|
|
206
|
+
Env: ['PYTHONUNBUFFERED=1']
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await container.start()
|
|
210
|
+
await container.wait()
|
|
211
|
+
|
|
212
|
+
const logs = await container.logs({
|
|
213
|
+
stdout: true,
|
|
214
|
+
stderr: true
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const { stdout, stderr } = parseDockerLogsSeparate(logs)
|
|
218
|
+
|
|
219
|
+
// 尝试解析 JSON 输出
|
|
220
|
+
const jsonStart = stdout.indexOf('{')
|
|
221
|
+
if (jsonStart !== -1) {
|
|
222
|
+
try {
|
|
223
|
+
const result = JSON.parse(stdout.substring(jsonStart))
|
|
224
|
+
return {
|
|
225
|
+
compatible: result.compatible && result.test_passed,
|
|
226
|
+
issues: result.error ? [result.error] : [],
|
|
227
|
+
details: result
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// JSON 解析失败
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 如果无法解析,检查 stderr 是否有 CUDA 错误
|
|
235
|
+
if (stderr.includes('no kernel image') || stderr.includes('CUDA error')) {
|
|
236
|
+
return {
|
|
237
|
+
compatible: false,
|
|
238
|
+
issues: [stderr],
|
|
239
|
+
details: { raw_output: stderr }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 默认返回未知状态
|
|
244
|
+
return {
|
|
245
|
+
compatible: true, // 假设兼容,让实际执行来验证
|
|
246
|
+
issues: [],
|
|
247
|
+
details: { stdout, stderr }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return {
|
|
252
|
+
compatible: false,
|
|
253
|
+
issues: [error.message],
|
|
254
|
+
details: { error: error.message }
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
if (container) {
|
|
258
|
+
try {
|
|
259
|
+
await container.remove({ force: true })
|
|
260
|
+
} catch {
|
|
261
|
+
// 忽略清理错误
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
124
267
|
/**
|
|
125
268
|
* 执行 Python 代码
|
|
126
269
|
* 使用 IPython Kernel 执行代码,支持富输出(图片、文本、错误等)
|
|
127
270
|
* @param {string} code - Python 代码
|
|
128
|
-
* @param {boolean} useGpu -
|
|
271
|
+
* @param {boolean} useGpu - 是否启用 GPU 设备
|
|
272
|
+
* @param {string|null} imageOverride - 可选,指定使用的镜像名称(覆盖默认选择)
|
|
129
273
|
* @returns {Promise<{success: boolean, outputs: Array, executionTime: number, gpuUsed: boolean}>}
|
|
130
274
|
*/
|
|
131
|
-
export async function runPythonCode(code, useGpu = false) {
|
|
275
|
+
export async function runPythonCode(code, useGpu = false, imageOverride = null) {
|
|
132
276
|
const startTime = Date.now()
|
|
133
277
|
|
|
134
|
-
|
|
135
|
-
|
|
278
|
+
log(`runPythonCode called, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}`)
|
|
279
|
+
|
|
280
|
+
// 选择镜像:优先使用指定的镜像,否则根据 useGpu 选择
|
|
281
|
+
const image = imageOverride || (useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu)
|
|
282
|
+
log(`Using image: ${image}`)
|
|
283
|
+
|
|
284
|
+
// GPU 兼容性预检查
|
|
285
|
+
if (useGpu) {
|
|
286
|
+
log('GPU mode: running CUDA compatibility pre-check...')
|
|
287
|
+
const compatResult = await checkCUDACompatibility()
|
|
288
|
+
log(`CUDA compatibility check result: ${JSON.stringify(compatResult)}`)
|
|
289
|
+
|
|
290
|
+
if (!compatResult.compatible) {
|
|
291
|
+
log('CUDA compatibility check failed')
|
|
292
|
+
const executionTime = (Date.now() - startTime) / 1000
|
|
293
|
+
|
|
294
|
+
// 构建详细的错误信息
|
|
295
|
+
const errorDetails = compatResult.details || {}
|
|
296
|
+
const errorType = errorDetails.error_type || 'unknown'
|
|
297
|
+
|
|
298
|
+
let errorMessage = 'CUDA 兼容性错误:PyTorch CUDA 版本与您的 GPU 不兼容\n\n'
|
|
299
|
+
|
|
300
|
+
if (errorType === 'compatibility' || compatResult.issues.some(i => i.includes('no kernel image'))) {
|
|
301
|
+
errorMessage += `诊断详情:\n`
|
|
302
|
+
errorMessage += `- PyTorch 版本: ${errorDetails.pytorch_version || '未知'}\n`
|
|
303
|
+
errorMessage += `- CUDA 版本: ${errorDetails.cuda_version || '未知'}\n`
|
|
304
|
+
errorMessage += `- GPU 设备: ${errorDetails.device_name || '未知'}\n`
|
|
305
|
+
errorMessage += `- 错误类型: CUDA kernel 不兼容\n\n`
|
|
306
|
+
errorMessage += `解决方案:\n`
|
|
307
|
+
errorMessage += `1. 使用 CPU 模式运行代码(在前端选择 "Run on CPU")\n`
|
|
308
|
+
errorMessage += `2. 在代码开头添加: device = torch.device('cpu')\n`
|
|
309
|
+
errorMessage += `3. 重新构建兼容的 Docker 镜像(修改 Dockerfile.sandbox 使用 CUDA 12.x)\n\n`
|
|
310
|
+
errorMessage += `更多诊断信息请运行: dmla doctor`
|
|
311
|
+
} else {
|
|
312
|
+
errorMessage += `错误详情: ${compatResult.issues.join('\n')}\n\n`
|
|
313
|
+
errorMessage += `建议使用 CPU 模式运行代码。`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
outputs: [{
|
|
319
|
+
type: 'error',
|
|
320
|
+
ename: 'CUDACompatError',
|
|
321
|
+
evalue: 'CUDA 兼容性错误',
|
|
322
|
+
traceback: [errorMessage]
|
|
323
|
+
}],
|
|
324
|
+
executionTime,
|
|
325
|
+
gpuUsed: false
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
log('CUDA compatibility check passed')
|
|
329
|
+
}
|
|
136
330
|
|
|
137
331
|
// 创建容器配置 - 使用 kernel_runner.py 执行代码
|
|
138
332
|
const containerConfig = {
|
|
@@ -148,25 +342,40 @@ export async function runPythonCode(code, useGpu = false) {
|
|
|
148
342
|
]
|
|
149
343
|
}
|
|
150
344
|
|
|
151
|
-
|
|
345
|
+
log('Container config created')
|
|
346
|
+
|
|
347
|
+
// Volume Mount 配置
|
|
152
348
|
const useMount = shouldMountSharedModules()
|
|
153
349
|
const sharedModulesPath = getSharedModulesPath()
|
|
350
|
+
const mountKernelRunner = shouldMountKernelRunner()
|
|
351
|
+
const kernelRunnerPath = getKernelRunnerPath()
|
|
352
|
+
|
|
353
|
+
// 收集所有需要挂载的路径
|
|
354
|
+
const binds = []
|
|
355
|
+
|
|
356
|
+
// 挂载共享模块
|
|
357
|
+
if (useMount && sharedModulesPath && fs.existsSync(sharedModulesPath)) {
|
|
358
|
+
binds.push(`${sharedModulesPath}:/usr/local/lib/python3.11/site-packages/shared:ro`)
|
|
359
|
+
console.log(`[Sandbox] 共享模块 Volume Mount: ${sharedModulesPath}`)
|
|
360
|
+
} else if (useMount && sharedModulesPath) {
|
|
361
|
+
console.warn(`[Sandbox] 警告: 共享模块目录不存在: ${sharedModulesPath}`)
|
|
362
|
+
}
|
|
154
363
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
console.log('[Sandbox] Volume Mount
|
|
364
|
+
// 挂载 kernel_runner.py(开发模式调试)
|
|
365
|
+
if (mountKernelRunner && kernelRunnerPath && fs.existsSync(kernelRunnerPath)) {
|
|
366
|
+
binds.push(`${kernelRunnerPath}:/workspace/kernel_runner.py:ro`)
|
|
367
|
+
console.log(`[Sandbox] kernel_runner.py Volume Mount: ${kernelRunnerPath}`)
|
|
368
|
+
} else if (mountKernelRunner && kernelRunnerPath) {
|
|
369
|
+
console.warn(`[Sandbox] 警告: kernel_runner.py 不存在: ${kernelRunnerPath}`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 设置 Binds
|
|
373
|
+
if (binds.length > 0) {
|
|
374
|
+
containerConfig.HostConfig.Binds = binds
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!PROJECT_ROOT) {
|
|
378
|
+
console.log('[Sandbox] 独立安装模式,无 Volume Mount')
|
|
170
379
|
}
|
|
171
380
|
|
|
172
381
|
// GPU 配置
|
|
@@ -183,41 +392,80 @@ export async function runPythonCode(code, useGpu = false) {
|
|
|
183
392
|
|
|
184
393
|
try {
|
|
185
394
|
// 创建容器
|
|
395
|
+
log('Creating container...')
|
|
186
396
|
container = await docker.createContainer(containerConfig)
|
|
397
|
+
log(`Container created: ${container.id}`)
|
|
187
398
|
|
|
188
399
|
// 设置超时
|
|
189
400
|
const timeoutPromise = new Promise((_, reject) => {
|
|
190
401
|
timeoutId = setTimeout(() => {
|
|
402
|
+
log('Execution timeout triggered')
|
|
191
403
|
reject(new Error('Execution timeout'))
|
|
192
404
|
}, SANDBOX_CONFIG.timeout + 10000) // 额外 10 秒用于清理
|
|
193
405
|
})
|
|
194
406
|
|
|
195
407
|
// 启动容器
|
|
408
|
+
log('Starting container...')
|
|
196
409
|
await container.start()
|
|
410
|
+
log('Container started')
|
|
197
411
|
|
|
198
412
|
// 等待执行完成
|
|
413
|
+
log('Waiting for container to finish...')
|
|
199
414
|
const waitPromise = container.wait()
|
|
200
415
|
|
|
201
416
|
// 竞速: 超时 vs 正常完成
|
|
202
417
|
const result = await Promise.race([waitPromise, timeoutPromise])
|
|
418
|
+
log(`Container finished, result: ${JSON.stringify(result)}`)
|
|
203
419
|
|
|
204
420
|
// 清除超时
|
|
205
421
|
if (timeoutId) clearTimeout(timeoutId)
|
|
206
422
|
|
|
207
423
|
// 获取输出
|
|
424
|
+
log('Getting container logs...')
|
|
208
425
|
const logs = await container.logs({
|
|
209
426
|
stdout: true,
|
|
210
427
|
stderr: true
|
|
211
428
|
})
|
|
212
429
|
|
|
213
|
-
// 解析输出
|
|
214
|
-
const
|
|
430
|
+
// 解析输出 - 分别处理 stdout 和 stderr
|
|
431
|
+
const { stdout, stderr } = parseDockerLogsSeparate(logs)
|
|
432
|
+
log(`Stdout length: ${stdout.length}`)
|
|
433
|
+
log(`Stderr length: ${stderr.length}`)
|
|
434
|
+
|
|
435
|
+
// stderr 可能包含调试信息
|
|
436
|
+
if (stderr.length > 0) {
|
|
437
|
+
log(`Stderr content preview: ${stderr.substring(0, 500)}`)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// stdout 可能包含 CUDA banner + JSON,需要提取 JSON 部分
|
|
441
|
+
// 找到第一个 '{' 作为 JSON 开始
|
|
442
|
+
const jsonStart = stdout.indexOf('{')
|
|
443
|
+
if (jsonStart === -1) {
|
|
444
|
+
log(`No JSON found in stdout`)
|
|
445
|
+
const executionTime = (Date.now() - startTime) / 1000
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
outputs: [{
|
|
449
|
+
type: 'error',
|
|
450
|
+
ename: 'OutputParseError',
|
|
451
|
+
evalue: 'No JSON output found',
|
|
452
|
+
traceback: [stdout.substring(0, 1000)]
|
|
453
|
+
}],
|
|
454
|
+
executionTime,
|
|
455
|
+
gpuUsed: useGpu
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const rawOutput = stdout.substring(jsonStart)
|
|
460
|
+
log(`JSON extracted from position ${jsonStart}, length: ${rawOutput.length}`)
|
|
215
461
|
|
|
216
462
|
// 解析 JSON 输出
|
|
217
463
|
let parsedResult
|
|
218
464
|
try {
|
|
219
465
|
parsedResult = JSON.parse(rawOutput)
|
|
466
|
+
log('Output parsed successfully')
|
|
220
467
|
} catch (parseError) {
|
|
468
|
+
log(`JSON parse error: ${parseError.message}`)
|
|
221
469
|
// 如果 JSON 解析失败,返回原始输出作为错误
|
|
222
470
|
const executionTime = (Date.now() - startTime) / 1000
|
|
223
471
|
return {
|
|
@@ -241,6 +489,8 @@ export async function runPythonCode(code, useGpu = false) {
|
|
|
241
489
|
}
|
|
242
490
|
|
|
243
491
|
} catch (error) {
|
|
492
|
+
log(`Execution error: ${error.message}`)
|
|
493
|
+
log(`Error stack: ${error.stack}`)
|
|
244
494
|
// 清除超时
|
|
245
495
|
if (timeoutId) clearTimeout(timeoutId)
|
|
246
496
|
|
|
@@ -260,11 +510,13 @@ export async function runPythonCode(code, useGpu = false) {
|
|
|
260
510
|
|
|
261
511
|
} finally {
|
|
262
512
|
// 清理容器
|
|
513
|
+
log('Cleaning up container...')
|
|
263
514
|
if (container) {
|
|
264
515
|
try {
|
|
265
516
|
await container.remove({ force: true })
|
|
517
|
+
log('Container removed')
|
|
266
518
|
} catch (e) {
|
|
267
|
-
|
|
519
|
+
log(`Container cleanup error: ${e.message}`)
|
|
268
520
|
}
|
|
269
521
|
}
|
|
270
522
|
}
|
|
@@ -305,6 +557,49 @@ function parseDockerLogs(logs) {
|
|
|
305
557
|
return logs.toString()
|
|
306
558
|
}
|
|
307
559
|
|
|
560
|
+
/**
|
|
561
|
+
* 解析 Docker 日志输出,分别返回 stdout 和 stderr
|
|
562
|
+
* Docker 日志格式: [8字节头][数据]
|
|
563
|
+
* 头部第一个字节: 0=stdin, 1=stdout, 2=stderr
|
|
564
|
+
*/
|
|
565
|
+
function parseDockerLogsSeparate(logs) {
|
|
566
|
+
if (!logs || logs.length === 0) return { stdout: '', stderr: '' }
|
|
567
|
+
|
|
568
|
+
// 如果是 Buffer
|
|
569
|
+
if (Buffer.isBuffer(logs)) {
|
|
570
|
+
let stdout = ''
|
|
571
|
+
let stderr = ''
|
|
572
|
+
let offset = 0
|
|
573
|
+
|
|
574
|
+
while (offset < logs.length) {
|
|
575
|
+
// 跳过 8 字节头
|
|
576
|
+
if (offset + 8 > logs.length) break
|
|
577
|
+
|
|
578
|
+
const streamType = logs[offset] // 1=stdout, 2=stderr
|
|
579
|
+
const length = logs.readUInt32BE(offset + 4)
|
|
580
|
+
|
|
581
|
+
offset += 8
|
|
582
|
+
|
|
583
|
+
if (offset + length > logs.length) break
|
|
584
|
+
|
|
585
|
+
const chunk = logs.slice(offset, offset + length).toString('utf8')
|
|
586
|
+
|
|
587
|
+
if (streamType === 1) {
|
|
588
|
+
stdout += chunk
|
|
589
|
+
} else if (streamType === 2) {
|
|
590
|
+
stderr += chunk
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
offset += length
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { stdout, stderr }
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 如果是字符串,无法区分,全部作为 stdout
|
|
600
|
+
return { stdout: logs.toString(), stderr: '' }
|
|
601
|
+
}
|
|
602
|
+
|
|
308
603
|
/**
|
|
309
604
|
* 检查沙箱镜像是否存在
|
|
310
605
|
*/
|
|
@@ -346,6 +641,8 @@ export async function pullImage(useGpu = false) {
|
|
|
346
641
|
export default {
|
|
347
642
|
runPythonCode,
|
|
348
643
|
checkGPUAvailable,
|
|
644
|
+
checkCUDACompatibility,
|
|
349
645
|
checkImageExists,
|
|
350
|
-
pullImage
|
|
646
|
+
pullImage,
|
|
647
|
+
SANDBOX_CONFIG
|
|
351
648
|
}
|