import { spawn, execFile, type ChildProcess } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); export const bridgePython = process.env.BRIDGE_PYTHON ?? (existsSync(resolve('.venv/bin/python')) ? resolve('.venv/bin/python') : 'python3'); export async function stopProcess(child: ChildProcess): Promise { if (child.exitCode !== null || child.signalCode !== null) return; await new Promise((resolve) => { const timer = setTimeout(() => child.kill('SIGKILL'), 3000); child.once('exit', () => { clearTimeout(timer); resolve(); }); child.kill('SIGTERM'); }); } export async function startBridge() { const token = randomBytes(32).toString('hex'); const child = spawn(bridgePython, ['-m', 'mujoco_control_bridge', '--port', '0'], { env: { ...process.env, PYTHONPATH: '', PYTHONNOUSERSITE: '1', MUJOCO_CONTROL_TOKEN: token }, stdio: ['ignore', 'pipe', 'pipe'], }); let output = '', errors = ''; child.stderr.on('data', (chunk: Buffer) => { errors = (errors + chunk.toString()).slice(-65536); }); try { const endpoint = await new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error(`桥接启动超时:${errors}`)), 15000); child.once('error', (error) => { clearTimeout(timeout); reject(error); }); child.once('exit', () => { clearTimeout(timeout); reject(new Error(`桥接启动失败:${errors}`)); }); child.stdout.on('data', (chunk: Buffer) => { output += chunk.toString(); const ready = output.match(/CONTROL_BRIDGE_READY (\{[^\n]+\})/); if (ready) { clearTimeout(timeout); resolve((JSON.parse(ready[1]) as { endpoint: string }).endpoint); } }); }); return { endpoint, token, stop: () => stopProcess(child), errors: () => errors, python: async ( script: string, python = bridgePython, environment: Record = {}, ) => { const { stdout, stderr } = await execFileAsync(python, ['-c', script], { env: { ...process.env, PYTHONPATH: '', PYTHONNOUSERSITE: '1', ...environment, MUJOCO_CONTROL_TOKEN: token, MUJOCO_CONTROL_ENDPOINT: endpoint, }, timeout: 120000, maxBuffer: 4 * 1024 * 1024, }); return { stdout, stderr }; }, }; } catch (error) { await stopProcess(child); throw error; } }