import { chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readlinkSync, unlinkSync, writeSync, } from "node:fs"; import { hostname, homedir } from "node:os"; import { isAbsolute, join, normalize, relative } from "node:path"; import { validateBrowserProfileName } from "./config.ts"; const PROFILE_ROOT_MODE = 0o700; const PROFILE_LOCK_MODE = 0o600; export interface ProfileSingletonLockOwnership { path: string; target: string; pid: number; processStartTime?: string; device: number; inode: number; } export interface ProfileLease { name: string; profileDirectory: string; lockPath: string; fd?: number; device: number; inode: number; /** Set only after the launched process proves the lock belongs to it. */ singletonLock?: ProfileSingletonLockOwnership; } export interface ProfileLeaseOptions { /** Test seam for the managed profile root. */ profileRoot?: string; } export const validateProfileName = validateBrowserProfileName; export function profileStorageRoot(env: NodeJS.ProcessEnv = process.env): string { const dataHome = env.XDG_DATA_HOME?.trim(); if (dataHome && !isAbsolute(dataHome)) { throw new Error("XDG_DATA_HOME must be an absolute path for named browser profiles"); } return join(dataHome || join(env.HOME?.trim() || homedir(), ".local", "share"), "fast-browser", "profiles"); } function assertContained(root: string, candidate: string): void { const rootPath = normalize(root); const candidatePath = normalize(candidate); const remainder = relative(rootPath, candidatePath); if (!remainder || remainder.startsWith("..") || isAbsolute(remainder)) { throw new Error("browser profile path escaped the managed profile root"); } } function ensurePrivateDirectory(path: string, label: string): void { mkdirSync(path, { recursive: true, mode: PROFILE_ROOT_MODE }); const before = lstatSync(path); if (!before.isDirectory() || before.isSymbolicLink()) throw new Error(`${label} must be a real directory`); if (typeof process.getuid === "function" && before.uid !== process.getuid()) { throw new Error(`${label} is not owned by this user`); } chmodSync(path, PROFILE_ROOT_MODE); const after = lstatSync(path); if (!after.isDirectory() || after.isSymbolicLink() || (after.mode & 0o077) !== 0) { throw new Error(`${label} must be private (0700)`); } } export function profileDirectory(name: string, options: ProfileLeaseOptions = {}): string { const safeName = validateProfileName(name); const root = options.profileRoot ?? profileStorageRoot(); ensurePrivateDirectory(root, "browser profile root"); const directory = join(root, safeName); assertContained(root, directory); if (!lstatDirectoryOrMissing(directory)) throw new Error("browser profile must be a real directory, not a symlink"); mkdirSync(directory, { recursive: true, mode: PROFILE_ROOT_MODE }); ensurePrivateDirectory(directory, "browser profile"); return directory; } function lstatDirectoryOrMissing(path: string): boolean { try { const stats = lstatSync(path); return stats.isDirectory() && !stats.isSymbolicLink(); } catch (error) { if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return true; throw error; } } function lockPathFor(root: string, name: string): string { const path = join(root, `.${name}.fast-browser-profile.lock`); assertContained(root, path); return path; } function activeSingletonLock(directory: string): string | undefined { const path = join(directory, "SingletonLock"); try { const stats = lstatSync(path); if (stats.isSymbolicLink() || stats.isFile()) return path; throw new Error("browser profile SingletonLock is not a regular lock entry"); } catch (error) { if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined; throw error; } } export function acquireProfileLease(name: string, options: ProfileLeaseOptions = {}): ProfileLease { const safeName = validateProfileName(name); const root = options.profileRoot ?? profileStorageRoot(); ensurePrivateDirectory(root, "browser profile root"); const directory = profileDirectory(safeName, { profileRoot: root }); const lockPath = lockPathFor(root, safeName); let fd: number; try { fd = openSync(lockPath, "wx", PROFILE_LOCK_MODE); } catch (error) { if ((error as NodeJS.ErrnoException)?.code === "EEXIST") { throw new Error(`browser profile ${safeName} already has an active or stale lease at ${lockPath}; verify the owning process is gone and remove the lease manually before retrying`); } throw new Error(`unable to acquire browser profile lease for ${safeName}`); } try { const singleton = activeSingletonLock(directory); if (singleton) { throw new Error(`browser profile ${safeName} has SingletonLock at ${singleton}; verify Chromium is not running and remove it manually only after inspection`); } const metadata = JSON.stringify({ pid: process.pid, profile: safeName }) + "\n"; writeSync(fd, metadata, undefined, "utf8"); const stats = fstatSync(fd); return { name: safeName, profileDirectory: directory, lockPath, fd, device: stats.dev, inode: stats.ino, }; } catch (error) { try { closeSync(fd); } catch { /* best effort */ } try { unlinkSync(lockPath); } catch { /* this process owns it; preserve unexpected replacement */ } throw error instanceof Error ? error : new Error(String(error)); } } export function releaseProfileLease(lease: ProfileLease): void { let failure: Error | undefined; try { if (activeSingletonLock(lease.profileDirectory)) { throw new Error(`browser profile SingletonLock remains at ${join(lease.profileDirectory, "SingletonLock")}; refusing to release the profile lease`); } const stats = lstatSync(lease.lockPath); if (stats.dev !== lease.device || stats.ino !== lease.inode) { throw new Error(`browser profile lease changed unexpectedly; refusing to remove ${lease.lockPath}`); } unlinkSync(lease.lockPath); } catch (error) { failure = error instanceof Error ? error : new Error(String(error)); } finally { if (lease.fd !== undefined) { const fd = lease.fd; lease.fd = undefined; try { closeSync(fd); } catch (error) { if (!failure) failure = error instanceof Error ? error : new Error(String(error)); } } } if (failure) throw failure; } function singletonLockDetails(lease: ProfileLease): ProfileSingletonLockOwnership | undefined { const path = join(lease.profileDirectory, "SingletonLock"); let stats; try { stats = lstatSync(path); } catch (error) { if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined; throw error; } if (!stats.isSymbolicLink()) { throw new Error(`browser profile SingletonLock is not a symlink; refusing to claim ownership of ${path}`); } const target = readlinkSync(path); const separator = target.lastIndexOf("-"); const host = separator > 0 ? target.slice(0, separator) : ""; const pid = separator > 0 ? Number(target.slice(separator + 1)) : NaN; if (host !== hostname() || !Number.isSafeInteger(pid) || pid < 1) { throw new Error(`browser profile SingletonLock ownership is not provable for ${path}`); } return { path, target, pid, device: stats.dev, inode: stats.ino }; } /** * Record the Chromium SingletonLock only when it names the process we spawned. * A missing marker is normal during early startup; an unexpected marker is * retained as an ownership failure rather than guessed to be stale. */ export function captureOwnedSingletonLock( lease: ProfileLease, pid: number | undefined, processStartTime?: string, ): void { if (pid === undefined) return; const details = singletonLockDetails(lease); if (!details) return; if (details.pid !== pid) { throw new Error(`browser profile SingletonLock names pid ${details.pid}, not owned browser pid ${pid}`); } lease.singletonLock = processStartTime === undefined ? details : { ...details, processStartTime }; } /** * Remove only the exact marker captured from our launched process, and only * after the caller has independently proved that that process is gone. */ export function removeOwnedSingletonLock( lease: ProfileLease, processGone: (ownership: ProfileSingletonLockOwnership) => boolean, ): void { const current = singletonLockDetails(lease); if (!current) return; const owned = lease.singletonLock; if (!owned || owned.path !== current.path || owned.target !== current.target || owned.device !== current.device || owned.inode !== current.inode) { throw new Error(`browser profile SingletonLock ownership is uncertain at ${current.path}; retaining the profile lease`); } if (!processGone(owned)) { throw new Error(`browser profile SingletonLock owner has not been proven gone at ${current.path}; retaining the profile lease`); } unlinkSync(current.path); lease.singletonLock = undefined; } /** Remove stale CDP metadata only while an exclusive named-profile lease is held. */ export function removeStaleDevToolsActivePort(lease: ProfileLease): void { const path = join(lease.profileDirectory, "DevToolsActivePort"); const stats = (() => { try { return lstatSync(path); } catch (error) { if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined; throw error; } })(); if (!stats) return; if (stats.isSymbolicLink() || !stats.isFile()) { throw new Error(`browser profile DevToolsActivePort is not a regular file; refusing to remove ${path}`); } unlinkSync(path); }