From a3be3489de43a4c7dd6471559f4dc3c972b24a57 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Sun, 2 Aug 2026 15:17:35 +0200 Subject: [PATCH] feat(worker): one-command packaging and --check install verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scripts/publish.ps1, which produces a self-contained single-file exe plus the headless AE build script, optionally zipped for copying to a render machine. Adds `VFXReviewWorker.exe --check`: resolves every configured path, confirms aerender/AfterFX/the build script exist, verifies each pathMappings target is reachable from that machine, and pings the server to confirm the API key is accepted — exiting non-zero if anything would stop the worker running. Catches a wrong path at install time rather than in the logs later. Verified from the published binary: the single-file exe resolves its scripts folder correctly, so the preview build script ships alongside the exe rather than embedded and can be patched without a rebuild. Co-Authored-By: Claude Fable 5 --- RenderWorker/README.md | 43 ++++++-- RenderWorker/VFXReviewWorker/Preflight.cs | 115 ++++++++++++++++++++++ RenderWorker/VFXReviewWorker/Program.cs | 9 ++ RenderWorker/scripts/publish.ps1 | 53 ++++++++++ 4 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 RenderWorker/VFXReviewWorker/Preflight.cs create mode 100644 RenderWorker/scripts/publish.ps1 diff --git a/RenderWorker/README.md b/RenderWorker/README.md index fbc213c..27578a8 100644 --- a/RenderWorker/README.md +++ b/RenderWorker/README.md @@ -63,16 +63,47 @@ the artist's AEP (then it is pure `aerender`), or the ffmpeg engine. Preview generation (Phase 4) and validation (Phase 3) plug into this same service later. -## Build +## Packaging for another machine -Requires the .NET 8+ SDK. +On a machine with the .NET 8+ SDK (only the packaging machine needs it): -```bash -cd RenderWorker/VFXReviewWorker -dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o publish +```powershell +.\scripts\publish.ps1 -Zip ``` -Produces a single `publish/VFXReviewWorker.exe` — no runtime install needed on render nodes. +Produces `publish\win-x64\` containing exactly two things: a self-contained +`VFXReviewWorker.exe` (~67 MB, no .NET runtime needed on the target) and +`scripts\vfxr_build_preview.jsx`, which the exe loads from its own folder at +runtime. **Keep them together** — the script is deliberately not embedded in the +exe so the slate/preview build can be patched without a rebuild. `-Zip` also +writes `VFXReviewWorker_.zip` for copying. + +### Installing on the target + +1. Copy the folder to the render machine, e.g. `C:\pipeline\VFXReviewWorker`. +2. Create `C:\ProgramData\VFXReviewWorker\config.json` (next section). +3. **Verify before installing the service:** + + ```powershell + .\VFXReviewWorker.exe --check + ``` + + This resolves every path, confirms `aerender.exe` / `AfterFX.com` / the build + script exist, checks each `pathMappings` target is reachable *from that + machine*, and pings the server to confirm the API key is accepted. It exits + non-zero if anything would stop the worker running, so a wrong path is caught + here instead of in the logs an hour later. + +4. Install the service (below), then confirm the machine appears on the web + **Pipeline → Machines** page. + +Run `--check` again under the *service account* (`runas /user:STUDIO\svc-render`) +if the service will run as someone other than the logged-in artist — mapped +drives and share permissions differ per account, and that is the most common +cause of a worker that registers fine but cannot find footage. + +Upgrading later: stop the service, replace the folder, start it again. Config +lives in ProgramData and is untouched. ## Configure diff --git a/RenderWorker/VFXReviewWorker/Preflight.cs b/RenderWorker/VFXReviewWorker/Preflight.cs new file mode 100644 index 0000000..9c77e0d --- /dev/null +++ b/RenderWorker/VFXReviewWorker/Preflight.cs @@ -0,0 +1,115 @@ +using System.Net.Http.Headers; + +namespace VFXReviewWorker; + +/// +/// `VFXReviewWorker.exe --check` — install verification for a new machine. +/// Resolves every path and dependency the worker needs, reports what is +/// missing, and exits non-zero if anything would stop it running. Cheaper than +/// installing the service and reading logs to find a typo'd path. +/// +public static class Preflight +{ + public static async Task RunAsync(string? configPath) + { + var ok = true; + Console.WriteLine("VFXReview RenderWorker preflight"); + Console.WriteLine(); + + var resolvedConfigPath = configPath ?? WorkerOptions.DefaultConfigPath; + WorkerOptions? options = null; + try + { + options = WorkerOptions.Load(configPath); + ok &= Report("config file", resolvedConfigPath, true); + } + catch (Exception ex) + { + Report("config file", resolvedConfigPath, false, ex.Message); + Console.WriteLine(); + Console.WriteLine("Cannot continue without a config file. See RenderWorker/README.md."); + return 1; + } + + Console.WriteLine($" machine name : {options.MachineName}"); + Console.WriteLine($" server : {options.ServerUrl}"); + Console.WriteLine($" api key : {(string.IsNullOrWhiteSpace(options.ApiKey) ? "(missing)" : "set")}"); + Console.WriteLine(); + + ok &= Report("aerender.exe", options.AerenderPath, File.Exists(options.AerenderPath)); + var afterFx = options.ResolveAfterFxPath(); + ok &= Report("AfterFX.com", afterFx, File.Exists(afterFx), + "needed for preview builds only"); + var script = options.ResolvePreviewScriptPath(); + ok &= Report("preview script", script, File.Exists(script), + "needed for preview builds only"); + + if (!string.IsNullOrWhiteSpace(options.FfmpegPath)) + { + Report("ffmpeg", options.FfmpegPath!, File.Exists(options.FfmpegPath!), + "optional — thumbnails are skipped without it"); + } + else + { + Console.WriteLine(" [ - ] ffmpeg not configured (thumbnails will be skipped)"); + } + + // Path mappings: prove the mapped roots are actually reachable here. + Console.WriteLine(); + if (options.PathMappings.Count == 0) + { + Console.WriteLine(" [ ! ] no pathMappings configured — manifest UNC paths will be used as-is"); + } + foreach (var m in options.PathMappings) + { + var reachable = Directory.Exists(m.To); + Report($"path map {m.From}", m.To, reachable, "target must be reachable from this machine"); + ok &= reachable; + } + + // Server reachability + API key validity in one call. + Console.WriteLine(); + try + { + using var http = new HttpClient { BaseAddress = new Uri(options.ServerUrl.TrimEnd('/') + "/"), Timeout = TimeSpan.FromSeconds(15) }; + http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey); + var res = await http.GetAsync("api/ext/projects"); + if (res.IsSuccessStatusCode) + { + Console.WriteLine(" [ok ] server reachable and API key accepted"); + } + else if ((int)res.StatusCode == 401) + { + Console.WriteLine(" [FAIL] server reachable but API key was rejected (401)"); + ok = false; + } + else + { + Console.WriteLine($" [FAIL] server responded {(int)res.StatusCode}"); + ok = false; + } + } + catch (Exception ex) + { + Console.WriteLine($" [FAIL] cannot reach {options.ServerUrl}: {ex.Message}"); + ok = false; + } + + Console.WriteLine(); + Console.WriteLine(ok + ? "All checks passed — safe to install the service." + : "One or more checks FAILED — fix the above before installing."); + return ok ? 0 : 1; + } + + private static bool Report(string label, string path, bool exists, string? note = null) + { + var status = exists ? "ok " : "FAIL"; + Console.WriteLine($" [{status}] {label,-16} {path}"); + if (!exists && note is not null) + { + Console.WriteLine($" ({note})"); + } + return exists; + } +} diff --git a/RenderWorker/VFXReviewWorker/Program.cs b/RenderWorker/VFXReviewWorker/Program.cs index c3a0372..c5984d7 100644 --- a/RenderWorker/VFXReviewWorker/Program.cs +++ b/RenderWorker/VFXReviewWorker/Program.cs @@ -11,6 +11,14 @@ using VFXReviewWorker; // Install as a service: see RenderWorker/README.md var configPath = args.FirstOrDefault(a => !a.StartsWith('-')); + +// Install verification on a new machine — resolves paths, checks tools and +// pings the server, then exits. Never starts the service loop. +if (args.Any(a => a.Equals("--check", StringComparison.OrdinalIgnoreCase))) +{ + return await Preflight.RunAsync(configPath); +} + var options = WorkerOptions.Load(configPath); // Positional args (config path) are consumed above and deliberately not @@ -28,3 +36,4 @@ builder.Services.AddSingleton(); builder.Services.AddHostedService(); await builder.Build().RunAsync(); +return 0; diff --git a/RenderWorker/scripts/publish.ps1 b/RenderWorker/scripts/publish.ps1 new file mode 100644 index 0000000..dfcb4a7 --- /dev/null +++ b/RenderWorker/scripts/publish.ps1 @@ -0,0 +1,53 @@ +<# +.SYNOPSIS + Packages the RenderWorker for installation on another machine. + +.DESCRIPTION + Produces a self-contained single-file build (no .NET runtime needed on the + target) plus the headless AE build script, and zips it for copying. + +.EXAMPLE + .\publish.ps1 + .\publish.ps1 -Zip +#> +[CmdletBinding()] +param( + [string]$OutDir = "", + [switch]$Zip +) + +$ErrorActionPreference = "Stop" +$root = Split-Path $PSScriptRoot -Parent +if (-not $OutDir) { $OutDir = Join-Path $root "publish\win-x64" } + +Write-Host "Publishing self-contained worker..." -ForegroundColor Cyan +& dotnet publish (Join-Path $root "VFXReviewWorker\VFXReviewWorker.csproj") ` + -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o $OutDir +if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed" } + +# The exe reads scripts\vfxr_build_preview.jsx from its own folder at runtime. +$scriptOut = Join-Path $OutDir "scripts\vfxr_build_preview.jsx" +if (-not (Test-Path $scriptOut)) { throw "Preview build script missing from output: $scriptOut" } + +Remove-Item (Join-Path $OutDir "*.pdb") -ErrorAction SilentlyContinue + +Write-Host "" +Write-Host "Package contents:" -ForegroundColor Green +Get-ChildItem $OutDir -Recurse -File | ForEach-Object { + Write-Host (" {0,-40} {1,8:N0} KB" -f $_.FullName.Substring($OutDir.Length).TrimStart('\'), ($_.Length / 1KB)) +} + +if ($Zip) { + $zipPath = Join-Path $root ("VFXReviewWorker_{0}.zip" -f (Get-Date -Format "yyyyMMdd")) + if (Test-Path $zipPath) { Remove-Item $zipPath } + Compress-Archive -Path (Join-Path $OutDir "*") -DestinationPath $zipPath + Write-Host "" + Write-Host "Zipped to: $zipPath" -ForegroundColor Green +} + +Write-Host "" +Write-Host "Next on the target machine:" -ForegroundColor Cyan +Write-Host " 1. Copy this folder to e.g. C:\pipeline\VFXReviewWorker" +Write-Host " 2. Create C:\ProgramData\VFXReviewWorker\config.json (see README)" +Write-Host " 3. Verify: .\VFXReviewWorker.exe --check" +Write-Host " 4. Install: sc.exe create VFXReviewRenderWorker binPath= ... start= auto obj= ... password= ..."