src / profile.ts
src / profile.ts
// Knowing whether the browser profile is already open, so the server never starts a second copy.
//
// Starting zen.exe with a profile that is already open does not fail: Zen opens another window
// or tab in the running browser. Firefox-family browsers keep a lock on an open profile, which
// is checked here, and every start is recorded on disk so that a server restarted moments later
// does not start the browser again while it is still coming up.
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { windowsFolders } from "./config";
// The profile's folder, from the browser's profiles.ini. Null when it cannot be found.
export function profileDir(profile: string, env: NodeJS.ProcessEnv = process.env): string | null {
// An explicit folder (tests and unusual setups) is the only place looked when it is given.
const home = env.HOME || env.USERPROFILE || os.homedir();
const roots = env.SIMPLESEARCH_PROFILES_DIR
? [env.SIMPLESEARCH_PROFILES_DIR]
: [
path.join(windowsFolders(env, home).appData, "zen"),
path.join(home, ".zen"),
path.join(home, "Library", "Application Support", "zen"),
];
for (const root of roots) {
let ini: string;
try {
ini = fs.readFileSync(path.join(root, "profiles.ini"), "utf8");
} catch {
continue;
}
for (const section of ini.split(/^\[/m)) {
const get = (key: string) => new RegExp(`^${key}=(.*)$`, "mi").exec(section)?.[1]?.trim();
if (get("Name") !== profile) continue;
const p = get("Path");
if (!p) continue;
return get("IsRelative") === "0" ? p : path.join(root, ...p.split(/[\\/]/));
}
}
return null;
}
// True when a running browser holds the profile, false when it does not, null when unknown.
export function profileInUse(dir: string, platform: NodeJS.Platform = process.platform): boolean | null {
if (platform === "win32") {
// The browser keeps parent.lock open without sharing; anyone else's open fails.
const lock = path.join(dir, "parent.lock");
if (!fs.existsSync(lock)) return false;
try {
fs.closeSync(fs.openSync(lock, "r+"));
return false;
} catch (e) {
const code = (e as NodeJS.ErrnoException).code;
return code === "EBUSY" || code === "EPERM" || code === "EACCES" ? true : null;
}
}
// Elsewhere the lock is a symbolic link naming the owner, "address:+pid".
try {
const target = fs.readlinkSync(path.join(dir, "lock"));
const pid = Number(/\+(\d+)$/.exec(target)?.[1]);
if (!pid) return null;
try {
process.kill(pid, 0);
return true;
} catch (e) {
return (e as NodeJS.ErrnoException).code === "EPERM" ? true : false;
}
} catch (e) {
return (e as NodeJS.ErrnoException).code === "ENOENT" ? false : null;
}
}
// When the browser was last started by any copy of the server, from a small file in the data folder.
export function lastLaunchAt(dataDir: string): number {
try {
return Number(JSON.parse(fs.readFileSync(path.join(dataDir, "launch.json"), "utf8")).at) || 0;
} catch {
return 0;
}
}
export function recordLaunch(dataDir: string, at = Date.now()): void {
try {
fs.mkdirSync(dataDir, { recursive: true });
fs.writeFileSync(path.join(dataDir, "launch.json"), JSON.stringify({ at }));
} catch {
// Without the record, only this process's own cooldown applies.
}
}
// Knowing whether the browser profile is already open, so the server never starts a second copy.
//
// Starting zen.exe with a profile that is already open does not fail: Zen opens another window
// or tab in the running browser. Firefox-family browsers keep a lock on an open profile, which
// is checked here, and every start is recorded on disk so that a server restarted moments later
// does not start the browser again while it is still coming up.
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { windowsFolders } from "./config";
// The profile's folder, from the browser's profiles.ini. Null when it cannot be found.
export function profileDir(profile: string, env: NodeJS.ProcessEnv = process.env): string | null {
// An explicit folder (tests and unusual setups) is the only place looked when it is given.
const home = env.HOME || env.USERPROFILE || os.homedir();
const roots = env.SIMPLESEARCH_PROFILES_DIR
? [env.SIMPLESEARCH_PROFILES_DIR]
: [
path.join(windowsFolders(env, home).appData, "zen"),
path.join(home, ".zen"),
path.join(home, "Library", "Application Support", "zen"),
];
for (const root of roots) {
let ini: string;
try {
ini = fs.readFileSync(path.join(root, "profiles.ini"), "utf8");
} catch {
continue;
}
for (const section of ini.split(/^\[/m)) {
const get = (key: string) => new RegExp(`^${key}=(.*)$`, "mi").exec(section)?.[1]?.trim();
if (get("Name") !== profile) continue;
const p = get("Path");
if (!p) continue;
return get("IsRelative") === "0" ? p : path.join(root, ...p.split(/[\\/]/));
}
}
return null;
}
// True when a running browser holds the profile, false when it does not, null when unknown.
export function profileInUse(dir: string, platform: NodeJS.Platform = process.platform): boolean | null {
if (platform === "win32") {
// The browser keeps parent.lock open without sharing; anyone else's open fails.
const lock = path.join(dir, "parent.lock");
if (!fs.existsSync(lock)) return false;
try {
fs.closeSync(fs.openSync(lock, "r+"));
return false;
} catch (e) {
const code = (e as NodeJS.ErrnoException).code;
return code === "EBUSY" || code === "EPERM" || code === "EACCES" ? true : null;
}
}
// Elsewhere the lock is a symbolic link naming the owner, "address:+pid".
try {
const target = fs.readlinkSync(path.join(dir, "lock"));
const pid = Number(/\+(\d+)$/.exec(target)?.[1]);
if (!pid) return null;
try {
process.kill(pid, 0);
return true;
} catch (e) {
return (e as NodeJS.ErrnoException).code === "EPERM" ? true : false;
}
} catch (e) {
return (e as NodeJS.ErrnoException).code === "ENOENT" ? false : null;
}
}
// When the browser was last started by any copy of the server, from a small file in the data folder.
export function lastLaunchAt(dataDir: string): number {
try {
return Number(JSON.parse(fs.readFileSync(path.join(dataDir, "launch.json"), "utf8")).at) || 0;
} catch {
return 0;
}
}
export function recordLaunch(dataDir: string, at = Date.now()): void {
try {
fs.mkdirSync(dataDir, { recursive: true });
fs.writeFileSync(path.join(dataDir, "launch.json"), JSON.stringify({ at }));
} catch {
// Without the record, only this process's own cooldown applies.
}
}