91e69f0bcb
Deploy / deploy (push) Successful in 3m18s
From the studio a single connection to Hetzner nbg1 (188 ms away) is capped near 0.1 MB/s, although a browser speed test shows 50 Mbps. Measured: 1 connection 0.13 MB/s, 8 -> 0.64, 24 -> 1.69 MB/s. At the old speed a ProRes MOV could not finish inside the 30 min request timeout. - Worker 1.7.2 uploads files >= 16 MB as S3 multipart: 8 MB parts on uploadConcurrency connections (config.json, default 16), each part retried up to 4 times; aborts the upload if a part keeps failing. - artifact-presign returns uploadId + per-part presigned URLs when asked for multipart; new E20b artifact-complete stitches or aborts. - Old workers and old servers fall back to the single PUT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
5.1 KiB
C#
110 lines
5.1 KiB
C#
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace VFXReviewWorker;
|
|
|
|
/// <summary>
|
|
/// Machine-local config (RenderPipeline2 §7.8) — deliberately minimal;
|
|
/// everything tunable lives in server SystemConfig and arrives via E6.
|
|
/// Default path: C:\ProgramData\VFXReviewWorker\config.json
|
|
/// </summary>
|
|
public sealed class WorkerOptions
|
|
{
|
|
public string ServerUrl { get; set; } = "";
|
|
public string ApiKey { get; set; } = "";
|
|
public string MachineName { get; set; } = Environment.MachineName;
|
|
public string AerenderPath { get; set; } = @"C:\Program Files\Adobe\Adobe After Effects 2026\Support Files\aerender.exe";
|
|
/// <summary>AfterFX.com — the console AE used for the headless preview build. Defaults beside aerender.</summary>
|
|
public string? AfterFxPath { get; set; }
|
|
/// <summary>vfxr_build_preview.jsx — defaults to scripts\ beside the worker exe.</summary>
|
|
public string? PreviewScriptPath { get; set; }
|
|
public string? FfmpegPath { get; set; }
|
|
/// <summary>
|
|
/// blender.exe. Its presence is what makes this machine eligible for
|
|
/// Blender jobs: the version is reported at registration and the server
|
|
/// only hands BLENDER_RENDER work to machines that have it.
|
|
/// </summary>
|
|
public string? BlenderPath { get; set; }
|
|
public string? OiiotoolPath { get; set; }
|
|
public List<PathMapping> PathMappings { get; set; } = new();
|
|
/// <summary>
|
|
/// Optional footage roots searched for a shot's plates when a manifest
|
|
/// carries no timing plate (rank 2 of §8.1a) — e.g. ["V:/_FOOTAGE/UNG"].
|
|
/// Leave empty and old manifests simply fall through to the API fallback.
|
|
/// </summary>
|
|
public List<string> PlateSearchRoots { get; set; } = new();
|
|
/// <summary>
|
|
/// Parallel connections per large upload. From the studio each connection
|
|
/// to object storage is capped near 0.1 MB/s, so throughput scales with
|
|
/// this (measured: 1 → 0.13 MB/s, 8 → 0.64, 24 → 1.69).
|
|
/// </summary>
|
|
public int UploadConcurrency { get; set; } = 16;
|
|
/// <summary>
|
|
/// Read from the assembly, not from config.json: a hand-maintained version
|
|
/// string is exactly the thing that goes stale, and a machine reporting a
|
|
/// version it is not running is worse than no version at all.
|
|
/// Includes the git SHA when publish.ps1 stamped one ("1.1.0+feb1c7b").
|
|
/// </summary>
|
|
public string WorkerVersion =>
|
|
typeof(WorkerOptions).Assembly
|
|
.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>()?.InformationalVersion
|
|
?? typeof(WorkerOptions).Assembly.GetName().Version?.ToString()
|
|
?? "unknown";
|
|
|
|
public string? AeVersion { get; set; }
|
|
|
|
public static string DataDir =>
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "VFXReviewWorker");
|
|
|
|
public static string DefaultConfigPath => Path.Combine(DataDir, "config.json");
|
|
public static string LogsDir => Path.Combine(DataDir, "logs");
|
|
public static string SpoolDir => Path.Combine(DataDir, "spool");
|
|
public static string ScratchDir => Path.Combine(DataDir, "scratch");
|
|
public static string StateFilePath => Path.Combine(DataDir, "current-job.json");
|
|
|
|
/// <summary>AfterFX.com sits beside aerender.exe in a standard AE install.</summary>
|
|
public string ResolveAfterFxPath()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(AfterFxPath)) return AfterFxPath;
|
|
var dir = Path.GetDirectoryName(AerenderPath);
|
|
return dir is null ? "AfterFX.com" : Path.Combine(dir, "AfterFX.com");
|
|
}
|
|
|
|
public string ResolvePreviewScriptPath()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(PreviewScriptPath)) return PreviewScriptPath;
|
|
return Path.Combine(AppContext.BaseDirectory, "scripts", "vfxr_build_preview.jsx");
|
|
}
|
|
|
|
public static WorkerOptions Load(string? path = null)
|
|
{
|
|
path ??= DefaultConfigPath;
|
|
if (!File.Exists(path))
|
|
{
|
|
throw new FileNotFoundException(
|
|
$"Worker config not found at {path}. Create it with serverUrl, apiKey and machineName (see RenderWorker/README.md).");
|
|
}
|
|
var json = File.ReadAllText(path);
|
|
var opts = JsonSerializer.Deserialize<WorkerOptions>(json, JsonOpts)
|
|
?? throw new InvalidOperationException($"Could not parse {path}");
|
|
if (string.IsNullOrWhiteSpace(opts.ServerUrl)) throw new InvalidOperationException("config.json: serverUrl is required");
|
|
if (string.IsNullOrWhiteSpace(opts.ApiKey)) throw new InvalidOperationException("config.json: apiKey is required");
|
|
if (string.IsNullOrWhiteSpace(opts.MachineName)) opts.MachineName = Environment.MachineName;
|
|
return opts;
|
|
}
|
|
|
|
public static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
};
|
|
}
|
|
|
|
public sealed class PathMapping
|
|
{
|
|
public string From { get; set; } = "";
|
|
public string To { get; set; } = "";
|
|
}
|