fix(connector): resolve server render paths on the workstation
Deploy / deploy (push) Successful in 6m4s
Deploy / deploy (push) Successful in 6m4s
QC's Open Render used the server's outputDir verbatim. The server
addresses renders from its own root (render.outputRoot, e.g. //SAN),
which the render worker translates via its own pathMappings because it
runs as a service account — the panel had no equivalent, so Open Render
failed on any machine that could not resolve that root.
Resolution now tries, in order: an optional Path Mappings setting, the
server path as given, then re-basing on Export Root. The last one is why
this usually needs no configuration at all: both sides lay renders out as
{root}/{showId}/{episode}/{shot}/{version}, so the tail can simply be
re-attached. A failure names every candidate it tried rather than
reporting one path that was never going to work.
Diagnostics gained the same check against a real folder from the QC
queue, since "Export Root exists" and "this machine can reach a render
the server wrote" are different questions and only the second one breaks
QC.
Verified against the live V: drive: a //SAN path, a backslash variant and
an explicitly-mapped path all resolve to the real render folder, while an
unknown root and a different show correctly resolve to nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+183
-9
@@ -12,7 +12,13 @@ var CONFIG = {
|
||||
bannerImagePath:"",
|
||||
artistEmail: "",
|
||||
handleFrames: 8,
|
||||
lastProjectCode:""
|
||||
lastProjectCode:"",
|
||||
// Optional prefix rewrites for paths the *server* supplies (render folders
|
||||
// are addressed as //SAN/... there, which a workstation may not resolve).
|
||||
// Format: "//SAN/=V:/_EXPORTS/" — several separated by ";".
|
||||
// Usually unnecessary: see resolveServerPath, which derives the local path
|
||||
// from Export Root when the layouts mirror each other.
|
||||
pathMappings: ""
|
||||
};
|
||||
|
||||
var SETTINGS_GROUP = "VFXReviewConnector";
|
||||
@@ -220,7 +226,8 @@ var PANEL_VERSION = "2.1.0";
|
||||
" | footageRoot=" + (CONFIG.footageRoot || "(empty)") +
|
||||
" | exportRoot=" + (CONFIG.exportRoot || "(empty)") +
|
||||
" | artistEmail=" + (CONFIG.artistEmail || "(empty)") +
|
||||
" | handleFrames=" + CONFIG.handleFrames;
|
||||
" | handleFrames=" + CONFIG.handleFrames +
|
||||
" | pathMappings=" + (CONFIG.pathMappings || "(none)");
|
||||
}
|
||||
|
||||
function diagStart(action) {
|
||||
@@ -786,6 +793,107 @@ var PANEL_VERSION = "2.1.0";
|
||||
return path + "/" + shot.shotCode + "/" + (shot.shotVersion || "v001");
|
||||
}
|
||||
|
||||
// ── Server paths → this machine ───────────────────────────────────────────
|
||||
//
|
||||
// The server addresses render folders from its own root (render.outputRoot,
|
||||
// e.g. "//SAN"), which a workstation may not resolve. The worker solves this
|
||||
// with pathMappings because it runs as a service account; the panel resolves
|
||||
// it here, and mostly without configuration: the server's export layout
|
||||
// mirrors the panel's from the show folder down —
|
||||
// {root}/{showId}/{episode}/{shotCode}/{version} on both sides — so the tail
|
||||
// can simply be re-based onto Export Root.
|
||||
|
||||
function normalizeSlashes(path) {
|
||||
return String(path).replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function parsePathMappings() {
|
||||
var raw = trimText(CONFIG.pathMappings);
|
||||
var mappings = [];
|
||||
var parts;
|
||||
var i;
|
||||
var pair;
|
||||
|
||||
if (!raw) {
|
||||
return mappings;
|
||||
}
|
||||
|
||||
parts = raw.split(";");
|
||||
for (i = 0; i < parts.length; i += 1) {
|
||||
pair = trimText(parts[i]).split("=");
|
||||
if (pair.length === 2 && trimText(pair[0]) && trimText(pair[1])) {
|
||||
mappings.push({ from: normalizeSlashes(trimText(pair[0])), to: normalizeSlashes(trimText(pair[1])) });
|
||||
}
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
|
||||
function applyPathMappings(path) {
|
||||
var mappings = parsePathMappings();
|
||||
var normalized = normalizeSlashes(path);
|
||||
var i;
|
||||
|
||||
for (i = 0; i < mappings.length; i += 1) {
|
||||
if (normalized.toLowerCase().indexOf(mappings[i].from.toLowerCase()) === 0) {
|
||||
return mappings[i].to + normalized.substring(mappings[i].from.length);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Re-base a server path onto Export Root from the show folder down, which
|
||||
// works whenever both sides use the documented layout.
|
||||
function rebaseOnExportRoot(path) {
|
||||
var showId = getShowId();
|
||||
var normalized = normalizeSlashes(path);
|
||||
var marker;
|
||||
var index;
|
||||
|
||||
if (!showId || !trimText(CONFIG.exportRoot)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
marker = "/" + showId + "/";
|
||||
index = normalized.toLowerCase().indexOf(marker.toLowerCase());
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeSlashes(trimText(CONFIG.exportRoot).replace(/\/+$/, "")) + normalized.substring(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a server-supplied path to something that exists on this machine.
|
||||
* Returns { path, how, tried } — `path` is null when nothing resolved, and
|
||||
* `tried` lists every candidate so a failure names them all.
|
||||
*/
|
||||
function resolveServerPath(serverPath) {
|
||||
var tried = [];
|
||||
var candidates = [];
|
||||
var mapped = applyPathMappings(serverPath);
|
||||
var rebased;
|
||||
var i;
|
||||
|
||||
if (mapped) {
|
||||
candidates.push({ path: mapped, how: "path mapping" });
|
||||
}
|
||||
candidates.push({ path: normalizeSlashes(serverPath), how: "server path" });
|
||||
|
||||
rebased = rebaseOnExportRoot(serverPath);
|
||||
if (rebased) {
|
||||
candidates.push({ path: rebased, how: "re-based on Export Root" });
|
||||
}
|
||||
|
||||
for (i = 0; i < candidates.length; i += 1) {
|
||||
tried.push(candidates[i].how + ": " + candidates[i].path);
|
||||
if (new Folder(candidates[i].path).exists) {
|
||||
return { path: candidates[i].path, how: candidates[i].how, tried: tried };
|
||||
}
|
||||
}
|
||||
|
||||
return { path: null, how: "", tried: tried };
|
||||
}
|
||||
|
||||
function ensureFolder(path) {
|
||||
var folder = new Folder(path);
|
||||
var parent;
|
||||
@@ -1793,6 +1901,46 @@ var PANEL_VERSION = "2.1.0";
|
||||
"\n" + failure.detail, diagReport());
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a real render folder from the server and checks this machine can
|
||||
* reach it. The server addresses renders from its own root, so a
|
||||
* workstation that has Export Root mapped can still fail here — which is
|
||||
* exactly what QC's Open Render trips over. Returns a problem count.
|
||||
*/
|
||||
function checkServerPathReachable() {
|
||||
var url = CONFIG.baseUrl + "/api/ext/qc/queue" +
|
||||
(CURRENT_PROJECT ? "?projectCode=" + encodeURIComponent(CURRENT_PROJECT.code) : "");
|
||||
var data = getAPIData(url);
|
||||
var entries;
|
||||
var resolved;
|
||||
var i;
|
||||
|
||||
if (!data) {
|
||||
diagLog("QC queue not reachable — skipping the render-path check");
|
||||
return 0;
|
||||
}
|
||||
|
||||
entries = getListFromResponse(data, "queue");
|
||||
if (!entries.length) {
|
||||
diagLog("no exports awaiting QC — render-path check skipped (nothing to point at)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
resolved = resolveServerPath(entries[0].outputDir);
|
||||
if (resolved.path) {
|
||||
diagLog("render folder OK: " + entries[0].shotCode + " via " + resolved.how + " -> " + resolved.path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
diagLog("PROBLEM cannot reach the render folder for " + entries[0].shotCode +
|
||||
" — QC's Open Render will fail on this machine");
|
||||
for (i = 0; i < resolved.tried.length; i += 1) {
|
||||
diagLog(" tried " + resolved.tried[i]);
|
||||
}
|
||||
diagLog(" - fix: set Export Root to match the layout, or add Settings -> Path Mappings, e.g. //SAN/=V:/_EXPORTS/");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Checks everything Build Shot depends on, without building anything, so a
|
||||
// workstation can be cleared start to finish before an artist is blocked.
|
||||
function runDiagnostics() {
|
||||
@@ -1909,6 +2057,11 @@ var PANEL_VERSION = "2.1.0";
|
||||
(DIAG.lastResponse ? summariseResponse(DIAG.lastResponse) : "no response"));
|
||||
}
|
||||
}
|
||||
|
||||
// Can this machine actually reach a render folder the server wrote?
|
||||
// That is a different question from Export Root existing, and it is
|
||||
// the one QC fails on.
|
||||
problems += checkServerPathReachable();
|
||||
}
|
||||
|
||||
for (i = 0; i < requiredComps.length; i += 1) {
|
||||
@@ -2919,6 +3072,12 @@ var PANEL_VERSION = "2.1.0";
|
||||
var fBanner = addField(dlg, "Banner Path:", CONFIG.bannerImagePath);
|
||||
var fEmail = addField(dlg, "Artist Email:", CONFIG.artistEmail);
|
||||
var fHandles= addField(dlg, "Handle Frames:", String(CONFIG.handleFrames));
|
||||
var fMappings = addField(dlg, "Path Mappings:", CONFIG.pathMappings);
|
||||
fMappings.helpTip =
|
||||
"Optional. Rewrites paths the server supplies (render folders are\n" +
|
||||
"addressed from its own root). Format: //SAN/=V:/_EXPORTS/\n" +
|
||||
"Several separated by ';'. Usually unnecessary — the panel first\n" +
|
||||
"tries the path as given, then re-bases it on Export Root.";
|
||||
|
||||
var btnRow = dlg.add("group");
|
||||
btnRow.orientation = "row";
|
||||
@@ -2939,6 +3098,7 @@ var PANEL_VERSION = "2.1.0";
|
||||
CONFIG.bannerImagePath = trimText(fBanner.text);
|
||||
CONFIG.artistEmail = trimText(fEmail.text);
|
||||
CONFIG.handleFrames = parseInt(fHandles.text, 10) || 8;
|
||||
CONFIG.pathMappings = trimText(fMappings.text);
|
||||
saveConfig();
|
||||
populateProjectDropdown();
|
||||
}
|
||||
@@ -3871,7 +4031,11 @@ var PANEL_VERSION = "2.1.0";
|
||||
// The export's own outputPattern prefix, so a version folder that also holds
|
||||
// the preview MOV/MP4 still resolves to the EXR sequence.
|
||||
function importQCSequence(entry) {
|
||||
var folder = new Folder(entry.outputDir);
|
||||
// The server addresses this folder from its own render root, which this
|
||||
// machine may not resolve — resolveServerPath tries the mapping, the
|
||||
// literal path, then Export Root (§ Server paths → this machine).
|
||||
var resolved = resolveServerPath(entry.outputDir);
|
||||
var folder;
|
||||
var prefix = String(entry.outputPattern || "").split("[")[0].replace(/\.$/, "");
|
||||
var files;
|
||||
var importOptions;
|
||||
@@ -3886,14 +4050,21 @@ var PANEL_VERSION = "2.1.0";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!folder.exists) {
|
||||
setQCStatus("Render folder not found: " + entry.outputDir);
|
||||
diagFail("QC import", entry.outputDir + " does not exist on this machine",
|
||||
[describeMissingPath(entry.outputDir),
|
||||
"The render folder is written by the farm — check this machine maps the same drive."]);
|
||||
if (!resolved.path) {
|
||||
setQCStatus("Render folder not found for " + entry.shotCode + " — see the report");
|
||||
diagFail("QC import", "could not resolve " + entry.outputDir + " on this machine",
|
||||
resolved.tried.concat([
|
||||
"The server writes renders under its own root (render.outputRoot); this machine has to reach the same storage.",
|
||||
"Either set Export Root so the layout lines up, or add a rewrite in Settings → Path Mappings, e.g. //SAN/=V:/_EXPORTS/"
|
||||
]));
|
||||
return false;
|
||||
}
|
||||
|
||||
folder = new Folder(resolved.path);
|
||||
if (resolved.how !== "server path") {
|
||||
diagLog("QC import: resolved " + entry.outputDir + " via " + resolved.how + " -> " + resolved.path);
|
||||
}
|
||||
|
||||
files = folder.getFiles(function (f) {
|
||||
return f instanceof File && /\.exr$/i.test(f.name) &&
|
||||
(!prefix || f.name.indexOf(prefix) === 0);
|
||||
@@ -3902,7 +4073,10 @@ var PANEL_VERSION = "2.1.0";
|
||||
files = folder.getFiles("*.exr");
|
||||
}
|
||||
if (!files || files.length < 1) {
|
||||
setQCStatus("No EXRs in " + entry.outputDir);
|
||||
setQCStatus("No EXRs in " + resolved.path);
|
||||
diagFail("QC import", "no .exr files in " + resolved.path,
|
||||
["Resolved via " + resolved.how + " from the server path " + entry.outputDir + ".",
|
||||
"Folder contents: " + describeFolderContents(folder)]);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,28 @@ Settings are stored per-machine via the **⚙ Settings** button in the panel (pe
|
||||
| **Banner Path** | Path to the panel banner image |
|
||||
| **Artist Email** | Used for render attribution in the pipeline |
|
||||
| **Handle Frames** | Plate handle frames prepended before the shot's source TC in-point (default: 8) |
|
||||
| **Path Mappings** | *Optional.* Rewrites paths the **server** supplies — see below |
|
||||
|
||||
### Server paths vs. this machine
|
||||
|
||||
The server addresses render folders from its own root (`render.outputRoot`,
|
||||
e.g. `//SAN`), which a workstation may not resolve — the render worker
|
||||
translates them with its own `pathMappings` because it runs as a service
|
||||
account. The panel needs the same translation to open a render for QC.
|
||||
|
||||
It resolves this itself in most cases, trying in order:
|
||||
|
||||
1. **Path Mappings**, if configured — format `//SAN/=V:/_EXPORTS/`, several
|
||||
separated by `;`.
|
||||
2. **The server path as given** — correct when `render.outputRoot` is already
|
||||
a path this machine can reach.
|
||||
3. **Re-based on Export Root** — the server's export layout mirrors the panel's
|
||||
from the show folder down (`{root}/{showId}/{episode}/{shot}/{version}`), so
|
||||
the tail is simply re-attached to Export Root.
|
||||
|
||||
Step 3 is why this usually needs no configuration. Set Path Mappings only when
|
||||
the two layouts genuinely differ. **Diagnostics** checks this against a real
|
||||
render folder from the QC queue and names every path it tried when it fails.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user