@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icyfenix-dmla/cli",
3
- "version": "2026.5.3-1019",
3
+ "version": "2026.5.3-1248",
4
4
  "description": "DMLA 沙箱服务命令行工具",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
  // 写入启动日志
@@ -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 目录
@@ -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
- return process.env.MOUNT_KERNEL_RUNNER !== 'false' && PROJECT_ROOT !== null
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
@@ -1,4 +1,4 @@
1
1
  {
2
- "buildTime": "2026-05-03T02:20:30.472Z",
3
- "cliVersion": "2026.5.3-1019"
2
+ "buildTime": "2026-05-03T04:48:50.733Z",
3
+ "cliVersion": "2026.5.3-1248"
4
4
  }