Spaces:
Running
Running
| import { spawn } from 'child_process' | |
| import { createLogger } from './logger' | |
| import { | |
| registerManimProcess, | |
| unregisterManimProcess, | |
| wasManimProcessCancelled | |
| } from './manim-process-registry' | |
| import { | |
| buildManimArgs, | |
| buildResult, | |
| createExecutionState, | |
| elapsedSeconds, | |
| handleStderrData, | |
| handleStdoutData, | |
| normalizeExecuteOptions, | |
| startMemoryMonitor | |
| } from './manim-executor-runtime' | |
| const logger = createLogger('ManimExecutor') | |
| export interface ManimExecutionResult { | |
| success: boolean | |
| stdout: string | |
| stderr: string | |
| peakMemoryMB: number | |
| exitCode?: number | |
| } | |
| export interface ManimExecuteOptions { | |
| jobId: string | |
| quality: string | |
| frameRate?: number | |
| format?: 'mp4' | 'png' | |
| sceneName?: string | |
| tempDir: string | |
| mediaDir: string | |
| timeoutMs?: number | |
| } | |
| export function executeManimCommand( | |
| codeFile: string, | |
| options: ManimExecuteOptions | |
| ): Promise<ManimExecutionResult> { | |
| const normalizedOptions = normalizeExecuteOptions(options) | |
| const args = buildManimArgs(codeFile, normalizedOptions) | |
| logger.info(`Job ${normalizedOptions.jobId}: starting manim process`, { | |
| command: `manim ${args.join(' ')}`, | |
| cwd: normalizedOptions.tempDir | |
| }) | |
| return new Promise((resolve) => { | |
| const startTime = Date.now() | |
| const state = createExecutionState() | |
| const proc = spawn('manim', args, { cwd: normalizedOptions.tempDir }) | |
| registerManimProcess(normalizedOptions.jobId, proc) | |
| const memoryMonitor = startMemoryMonitor(proc, normalizedOptions, state) | |
| let timeoutTimer: NodeJS.Timeout | null = null | |
| let settled = false | |
| const settle = (result: ManimExecutionResult): void => { | |
| if (settled) { | |
| return | |
| } | |
| settled = true | |
| if (timeoutTimer) { | |
| clearTimeout(timeoutTimer) | |
| } | |
| clearInterval(memoryMonitor) | |
| unregisterManimProcess(normalizedOptions.jobId) | |
| resolve(result) | |
| } | |
| proc.stdout.on('data', (data) => { | |
| handleStdoutData(state, normalizedOptions.jobId, data.toString()) | |
| }) | |
| proc.stderr.on('data', (data) => { | |
| handleStderrData(state, normalizedOptions.jobId, data.toString()) | |
| }) | |
| timeoutTimer = setTimeout(() => { | |
| const elapsed = elapsedSeconds(startTime) | |
| logger.warn(`Job ${normalizedOptions.jobId}: manim render timeout (${elapsed}s), killing process`, { | |
| peakMemoryMB: state.peakMemoryMB | |
| }) | |
| proc.kill('SIGKILL') | |
| settle( | |
| buildResult( | |
| false, | |
| state, | |
| state.stderr || `Manim render timeout (${Math.round(normalizedOptions.timeoutMs / 1000)} seconds)` | |
| ) | |
| ) | |
| }, normalizedOptions.timeoutMs) | |
| proc.on('close', (code) => { | |
| const elapsed = elapsedSeconds(startTime) | |
| const cancelled = wasManimProcessCancelled(normalizedOptions.jobId) | |
| if (cancelled) { | |
| logger.warn(`Job ${normalizedOptions.jobId}: Manim cancelled`, { elapsed: `${elapsed}s` }) | |
| settle(buildResult(false, state, 'Job cancelled', code ?? undefined)) | |
| return | |
| } | |
| if (code === 0) { | |
| logger.info(`Job ${normalizedOptions.jobId}: manim completed`, { | |
| elapsed: `${elapsed}s`, | |
| exitCode: code, | |
| stdoutLength: state.stdout.length, | |
| stderrLength: state.stderr.length, | |
| peakMemoryMB: state.peakMemoryMB | |
| }) | |
| settle(buildResult(true, state, undefined, code ?? undefined)) | |
| return | |
| } | |
| logger.error(`Job ${normalizedOptions.jobId}: manim exited with error`, { | |
| elapsed: `${elapsed}s`, | |
| exitCode: code, | |
| stdoutLength: state.stdout.length, | |
| stderrLength: state.stderr.length, | |
| stderrPreview: state.stderr.slice(-500), | |
| peakMemoryMB: state.peakMemoryMB | |
| }) | |
| settle(buildResult(false, state, undefined, code ?? undefined)) | |
| }) | |
| proc.on('error', (error) => { | |
| const elapsed = elapsedSeconds(startTime) | |
| const cancelled = wasManimProcessCancelled(normalizedOptions.jobId) | |
| if (cancelled) { | |
| logger.warn(`Job ${normalizedOptions.jobId}: Manim cancelled`, { elapsed: `${elapsed}s` }) | |
| settle(buildResult(false, state, 'Job cancelled')) | |
| return | |
| } | |
| logger.error(`Job ${normalizedOptions.jobId}: manim process start failed`, { | |
| elapsed: `${elapsed}s`, | |
| errorMessage: error.message, | |
| errorStack: error.stack | |
| }) | |
| settle(buildResult(false, state, error.message)) | |
| }) | |
| }) | |
| } | |