feat(worker): one-command packaging and --check install verification

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:
twotalesanimation
2026-08-02 15:17:35 +02:00
parent cc89415a29
commit a3be3489de
4 changed files with 214 additions and 6 deletions
+115
View File
@@ -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;
}
}
+9
View File
@@ -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;