@icyfenix-dmla/cli 2026.5.3-1019 → 2026.5.3-1248
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/server.js +41 -0
- package/src/server/index.js +18 -5
- package/src/server/routes/sandbox.js +28 -1
- package/src/server/sandbox.js +119 -7
- package/version.json +2 -2
package/package.json
CHANGED
package/src/commands/server.js
CHANGED
|
@@ -326,6 +326,25 @@ function findServerPath() {
|
|
|
326
326
|
return null
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* 查找 kernel_runner.py 路径
|
|
331
|
+
* --dev 模式下需要挂载此文件
|
|
332
|
+
*/
|
|
333
|
+
function findKernelRunnerPath() {
|
|
334
|
+
// 开发环境路径:packages/cli/src/commands -> ../../../local-server/src/kernel_runner.py
|
|
335
|
+
const devPath = path.resolve(__dirname, '../../../local-server/src/kernel_runner.py')
|
|
336
|
+
// npm 包路径:packages/cli/src/commands -> ../server/kernel_runner.py(构建后)
|
|
337
|
+
const npmPath = path.resolve(__dirname, '../server/kernel_runner.py')
|
|
338
|
+
|
|
339
|
+
if (fs.existsSync(devPath)) {
|
|
340
|
+
return devPath
|
|
341
|
+
}
|
|
342
|
+
if (fs.existsSync(npmPath)) {
|
|
343
|
+
return npmPath
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
329
348
|
/**
|
|
330
349
|
* 查找共享模块目录
|
|
331
350
|
* --dev 模式下需要挂载此目录
|
|
@@ -427,10 +446,17 @@ export async function startServerSync(port, useGpu = false, dev = false) {
|
|
|
427
446
|
|
|
428
447
|
// 查找共享模块路径(--dev 模式需要)
|
|
429
448
|
const sharedModulesPath = dev ? findSharedModulesPath() : null
|
|
449
|
+
// 查找 kernel_runner.py 路径(--dev 模式需要)
|
|
450
|
+
const kernelRunnerPath = dev ? findKernelRunnerPath() : null
|
|
451
|
+
|
|
430
452
|
if (dev && !sharedModulesPath) {
|
|
431
453
|
console.log(chalk.yellow('⚠️ --dev 模式需要共享模块目录'))
|
|
432
454
|
console.log(chalk.gray(' 未找到 shared_modules,将仅使用镜像内置模块'))
|
|
433
455
|
}
|
|
456
|
+
if (dev && !kernelRunnerPath) {
|
|
457
|
+
console.log(chalk.yellow('⚠️ --dev 模式需要 kernel_runner.py'))
|
|
458
|
+
console.log(chalk.gray(' 未找到 kernel_runner.py,将仅使用镜像内置版本'))
|
|
459
|
+
}
|
|
434
460
|
|
|
435
461
|
console.log(chalk.gray(` 镜像类型: ${imageResolution.message}`))
|
|
436
462
|
console.log(chalk.gray(' 同步模式启动...'))
|
|
@@ -438,6 +464,9 @@ export async function startServerSync(port, useGpu = false, dev = false) {
|
|
|
438
464
|
if (dev && sharedModulesPath) {
|
|
439
465
|
console.log(chalk.gray(` 共享模块: ${sharedModulesPath}`))
|
|
440
466
|
}
|
|
467
|
+
if (dev && kernelRunnerPath) {
|
|
468
|
+
console.log(chalk.gray(` 执行器: ${kernelRunnerPath}`))
|
|
469
|
+
}
|
|
441
470
|
console.log()
|
|
442
471
|
|
|
443
472
|
// 设置环境变量
|
|
@@ -452,6 +481,9 @@ export async function startServerSync(port, useGpu = false, dev = false) {
|
|
|
452
481
|
if (sharedModulesPath) {
|
|
453
482
|
process.env.SHARED_MODULES_PATH = sharedModulesPath
|
|
454
483
|
}
|
|
484
|
+
if (kernelRunnerPath) {
|
|
485
|
+
process.env.KERNEL_RUNNER_PATH = kernelRunnerPath
|
|
486
|
+
}
|
|
455
487
|
}
|
|
456
488
|
|
|
457
489
|
// 动态 import 服务器模块并直接运行
|
|
@@ -542,9 +574,15 @@ export async function startServer(port, useGpu = false, dev = false) {
|
|
|
542
574
|
|
|
543
575
|
// 查找共享模块路径(--dev 模式需要)
|
|
544
576
|
const sharedModulesPath = dev ? findSharedModulesPath() : null
|
|
577
|
+
// 查找 kernel_runner.py 路径(--dev 模式需要)
|
|
578
|
+
const kernelRunnerPath = dev ? findKernelRunnerPath() : null
|
|
579
|
+
|
|
545
580
|
if (dev && sharedModulesPath) {
|
|
546
581
|
console.log(chalk.gray(` 共享模块: ${sharedModulesPath}`))
|
|
547
582
|
}
|
|
583
|
+
if (dev && kernelRunnerPath) {
|
|
584
|
+
console.log(chalk.gray(` 执行器: ${kernelRunnerPath}`))
|
|
585
|
+
}
|
|
548
586
|
|
|
549
587
|
// 日志文件路径
|
|
550
588
|
const logDir = path.resolve(__dirname, '../../logs')
|
|
@@ -574,6 +612,9 @@ export async function startServer(port, useGpu = false, dev = false) {
|
|
|
574
612
|
if (sharedModulesPath) {
|
|
575
613
|
env.SHARED_MODULES_PATH = sharedModulesPath
|
|
576
614
|
}
|
|
615
|
+
if (kernelRunnerPath) {
|
|
616
|
+
env.KERNEL_RUNNER_PATH = kernelRunnerPath
|
|
617
|
+
}
|
|
577
618
|
}
|
|
578
619
|
|
|
579
620
|
// 写入启动日志
|
package/src/server/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import cors from 'cors'
|
|
|
7
7
|
import { fileURLToPath } from 'url'
|
|
8
8
|
import { resolve } from 'path'
|
|
9
9
|
import sandboxRouter from './routes/sandbox.js'
|
|
10
|
+
import { cleanupAllContainers } from './sandbox.js'
|
|
10
11
|
|
|
11
12
|
export const app = express()
|
|
12
13
|
const PORT = process.env.PORT || 3001
|
|
@@ -76,14 +77,26 @@ process.on('unhandledRejection', (reason, promise) => {
|
|
|
76
77
|
log(`UNHANDLED REJECTION: ${reason}`)
|
|
77
78
|
})
|
|
78
79
|
|
|
79
|
-
// 捕获进程信号
|
|
80
|
-
process.on('SIGTERM', () => {
|
|
81
|
-
log('Received SIGTERM')
|
|
80
|
+
// 捕获进程信号 - 优雅退出,清理所有容器
|
|
81
|
+
process.on('SIGTERM', async () => {
|
|
82
|
+
log('Received SIGTERM, cleaning up containers...')
|
|
83
|
+
try {
|
|
84
|
+
await cleanupAllContainers()
|
|
85
|
+
log('All containers cleaned up')
|
|
86
|
+
} catch (e) {
|
|
87
|
+
log(`Cleanup error: ${e.message}`)
|
|
88
|
+
}
|
|
82
89
|
process.exit(0)
|
|
83
90
|
})
|
|
84
91
|
|
|
85
|
-
process.on('SIGINT', () => {
|
|
86
|
-
log('Received SIGINT')
|
|
92
|
+
process.on('SIGINT', async () => {
|
|
93
|
+
log('Received SIGINT (Ctrl+C), cleaning up containers...')
|
|
94
|
+
try {
|
|
95
|
+
await cleanupAllContainers()
|
|
96
|
+
log('All containers cleaned up')
|
|
97
|
+
} catch (e) {
|
|
98
|
+
log(`Cleanup error: ${e.message}`)
|
|
99
|
+
}
|
|
87
100
|
process.exit(0)
|
|
88
101
|
})
|
|
89
102
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Router } from 'express'
|
|
5
5
|
import Docker from 'dockerode'
|
|
6
|
-
import sandbox, { runPythonCode, checkImageExists, checkGPUAvailable, checkCUDACompatibility } from '../sandbox.js'
|
|
6
|
+
import sandbox, { runPythonCode, checkImageExists, checkGPUAvailable, checkCUDACompatibility, abortExecution } from '../sandbox.js'
|
|
7
7
|
|
|
8
8
|
const { SANDBOX_CONFIG } = sandbox
|
|
9
9
|
const docker = new Docker()
|
|
@@ -232,4 +232,31 @@ router.get('/cuda-compat', async (req, res) => {
|
|
|
232
232
|
}
|
|
233
233
|
})
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* 中止执行
|
|
237
|
+
* POST /api/sandbox/abort
|
|
238
|
+
* Body: { executionId?: string } // 可选,不传则中止所有
|
|
239
|
+
*/
|
|
240
|
+
router.post('/abort', async (req, res) => {
|
|
241
|
+
const { executionId = null } = req.body
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const result = await abortExecution(executionId)
|
|
245
|
+
|
|
246
|
+
res.json({
|
|
247
|
+
success: result.success,
|
|
248
|
+
stopped: result.stopped,
|
|
249
|
+
message: result.stopped > 0
|
|
250
|
+
? `已中止 ${result.stopped} 个执行任务`
|
|
251
|
+
: '没有正在运行的任务'
|
|
252
|
+
})
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('[Sandbox] Abort error:', error)
|
|
255
|
+
res.status(500).json({
|
|
256
|
+
success: false,
|
|
257
|
+
error: error.message || '中止失败'
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
235
262
|
export default router
|
package/src/server/sandbox.js
CHANGED
|
@@ -20,6 +20,91 @@ function log(message) {
|
|
|
20
20
|
// 启动时记录
|
|
21
21
|
log('Sandbox module initialized')
|
|
22
22
|
|
|
23
|
+
// ==================== 全局容器追踪 ====================
|
|
24
|
+
// 用于追踪所有活跃的容器,支持中止操作和优雅退出清理
|
|
25
|
+
const activeContainers = new Map()
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 生成唯一的执行 ID
|
|
29
|
+
* 格式: exec-{时间戳}-{随机后缀}
|
|
30
|
+
*/
|
|
31
|
+
function generateExecutionId() {
|
|
32
|
+
return `exec-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 注册容器到活跃列表
|
|
37
|
+
* @param {string} executionId - 执行 ID
|
|
38
|
+
* @param {Docker.Container} container - Docker 容器实例
|
|
39
|
+
*/
|
|
40
|
+
function registerContainer(executionId, container) {
|
|
41
|
+
activeContainers.set(executionId, container)
|
|
42
|
+
log(`Container registered: ${executionId} -> ${container.id}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 从活跃列表移除容器
|
|
47
|
+
* @param {string} executionId - 执行 ID
|
|
48
|
+
*/
|
|
49
|
+
function unregisterContainer(executionId) {
|
|
50
|
+
activeContainers.delete(executionId)
|
|
51
|
+
log(`Container unregistered: ${executionId}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 清理所有活跃容器
|
|
56
|
+
* 用于中止操作和进程退出时清理
|
|
57
|
+
* @returns {Promise<number>} 清理的容器数量
|
|
58
|
+
*/
|
|
59
|
+
export async function cleanupAllContainers() {
|
|
60
|
+
const count = activeContainers.size
|
|
61
|
+
log(`Cleaning up ${count} active containers...`)
|
|
62
|
+
|
|
63
|
+
for (const [executionId, container] of activeContainers) {
|
|
64
|
+
try {
|
|
65
|
+
await container.stop({ t: 5 }) // 5秒优雅停止
|
|
66
|
+
await container.remove({ force: true })
|
|
67
|
+
log(`Container stopped and removed: ${executionId}`)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
log(`Failed to cleanup container ${executionId}: ${e.message}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
activeContainers.clear()
|
|
74
|
+
log(`All containers cleaned up: ${count}`)
|
|
75
|
+
return count
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 中止指定执行或所有执行
|
|
80
|
+
* @param {string|null} executionId - 执行 ID,null 表示中止所有
|
|
81
|
+
* @returns {Promise<{success: boolean, stopped: number}>}
|
|
82
|
+
*/
|
|
83
|
+
export async function abortExecution(executionId = null) {
|
|
84
|
+
const containersToStop = executionId
|
|
85
|
+
? [activeContainers.get(executionId)].filter(c => c)
|
|
86
|
+
: Array.from(activeContainers.values())
|
|
87
|
+
|
|
88
|
+
log(`Aborting ${containersToStop.length} containers (executionId: ${executionId || 'all'})`)
|
|
89
|
+
|
|
90
|
+
let stopped = 0
|
|
91
|
+
for (const [id, container] of containersToStop.map(c => [activeContainers.entries().find(([k, v]) => v === c)?.[0], c])) {
|
|
92
|
+
if (container) {
|
|
93
|
+
try {
|
|
94
|
+
await container.stop({ t: 5 })
|
|
95
|
+
await container.remove({ force: true })
|
|
96
|
+
activeContainers.delete(id)
|
|
97
|
+
stopped++
|
|
98
|
+
log(`Container aborted: ${id}`)
|
|
99
|
+
} catch (e) {
|
|
100
|
+
log(`Failed to abort container ${id}: ${e.message}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { success: true, stopped }
|
|
106
|
+
}
|
|
107
|
+
|
|
23
108
|
// 检测运行模式并计算正确的路径
|
|
24
109
|
// 开发模式: 从 local-server/src 运行,项目根目录在上两级
|
|
25
110
|
// 独立模式: 从 packages/cli/src/server 运行,无 shared_modules 目录
|
|
@@ -123,6 +208,11 @@ function getProgressReporterPath() {
|
|
|
123
208
|
if (process.env.PROGRESS_REPORTER_PATH) {
|
|
124
209
|
return process.env.PROGRESS_REPORTER_PATH
|
|
125
210
|
}
|
|
211
|
+
// --dev 模式下,尝试从 KERNEL_RUNNER_PATH 推断(同一目录)
|
|
212
|
+
if (process.env.KERNEL_RUNNER_PATH) {
|
|
213
|
+
const kernelDir = path.dirname(process.env.KERNEL_RUNNER_PATH)
|
|
214
|
+
return path.join(kernelDir, 'dmla_progress.py')
|
|
215
|
+
}
|
|
126
216
|
return DEFAULT_PROGRESS_REPORTER_PATH
|
|
127
217
|
}
|
|
128
218
|
|
|
@@ -140,9 +230,15 @@ function shouldMountSharedModules() {
|
|
|
140
230
|
|
|
141
231
|
/**
|
|
142
232
|
* 检查是否挂载本地 kernel_runner.py(开发模式)
|
|
233
|
+
* 条件1: MOUNT_KERNEL_RUNNER 不是 'false'(默认启用)
|
|
234
|
+
* 条件2: 有可用的路径(PROJECT_ROOT 或环境变量指定)
|
|
143
235
|
*/
|
|
144
236
|
function shouldMountKernelRunner() {
|
|
145
|
-
|
|
237
|
+
if (process.env.MOUNT_KERNEL_RUNNER === 'false') {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
// 有环境变量指定的路径,或者有 PROJECT_ROOT
|
|
241
|
+
return process.env.KERNEL_RUNNER_PATH || PROJECT_ROOT !== null
|
|
146
242
|
}
|
|
147
243
|
|
|
148
244
|
/**
|
|
@@ -320,12 +416,15 @@ print(json.dumps(result))
|
|
|
320
416
|
export async function runPythonCode(code, useGpu = false, imageOverride = null, timeoutOverride = null) {
|
|
321
417
|
const startTime = Date.now()
|
|
322
418
|
|
|
419
|
+
// 生成唯一执行 ID
|
|
420
|
+
const executionId = generateExecutionId()
|
|
421
|
+
|
|
323
422
|
// 计算实际超时时间
|
|
324
423
|
const actualTimeout = timeoutOverride === null
|
|
325
424
|
? null // unlimited
|
|
326
425
|
: (timeoutOverride || Math.floor(SANDBOX_CONFIG.timeout / 1000))
|
|
327
426
|
|
|
328
|
-
log(`runPythonCode called, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}, timeout=${actualTimeout === null ? 'unlimited' : actualTimeout}`)
|
|
427
|
+
log(`runPythonCode called, executionId=${executionId}, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}, timeout=${actualTimeout === null ? 'unlimited' : actualTimeout}`)
|
|
329
428
|
|
|
330
429
|
// 选择镜像:优先使用指定的镜像,否则根据 useGpu 选择
|
|
331
430
|
const image = imageOverride || (useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu)
|
|
@@ -372,7 +471,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
372
471
|
traceback: [errorMessage]
|
|
373
472
|
}],
|
|
374
473
|
executionTime,
|
|
375
|
-
gpuUsed: false
|
|
474
|
+
gpuUsed: false,
|
|
475
|
+
executionId
|
|
376
476
|
}
|
|
377
477
|
}
|
|
378
478
|
log('CUDA compatibility check passed')
|
|
@@ -471,6 +571,9 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
471
571
|
container = await docker.createContainer(containerConfig)
|
|
472
572
|
log(`Container created: ${container.id}`)
|
|
473
573
|
|
|
574
|
+
// 注册到活跃容器列表
|
|
575
|
+
registerContainer(executionId, container)
|
|
576
|
+
|
|
474
577
|
// 设置超时(使用动态计算的超时时间,unlimited 时为 24 小时)
|
|
475
578
|
const containerTimeoutMs = timeoutSeconds * 1000 + 10000 // 转换为毫秒,额外 10 秒用于清理
|
|
476
579
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -528,7 +631,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
528
631
|
traceback: [stdout.substring(0, 1000)]
|
|
529
632
|
}],
|
|
530
633
|
executionTime,
|
|
531
|
-
gpuUsed: useGpu
|
|
634
|
+
gpuUsed: useGpu,
|
|
635
|
+
executionId
|
|
532
636
|
}
|
|
533
637
|
}
|
|
534
638
|
|
|
@@ -553,7 +657,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
553
657
|
traceback: [rawOutput]
|
|
554
658
|
}],
|
|
555
659
|
executionTime,
|
|
556
|
-
gpuUsed: useGpu
|
|
660
|
+
gpuUsed: useGpu,
|
|
661
|
+
executionId
|
|
557
662
|
}
|
|
558
663
|
}
|
|
559
664
|
|
|
@@ -561,7 +666,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
561
666
|
success: parsedResult.success,
|
|
562
667
|
outputs: parsedResult.outputs || [],
|
|
563
668
|
executionTime: parsedResult.executionTime || (Date.now() - startTime) / 1000,
|
|
564
|
-
gpuUsed: useGpu
|
|
669
|
+
gpuUsed: useGpu,
|
|
670
|
+
executionId
|
|
565
671
|
}
|
|
566
672
|
|
|
567
673
|
} catch (error) {
|
|
@@ -581,10 +687,14 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
581
687
|
traceback: [error.message || 'Unknown error']
|
|
582
688
|
}],
|
|
583
689
|
executionTime,
|
|
584
|
-
gpuUsed: useGpu
|
|
690
|
+
gpuUsed: useGpu,
|
|
691
|
+
executionId
|
|
585
692
|
}
|
|
586
693
|
|
|
587
694
|
} finally {
|
|
695
|
+
// 从活跃列表移除
|
|
696
|
+
unregisterContainer(executionId)
|
|
697
|
+
|
|
588
698
|
// 清理容器
|
|
589
699
|
log('Cleaning up container...')
|
|
590
700
|
if (container) {
|
|
@@ -720,5 +830,7 @@ export default {
|
|
|
720
830
|
checkCUDACompatibility,
|
|
721
831
|
checkImageExists,
|
|
722
832
|
pullImage,
|
|
833
|
+
cleanupAllContainers,
|
|
834
|
+
abortExecution,
|
|
723
835
|
SANDBOX_CONFIG
|
|
724
836
|
}
|
package/version.json
CHANGED