Fix: job ids from one source with an exclusive mkdir, an integer seed at the door, Blender handed a private copy and its FBX created exclusively (Soul s1: job-id-collision, seed-recorded-not-used, bvh-swap-after-check)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-24 23:19:16 +02:00
commit ea7ce34d90
2 changed files with 113 additions and 42 deletions

View file

@ -1360,10 +1360,16 @@ test("an fbx job exports every listed BVH", async () => {
const [probe, run] = daemon.starts();
assert.deepEqual(probe.argv, ["-b", "--factory-startup", "--version"]);
assert.deepEqual(run.argv, [
"-b", "--factory-startup", "--python-use-system-env", "--python-exit-code", "1", "--python", exportScriptPath,
"--", path.join(job.artifactDir, "motion.bvh"), path.join(job.artifactDir, "motion.fbx"), "wave_01",
assert.deepEqual(run.argv.slice(0, 8), [
"-b", "--factory-startup", "--python-use-system-env", "--python-exit-code", "1", "--python", exportScriptPath, "--",
]);
// Blender reads a private copy and writes a private FBX, in a directory made for this export alone: it is never
// handed a path in the job's directory, and the directory is gone once the export is over.
const [privateBvh, privateFbx, claimed] = run.argv.slice(8);
assert.match(path.relative(daemon.stateRoot, privateBvh), /^work\/export-[A-Za-z0-9]{6}\/in\.bvh$/);
assert.equal(privateFbx, path.join(path.dirname(privateBvh), "out.fbx"));
assert.equal(claimed, "wave_01");
assert.deepEqual(readdirSync(path.join(daemon.stateRoot, "work")), []);
assert.deepEqual(run.env, {
PATH: process.env.PATH,
LANG: "xx_TEST.UTF-8",
@ -1651,7 +1657,7 @@ test("rest-fbx is a job whose FBX is readable", async () => {
const answer = await request(daemon.port, "POST", "/motion/rest-fbx", { body: "{}", headers: { "content-type": "application/json" } });
assert.equal(answer.status, 200);
const job = answer.body;
assert.match(job.jobId, /^rest-[0-9]{13}$/);
assert.match(job.jobId, /^rest-[0-9]{13}-[0-9a-f]{8}$/);
assert.equal(job.schema, "gamecult.vili.motion_job.v1");
assert.deepEqual(job.export.clips, [{ file: "SOMA77_Rest.fbx", clipName: "SOMA77_Rest", bones: 78, frames: [1, 60], fps: 30 }]);
assert.deepEqual(job.files.map((file) => file.name), ["SOMA77_Rest.bvh", "SOMA77_Rest.fbx"]);

View file

@ -1,9 +1,9 @@
#!/usr/bin/env node
import http from "node:http";
import os from "node:os";
import { createHash, randomInt } from "node:crypto";
import { closeSync, lstatSync, openSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { createHash, randomBytes, randomInt } from "node:crypto";
import { closeSync, constants, lstatSync, openSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
import { copyFile, mkdir, mkdtemp, open, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import readline from "node:readline";
import net from "node:net";
@ -564,44 +564,88 @@ async function probeBlender() {
blender.version = run.code === 0 && run.first?.startsWith("Blender ") ? run.first : null;
}
// One BVH to one FBX beside it. The FBX counts only if Blender exits 0, prints its result line and the file exists.
// A failure names the exit or the signal and never Blender's output.
async function exportClip(bvh, fbx, clipName, timeoutMs) {
const run = await runBlender([
"-b", "--factory-startup", "--python-use-system-env", "--python-exit-code", "1",
"--python", exportScript, "--", bvh, fbx, clipName,
], timeoutMs);
let info = null;
if (run.code === 0 && run.result !== null) {
try {
const parsed = JSON.parse(run.result);
if (Number.isInteger(parsed.bones) && Array.isArray(parsed.frames) && parsed.frames.length === 2
&& parsed.frames.every(Number.isInteger) && Number.isFinite(parsed.fps)) info = parsed;
} catch {
// not the result line
}
// The bytes of a regular file strictly inside `directory`, or null. The file is opened without following a symlink
// and the open file is the one checked, so a path swapped after the check cannot redirect the read.
async function readInside(file, directory) {
let handle = null;
try {
handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
const opened = await handle.stat();
if (!opened.isFile() || !regularFileInside(file, directory)) return null;
const named = lstatSync(file);
if (named.dev !== opened.dev || named.ino !== opened.ino) return null;
return await handle.readFile();
} catch {
return null;
} finally {
await handle?.close();
}
}
// One BVH to one FBX beside it. Blender never sees a path in the job's artifact directory: it reads a private copy of
// the BVH and writes a private FBX in a directory made for this export alone, and the FBX is then created beside the
// BVH exclusively, so nothing planted at either path is read, written through or replaced. The FBX counts only if
// Blender exits 0, prints its result line and its file exists. A failure names the exit or the signal and never
// Blender's output.
async function exportClip(bvh, fbx, clipName, timeoutMs, artifactDir) {
const input = await readInside(bvh, artifactDir);
if (input === null) return { error: "fbx-export-refused" };
guard();
await mkdir(path.join(args.stateRoot, "work"), { recursive: true });
guard();
const work = await mkdtemp(path.join(args.stateRoot, "work", "export-"));
try {
const privateBvh = path.join(work, "in.bvh");
const privateFbx = path.join(work, "out.fbx");
guard();
await writeFile(privateBvh, input, { flag: "wx" });
const run = await runBlender([
"-b", "--factory-startup", "--python-use-system-env", "--python-exit-code", "1",
"--python", exportScript, "--", privateBvh, privateFbx, clipName,
], timeoutMs);
let info = null;
if (run.code === 0 && run.result !== null) {
try {
const parsed = JSON.parse(run.result);
if (Number.isInteger(parsed.bones) && Array.isArray(parsed.frames) && parsed.frames.length === 2
&& parsed.frames.every(Number.isInteger) && Number.isFinite(parsed.fps)) info = parsed;
} catch {
// not the result line
}
}
const made = info ? await readInside(privateFbx, work) : null;
if (made !== null) {
guard();
try {
await writeFile(fbx, made, { flag: "wx" });
return { clip: { file: fbx, clipName, bones: info.bones, frames: info.frames, fps: info.fps } };
} catch (error) {
if (error instanceof LeaseLost) throw error;
return { error: "fbx-export-refused" };
}
}
const how = run.code === 0 ? "no result" : run.code !== null ? `exit ${run.code}` : `signal ${run.signal ?? "none"}`;
return { error: `fbx-export-failed (${how})` };
} finally {
if (mayWrite()) await rm(work, { recursive: true, force: true });
}
if (info && isRegularFile(fbx)) return { clip: { file: fbx, clipName, bones: info.bones, frames: info.frames, fps: info.fps } };
const how = run.code === 0 ? "no result" : run.code !== null ? `exit ${run.code}` : `signal ${run.signal ?? "none"}`;
return { error: `fbx-export-failed (${how})` };
}
// Every BVH the worker listed becomes an FBX, or the job fails. A listed BVH must be a regular file inside the job's
// artifact directory, and all are checked before Blender starts for any. Sample N of several is <clipName>_NN.
// artifact directory, and that is decided when its export begins (exportClip), never earlier. Sample N of several is
// <clipName>_NN.
async function exportListed(listed, artifactDir, clipName, timeoutMs) {
const bvhs = [];
for (const file of Array.isArray(listed) ? listed : []) {
if (typeof file !== "string" || !file.endsWith(".bvh")) continue;
const full = path.resolve(artifactDir, file);
if (!regularFileInside(full, artifactDir)) return { clips: [], error: "fbx-export-refused" };
bvhs.push(full);
bvhs.push(path.resolve(artifactDir, file));
}
if (bvhs.length === 0) return { clips: [], error: "fbx-export-failed (no bvh)" };
bvhs.sort();
const clips = [];
for (const [index, bvh] of bvhs.entries()) {
const name = bvhs.length === 1 ? clipName : `${clipName}_${String(index).padStart(2, "0")}`;
const done = await exportClip(bvh, `${bvh.slice(0, -".bvh".length)}.fbx`, name, timeoutMs);
const done = await exportClip(bvh, `${bvh.slice(0, -".bvh".length)}.fbx`, name, timeoutMs, artifactDir);
if (done.error) return { clips, error: done.error };
clips.push({ ...done.clip, file: path.relative(artifactDir, done.clip.file).split(path.sep).join("/") });
}
@ -751,6 +795,28 @@ async function underLease(response, run) {
}
}
// A seed is an integer Kimodo can take: the worker seeds with int(seed), so the daemon records exactly what it sends.
const SEED_LIMIT = 2 ** 31;
// The one source of job ids, for a generate job and a rest job alike: the clock for order, random bits for
// uniqueness, and an exclusive mkdir that decides, so two jobs never share a directory, a record or a file.
async function newJobDir(prefix) {
guard();
await mkdir(path.join(args.stateRoot, "artifacts"), { recursive: true });
for (let attempt = 0; attempt < 8; attempt += 1) {
const jobId = `${prefix}-${Date.now()}-${randomBytes(4).toString("hex")}`;
const artifactDir = path.join(args.stateRoot, "artifacts", jobId);
guard();
try {
await mkdir(artifactDir);
return { jobId, artifactDir };
} catch (error) {
if (error.code !== "EEXIST") throw error;
}
}
throw new Error("no free job id");
}
async function generate(request, response) {
guard();
if (workerUnconfigured()) {
@ -797,21 +863,23 @@ async function generate(request, response) {
bvhStandardTpose: fbx || boolValue(askedTpose),
};
const jobId = `vili-${Date.now()}`;
const clipName = fbx ? (body.clipName ?? jobId) : undefined;
const artifactDir = path.join(args.stateRoot, "artifacts", jobId);
guard();
await mkdir(artifactDir, { recursive: true });
const duration = firstDefined(body.durationSeconds, body.duration, 4);
const bodyMeta = typeof body.meta === "object" && body.meta ? body.meta : {};
const askedSeed = firstDefined(body.seed, bodyMeta.seed);
if (askedSeed !== undefined && !(Number.isInteger(askedSeed) && askedSeed >= 0 && askedSeed < SEED_LIMIT)) {
json(response, 400, { ok: false, error: "Invalid seed." });
return;
}
const { jobId, artifactDir } = await newJobDir("vili");
const clipName = fbx ? (body.clipName ?? jobId) : undefined;
const duration = firstDefined(body.durationSeconds, body.duration, 4);
const diffusionSteps = Math.max(1, Number(firstDefined(
body.diffusionSteps,
body.diffusion_steps,
bodyMeta.diffusion_steps,
20,
)));
const askedSeed = Number(firstDefined(body.seed, bodyMeta.seed));
const seed = Number.isFinite(askedSeed) ? askedSeed : randomInt(0, 2 ** 31);
const seed = askedSeed ?? randomInt(0, SEED_LIMIT);
const numSamples = Math.max(1, Number(firstDefined(
body.numSamples,
body.num_samples,
@ -944,10 +1012,7 @@ async function restFbx(response) {
json(response, 503, { ok: false, code: "rest-skeleton-missing" });
return;
}
const jobId = `rest-${Date.now()}`;
const artifactDir = path.join(args.stateRoot, "artifacts", jobId);
guard();
await mkdir(artifactDir, { recursive: true });
const { jobId, artifactDir } = await newJobDir("rest");
guard();
await copyFile(restBvh, path.join(artifactDir, "SOMA77_Rest.bvh"));
const exported = await exportListed([path.join(artifactDir, "SOMA77_Rest.bvh")], artifactDir, "SOMA77_Rest", EXPORT_TIMEOUT_MS);
@ -970,7 +1035,7 @@ async function restFbx(response) {
}
// Reads only: a job's own FBX and BVH files, by exact name, regular files inside the job's artifact directory.
const ARTIFACT_ROUTE = /^\/motion\/artifacts\/((?:vili|rest)-[0-9]{13})\/(motion\.fbx|motion\.bvh|SOMA77_Rest\.fbx|motion\/motion_[0-9]{2}\.(?:fbx|bvh))$/;
const ARTIFACT_ROUTE = /^\/motion\/artifacts\/((?:vili|rest)-[0-9]{13}-[0-9a-f]{8})\/(motion\.fbx|motion\.bvh|SOMA77_Rest\.fbx|motion\/motion_[0-9]{2}\.(?:fbx|bvh))$/;
async function serveArtifact(response, jobId, name) {
const directory = path.join(args.stateRoot, "artifacts", jobId);