70cf45ead4
Deploy / deploy (push) Successful in 7m2s
Blender work is not a shot submission: no Export, no version, no validation, QC or delivery. It is "Ctrl+F12, but on the farm", so it is modelled as what it is — a queue entry and nothing more. That turned out to need very little. Claim, progress, fail and complete-render already tolerated a job with no Export behind them, so a Blender job reuses the whole lifecycle; RenderJob gains projectId, label and submittedBy so a job with no shot code still has something to show. Capability gating is new and was already missing: the claim now filters job types by the machine's reported tools. Without it a workstation without Blender would claim Blender work and fail it repeatedly, burning the attempts while a machine that could have rendered it sat idle. The worker asks for Blender jobs unconditionally and the server refuses them — trusting the worker to self-select would have left the same hole. The runner treats a clean exit with no saved frames as a failure: Blender exits 0 when a frame range is empty or an output path is unwritable, and a job that silently rendered nothing is the worst outcome available. Standalone jobs auto-requeue on a transient failure like any other work; delivery builds are deliberately excluded, since a half-written package wants a human first. Adds the Blender add-on (BlenderPanel/) and its docs. The add-on refuses to submit an unsaved file — the farm opens the .blend from disk, so unsaved changes would render the previous state without warning. Its payload is pinned by a contract test, since add-on and server only ever meet over JSON. Worker 1.5.0. Adds migration 20260815000000_blender_render_jobs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
69 lines
2.3 KiB
C#
69 lines
2.3 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace VFXReviewWorker;
|
|
|
|
/// <summary>
|
|
/// Parses `blender -b` stdout. Blender reports the frame it is working on in
|
|
/// its status lines ("Fra:1042 Mem:512.00M ...") and confirms each written file
|
|
/// with "Saved: '...'". The saved lines are the reliable signal — a frame can
|
|
/// be reported for a long time while it renders — so progress counts those and
|
|
/// falls back to Fra: for a first sign of life.
|
|
/// </summary>
|
|
public static partial class BlenderProgressParser
|
|
{
|
|
[GeneratedRegex(@"^Fra:(\d+)", RegexOptions.Compiled)]
|
|
private static partial Regex FrameLine();
|
|
|
|
[GeneratedRegex(@"^Saved:\s*'(.+)'", RegexOptions.Compiled)]
|
|
private static partial Regex SavedLine();
|
|
|
|
[GeneratedRegex(@"^(Error|Warning): |^Error: ", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
|
|
private static partial Regex ErrorLine();
|
|
|
|
/// <summary>
|
|
/// Failures that will repeat identically on another machine — a missing
|
|
/// file, an unknown scene, an unwritable output. Retrying these burns the
|
|
/// queue for nothing; a human has to fix the .blend or the path.
|
|
/// </summary>
|
|
private static readonly string[] NonRetryablePatterns =
|
|
{
|
|
"cannot read file",
|
|
"file not found",
|
|
"unable to open",
|
|
"is not a blend file",
|
|
"failed to read blend file",
|
|
"unknown scene",
|
|
"scene not found",
|
|
"unable to create directory",
|
|
"cannot write file",
|
|
"no camera",
|
|
"python script fail",
|
|
"unknown argument",
|
|
};
|
|
|
|
/// <summary>The frame Blender is currently working on, or null.</summary>
|
|
public static int? ParseFrame(string line)
|
|
{
|
|
var m = FrameLine().Match(line);
|
|
return m.Success && int.TryParse(m.Groups[1].Value, out var f) ? f : null;
|
|
}
|
|
|
|
/// <summary>The path of a frame Blender has just written, or null.</summary>
|
|
public static string? ParseSaved(string line)
|
|
{
|
|
var m = SavedLine().Match(line);
|
|
return m.Success ? m.Groups[1].Value : null;
|
|
}
|
|
|
|
public static bool IsErrorLine(string line) => ErrorLine().IsMatch(line);
|
|
|
|
public static bool IsNonRetryableError(string line)
|
|
{
|
|
foreach (var p in NonRetryablePatterns)
|
|
{
|
|
if (line.Contains(p, StringComparison.OrdinalIgnoreCase)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|