// Which report-pipeline processes are working in a project RIGHT NOW (release // 17): what `umtool storage move-out|move-back|deliverables` refuses over. // // Linux /proc; elsewhere, none. Imported by the CLI only (bin/umtool.mjs) -- // never by anything the app bundles: a literal /proc path is a directory to // Turbopack. // // WHAT IT SEES, and how: // - the pipeline's node scripts (SCRIPTS), when an argument is the project // directory or a path under it -- how the build steps name a project // (`/video.manifest.json`, `--out /out`); // - the same scripts when their `--project` value names THIS project: equal // to its id, equal to its name, or resolving against the process's own // working directory to the project directory. Whole values, never a // prefix (`elfpire-eva-2` is not `elfpire-eva`). This is how the app runs // a cut (`cut-from-cache.mjs --project `) and a share batch // (`share-batch.mjs --project `). A name matched by a same-named // project elsewhere reads busy too, which only ever refuses; // - the report's OWN scripts (apply-manifest.py, build.py), which name no // path: by script name and working directory. // WHAT IT CANNOT SEE: the app's in-process work (none of it writes clips/ or // share-*; the app's own jobs are kept apart by its one-job-at-a-time rule), // a hand-run command that is none of these scripts (an ffmpeg into clips/), // and anything on another machine. import { readdirSync, readFileSync, readlinkSync, realpathSync } from "node:fs"; import path from "node:path"; export const PIPELINE_SCRIPTS = /(build-video|check-availability|render-cards|compose-chrome|verify-build|fetch-via-editor|resolve-windows|cut-from-cache|share-batch)\.mjs/; const OWN_SCRIPTS = /(^|\/)(apply-manifest|build)\.py$/; /** * Does one process's argv (and working directory) name this project? * Pure: the /proc reads are the caller's. * @param {string[]} args * @param {string | null} cwd the process's working directory, when readable * @param {{ dir: string, realDir?: string, id?: string, name?: string }} project */ export function namesProject(args, cwd, project) { const dirs = [...new Set([project.dir, project.realDir ?? project.dir])]; const underDir = (a) => dirs.some((d) => a === d || a.startsWith(d + "/")); if (args.some((a) => PIPELINE_SCRIPTS.test(a))) { if (args.some(underDir)) return true; for (let i = 0; i < args.length - 1; i++) { if (args[i] !== "--project") continue; const v = args[i + 1]; if (!v) continue; if (v === project.id || v === project.name) return true; if (cwd && dirs.includes(path.resolve(cwd, v))) return true; } return false; } if (args.some((a) => OWN_SCRIPTS.test(a))) return !!cwd && dirs.includes(cwd); return false; } /** * The pids of pipeline processes working in `project` (see the top of the file). * @param {{ dir: string, id?: string, name?: string }} project * @param {{ procRoot?: string, selfPid?: number }} [opts] * @returns {number[]} */ export function pipelineProcessesFor(project, { procRoot = "/proc", selfPid = process.pid } = {}) { let realDir = project.dir; try { realDir = realpathSync(project.dir); } catch { /* gone: nothing can be running in it */ } const ref = { ...project, realDir }; const pids = []; let entries = []; try { entries = readdirSync(procRoot).filter((n) => /^\d+$/.test(n)); } catch { return pids; } for (const pid of entries) { if (Number(pid) === selfPid) continue; let args; try { args = readFileSync(`${procRoot}/${pid}/cmdline`, "utf8").split("\0").filter((a) => a !== ""); } catch { continue; } if (!args.some((a) => PIPELINE_SCRIPTS.test(a) || OWN_SCRIPTS.test(a))) continue; let cwd = null; try { cwd = readlinkSync(`${procRoot}/${pid}/cwd`); } catch { /* another user's process, or gone */ } if (namesProject(args, cwd, ref)) pids.push(Number(pid)); } return pids; }