feat(worker): one-command packaging and --check install verification
Deploy / deploy (push) Failing after 15m27s
Deploy / deploy (push) Failing after 15m27s
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 <noreply@anthropic.com>
This commit is contained in:
+37
-6
@@ -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_<date>.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
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace VFXReviewWorker;
|
||||
|
||||
/// <summary>
|
||||
/// `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.
|
||||
/// </summary>
|
||||
public static class Preflight
|
||||
{
|
||||
public static async Task<int> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<JobExecutor>();
|
||||
builder.Services.AddHostedService<WorkerService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
return 0;
|
||||
|
||||
@@ -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= ..."
|
||||
Reference in New Issue
Block a user