@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icyfenix-dmla/cli",
3
- "version": "2026.5.3-1019",
3
+ "version": "2026.5.3-1103",
4
4
  "description": "DMLA 沙箱服务命令行工具",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
@@ -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
@@ -1,4 +1,4 @@
1
1
  {
2
- "buildTime": "2026-05-03T02:20:30.472Z",
3
- "cliVersion": "2026.5.3-1019"
2
+ "buildTime": "2026-05-03T03:03:40.325Z",
3
+ "cliVersion": "2026.5.3-1103"
4
4
  }