@icyfenix-dmla/cli 2026.5.3-1019 → 2026.5.3-1103
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/server/index.js +18 -5
- package/src/server/routes/sandbox.js +28 -1
- package/src/server/sandbox.js +107 -6
- package/version.json +2 -2
package/package.json
CHANGED
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 目录
|
|
@@ -320,12 +405,15 @@ print(json.dumps(result))
|
|
|
320
405
|
export async function runPythonCode(code, useGpu = false, imageOverride = null, timeoutOverride = null) {
|
|
321
406
|
const startTime = Date.now()
|
|
322
407
|
|
|
408
|
+
// 生成唯一执行 ID
|
|
409
|
+
const executionId = generateExecutionId()
|
|
410
|
+
|
|
323
411
|
// 计算实际超时时间
|
|
324
412
|
const actualTimeout = timeoutOverride === null
|
|
325
413
|
? null // unlimited
|
|
326
414
|
: (timeoutOverride || Math.floor(SANDBOX_CONFIG.timeout / 1000))
|
|
327
415
|
|
|
328
|
-
log(`runPythonCode called, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}, timeout=${actualTimeout === null ? 'unlimited' : actualTimeout}`)
|
|
416
|
+
log(`runPythonCode called, executionId=${executionId}, useGpu=${useGpu}, code length=${code.length}, imageOverride=${imageOverride}, timeout=${actualTimeout === null ? 'unlimited' : actualTimeout}`)
|
|
329
417
|
|
|
330
418
|
// 选择镜像:优先使用指定的镜像,否则根据 useGpu 选择
|
|
331
419
|
const image = imageOverride || (useGpu ? SANDBOX_CONFIG.imageGpu : SANDBOX_CONFIG.imageCpu)
|
|
@@ -372,7 +460,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
372
460
|
traceback: [errorMessage]
|
|
373
461
|
}],
|
|
374
462
|
executionTime,
|
|
375
|
-
gpuUsed: false
|
|
463
|
+
gpuUsed: false,
|
|
464
|
+
executionId
|
|
376
465
|
}
|
|
377
466
|
}
|
|
378
467
|
log('CUDA compatibility check passed')
|
|
@@ -471,6 +560,9 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
471
560
|
container = await docker.createContainer(containerConfig)
|
|
472
561
|
log(`Container created: ${container.id}`)
|
|
473
562
|
|
|
563
|
+
// 注册到活跃容器列表
|
|
564
|
+
registerContainer(executionId, container)
|
|
565
|
+
|
|
474
566
|
// 设置超时(使用动态计算的超时时间,unlimited 时为 24 小时)
|
|
475
567
|
const containerTimeoutMs = timeoutSeconds * 1000 + 10000 // 转换为毫秒,额外 10 秒用于清理
|
|
476
568
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -528,7 +620,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
528
620
|
traceback: [stdout.substring(0, 1000)]
|
|
529
621
|
}],
|
|
530
622
|
executionTime,
|
|
531
|
-
gpuUsed: useGpu
|
|
623
|
+
gpuUsed: useGpu,
|
|
624
|
+
executionId
|
|
532
625
|
}
|
|
533
626
|
}
|
|
534
627
|
|
|
@@ -553,7 +646,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
553
646
|
traceback: [rawOutput]
|
|
554
647
|
}],
|
|
555
648
|
executionTime,
|
|
556
|
-
gpuUsed: useGpu
|
|
649
|
+
gpuUsed: useGpu,
|
|
650
|
+
executionId
|
|
557
651
|
}
|
|
558
652
|
}
|
|
559
653
|
|
|
@@ -561,7 +655,8 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
561
655
|
success: parsedResult.success,
|
|
562
656
|
outputs: parsedResult.outputs || [],
|
|
563
657
|
executionTime: parsedResult.executionTime || (Date.now() - startTime) / 1000,
|
|
564
|
-
gpuUsed: useGpu
|
|
658
|
+
gpuUsed: useGpu,
|
|
659
|
+
executionId
|
|
565
660
|
}
|
|
566
661
|
|
|
567
662
|
} catch (error) {
|
|
@@ -581,10 +676,14 @@ export async function runPythonCode(code, useGpu = false, imageOverride = null,
|
|
|
581
676
|
traceback: [error.message || 'Unknown error']
|
|
582
677
|
}],
|
|
583
678
|
executionTime,
|
|
584
|
-
gpuUsed: useGpu
|
|
679
|
+
gpuUsed: useGpu,
|
|
680
|
+
executionId
|
|
585
681
|
}
|
|
586
682
|
|
|
587
683
|
} finally {
|
|
684
|
+
// 从活跃列表移除
|
|
685
|
+
unregisterContainer(executionId)
|
|
686
|
+
|
|
588
687
|
// 清理容器
|
|
589
688
|
log('Cleaning up container...')
|
|
590
689
|
if (container) {
|
|
@@ -720,5 +819,7 @@ export default {
|
|
|
720
819
|
checkCUDACompatibility,
|
|
721
820
|
checkImageExists,
|
|
722
821
|
pullImage,
|
|
822
|
+
cleanupAllContainers,
|
|
823
|
+
abortExecution,
|
|
723
824
|
SANDBOX_CONFIG
|
|
724
825
|
}
|
package/version.json
CHANGED