import { spawn, execFileSync } from 'child_process'; import { mkdirSync, writeFileSync, createReadStream, statSync, unlinkSync } from 'node:fs'; import { dirname } from 'node:path'; import { v4 as uuidv4 } from 'uuid'; import { createUploadStream } from './s3/client.js'; /** * Reads stderr lines from a spawned process until it finds a JSON line. * Non-JSON lines (e.g. [board] log messages from the bridge) are logged * and skipped. Resolves with the parsed JSON object when a JSON line arrives. * Rejects if the process exits before emitting JSON, or if timeoutMs elapses. */ function readFirstStderrLine(proc, timeoutMs = 35_000) { return new Promise((resolve, reject) => { let buf = ''; let settled = false; const settle = (fn) => { if (settled) return; settled = true; fn(); }; const timer = setTimeout(() => { settle(() => reject(new Error(`deltacast-capture: timed out waiting for format JSON after ${timeoutMs}ms`))); }, timeoutMs); proc.stderr.setEncoding('utf8'); proc.stderr.on('data', (chunk) => { buf += chunk; let nl; // Process all complete lines in the buffer while ((nl = buf.indexOf('\n')) !== -1) { const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); if (!line) continue; // Skip non-JSON log lines emitted by the bridge (e.g. "[board] waiting...") if (!line.startsWith('{')) { console.error(`[deltacast-bridge] ${line}`); continue; } clearTimeout(timer); try { const parsed = JSON.parse(line); if (parsed.error) { settle(() => reject(new Error(`deltacast-capture: ${parsed.error}`))); } else { settle(() => resolve(parsed)); } } catch (e) { settle(() => reject(new Error(`deltacast-capture: invalid JSON on stderr: ${line}`))); } return; } }); proc.on('exit', (code) => { clearTimeout(timer); settle(() => reject(new Error(`deltacast-capture: exited with code ${code} before emitting format JSON`))); }); }); }