using System.Text.RegularExpressions; namespace VFXReviewWorker; /// /// 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. /// /// Blender 5.x logs through CLOG, which prefixes every line with an elapsed /// time and a category ("00:02.954 render | Saved: '...'") and /// reports errors as "ERROR ..." rather than "Error: ...". The prefix is /// stripped before matching, so both formats parse the same way. /// public static partial class BlenderProgressParser { [GeneratedRegex(@"^(?:\d+:)?\d{2}:\d{2}\.\d{3}\s+\S+\s*\| ?", RegexOptions.Compiled)] private static partial Regex LogPrefix(); [GeneratedRegex(@"^Fra:\s*(\d+)", RegexOptions.Compiled)] private static partial Regex FrameLine(); [GeneratedRegex(@"^Saved:\s*'(.+)'", RegexOptions.Compiled)] private static partial Regex SavedLine(); // Errors only. Warnings are not failures, and add-ons print them on every // launch ("Warning: add-on missing 'bl_info'"), so counting them made an // add-on's complaint the reported reason for a failed render. [GeneratedRegex(@"(?i:^Error: )|^(ERROR|FATAL) ", RegexOptions.Compiled)] private static partial Regex ErrorLine(); /// The line without Blender 5.x's "elapsed category | " prefix. private static string Message(string line) { var m = LogPrefix().Match(line); return m.Success ? line[m.Length..] : line; } /// /// 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. /// 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", "couldn't create directory", "cannot write file", "cannot save:", "no camera", "python script fail", "unknown argument", }; /// The frame Blender is currently working on, or null. public static int? ParseFrame(string line) { var m = FrameLine().Match(Message(line)); return m.Success && int.TryParse(m.Groups[1].Value, out var f) ? f : null; } /// The path of a frame Blender has just written, or null. public static string? ParseSaved(string line) { var m = SavedLine().Match(Message(line)); return m.Success ? m.Groups[1].Value : null; } public static bool IsErrorLine(string line) => ErrorLine().IsMatch(Message(line)); public static bool IsNonRetryableError(string line) { foreach (var p in NonRetryablePatterns) { if (line.Contains(p, StringComparison.OrdinalIgnoreCase)) return true; } return false; } }