#!/usr/bin/env node "use strict"; const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const RUNNER_ID = "aetherbound-browser-temp-v1"; const OWNER_FILE = ".abg-browser-owner.json"; function safeLabel(value) { const normalized = String(value || "browser").replace(/[^A-Za-z0-9_.-]+/g, "-"); return normalized.replace(/^-+|-+$/g, "") || "browser"; } function processAlive(pid) { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error && error.code === "EPERM"; } } function cleanupStaleOwnedRoots(parent) { for (const entry of fs.readdirSync(parent, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const candidate = path.join(parent, entry.name); const ownerPath = path.join(candidate, OWNER_FILE); let owner; try { owner = JSON.parse(fs.readFileSync(ownerPath, "utf8")); } catch (_error) { continue; } if (owner.createdBy !== RUNNER_ID || processAlive(Number(owner.pid))) continue; fs.rmSync(candidate, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } } function installBrowserTemp(label) { const configuredParent = process.env.ABG_BROWSER_TEMP_PARENT; const parent = path.resolve(configuredParent || path.join(os.tmpdir(), "aetherbound-browser-runs")); fs.mkdirSync(parent, { recursive: true }); cleanupStaleOwnedRoots(parent); const root = fs.mkdtempSync(path.join(parent, `${safeLabel(label)}-`)); fs.writeFileSync( path.join(root, OWNER_FILE), `${JSON.stringify({ createdBy: RUNNER_ID, pid: process.pid, label: safeLabel(label) })}\n`, ); // Playwright and Chrome consult these variables when creating profiles, // artifacts and macOS code-signing copies. Configure them before Playwright // is required by the calling script. process.env.TMPDIR = root; process.env.TMP = root; process.env.TEMP = root; let cleaned = false; const cleanup = () => { if (cleaned) return; cleaned = true; fs.rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); if (!configuredParent) { try { fs.rmdirSync(parent); } catch (_error) { // Another active browser attempt may still own a sibling directory. } } }; process.once("exit", cleanup); for (const [signal, exitCode] of [["SIGINT", 130], ["SIGTERM", 143], ["SIGHUP", 129]]) { process.once(signal, () => { cleanup(); process.exit(exitCode); }); } return { root, cleanup }; } module.exports = { installBrowserTemp };