141 lines
6.2 KiB
JavaScript
141 lines
6.2 KiB
JavaScript
// What the host and the human commands read before they touch the bus:
|
|
// the system config (through scripts/mosaic-config.mjs, as `mosaic business`
|
|
// does), the business file, and the boot message for the broker process.
|
|
// Everything here reads and refuses; nothing writes.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { existsSync, lstatSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { BusinessError, accessFilePath, configDir, loadAccess, loadBusiness, loadProject, projectFilePath, resolveInstance, systemVars } from "../../business/src/index.mjs";
|
|
import { busBusiness } from "../../bus/src/business.mjs";
|
|
import { BusError } from "../../bus/src/broker.mjs";
|
|
import { CliError } from "./errors.mjs";
|
|
import { accessContextFor } from "./access.mjs";
|
|
|
|
export const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
|
|
// The broker's socket. One broker per data root: the bus store takes
|
|
// `<dataRoot>/bus/writer.lock` (packages/bus/src/store.mjs).
|
|
export const socketPath = (dataRoot) => join(dataRoot, "bus", "broker.sock");
|
|
|
|
export function loadSystem({ env = process.env } = {}) {
|
|
const proc = spawnSync(process.execPath, [join(REPO, "scripts", "mosaic-config.mjs"), "validate"], {
|
|
encoding: "utf8",
|
|
maxBuffer: 1024 * 1024,
|
|
env,
|
|
});
|
|
if (proc.status !== 0) throw new CliError(`system config problem (mosaic-config exit ${proc.status}): ${proc.stderr.trim()}`, 3);
|
|
try {
|
|
return JSON.parse(proc.stdout);
|
|
} catch {
|
|
throw new CliError("system config: mosaic-config printed something that isn't JSON", 3);
|
|
}
|
|
}
|
|
|
|
export function rolesDir(env = process.env) {
|
|
return resolve(env.MOSAIC_ROLES_DIR || join(REPO, "roles"));
|
|
}
|
|
|
|
// A business-package or adapter refusal is a config problem: exit 3.
|
|
function business(fn) {
|
|
try {
|
|
return fn();
|
|
} catch (error) {
|
|
if (error instanceof BusinessError) throw new CliError(error.message, 3);
|
|
if (error instanceof BusError) throw new CliError(`business adapter refused: ${error.code}`, 3);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// config.trackers[<business>] (lead decision 70): plain data from the
|
|
// tracker.* variables. tracker.project is a project-layer variable, so the
|
|
// entry comes from the one declared project whose file sets it. No
|
|
// tracker.baseUrl, or no project naming a tracker project, means no entry
|
|
// and no task verbs; two projects naming one each refuse, since the boot
|
|
// shape holds one tracker project per business.
|
|
function trackerFor(system, b, warn) {
|
|
const first = Object.keys(b.roles)[0];
|
|
const base = resolveInstance({ system, business: b, instance: first }).vars;
|
|
if (base["tracker.baseUrl"] === undefined) return null;
|
|
const named = [];
|
|
for (const [id, entry] of Object.entries(b.projects)) {
|
|
if (entry.kind === 'library') continue;
|
|
if (!existsSync(projectFilePath(entry.root))) continue;
|
|
const project = loadProject(entry.root);
|
|
if (project.id !== id) throw new CliError(`project file ${project.file} has id ${project.id}, but business ${b.id} declares it as ${id}`, 3);
|
|
const vars = resolveInstance({ system, business: b, project, instance: first }).vars;
|
|
if (vars["tracker.project"] !== undefined) named.push({ id, vars });
|
|
}
|
|
if (named.length > 1) {
|
|
throw new CliError(`business ${b.id}: projects ${named.map((n) => n.id).join(", ")} each set tracker.project; the broker takes one tracker project per business`, 3);
|
|
}
|
|
if (named.length === 0) {
|
|
warn(`business ${b.id} sets tracker.baseUrl, but no project file sets tracker.project; the broker gets no task verbs`);
|
|
return null;
|
|
}
|
|
const v = named[0].vars;
|
|
return {
|
|
baseUrl: v["tracker.baseUrl"],
|
|
project: v["tracker.project"],
|
|
pollSeconds: v["tracker.pollSeconds"],
|
|
reconcileMinutes: v["tracker.reconcileMinutes"],
|
|
descendants: v["tracker.descendants"],
|
|
};
|
|
}
|
|
|
|
// FED R6 (#1800, sage-3142): the runtime's `access`, { file, context }. The broker reads the file
|
|
// with R1's loadAccess on a user request that finds the file's stat changed, and on its 30 s timer
|
|
// (FED R4, #1801), so a bad file leaves every user with no grant and the owner's bus still boots
|
|
// (sage-3153). The context is the cli's accessContextFor, peers included. No file, or a context
|
|
// that can't be built, means no `access`: no user binds, as before R6.
|
|
function accessFor(system, env, warn) {
|
|
const file = accessFilePath(configDir(env));
|
|
try {
|
|
lstatSync(file);
|
|
} catch {
|
|
return null;
|
|
}
|
|
let context;
|
|
try {
|
|
context = accessContextFor(system, env);
|
|
} catch (e) {
|
|
warn(`access.json is present but its context can't be read, so no granted user binds: ${e.message}`);
|
|
return null;
|
|
}
|
|
// sage-3153: one line at boot when the loader refuses the file, so the owner sees why no user
|
|
// binds. The loader's messages can quote the file, so the line names only the path and a code
|
|
// from its exit code: 4 is a file it could not open as a regular file, anything else invalid.
|
|
try {
|
|
loadAccess(file, context);
|
|
} catch (e) {
|
|
const code = e instanceof BusinessError && e.exitCode !== 4 ? "access-invalid" : "access-unreadable";
|
|
warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);
|
|
}
|
|
return { file, context };
|
|
}
|
|
|
|
// The `{op: 'boot'}` config for packages/bus/src/process.mjs, for one
|
|
// business. The host holds a reader capability for the notifier and binds
|
|
// launches later through bindLaunch, so `launches` starts empty.
|
|
export function bootConfig({ system, businessId, env = process.env, warn = () => {} }) {
|
|
return business(() => {
|
|
const vars = systemVars(system);
|
|
const b = loadBusiness(businessId, { dir: configDir(env), rolesDir: rolesDir(env) });
|
|
const resolved = {};
|
|
for (const instance of Object.keys(b.roles)) resolved[instance] = resolveInstance({ system: vars, business: b, instance });
|
|
const config = {
|
|
dataRoot: system.dataRoot,
|
|
businesses: { [b.id]: busBusiness(b, resolved) },
|
|
organization: {dir:configDir(env),rolesDir:rolesDir(env),system:vars},
|
|
launches: [],
|
|
readers: [b.id],
|
|
};
|
|
const tracker = trackerFor(vars, b, warn);
|
|
if (tracker) config.trackers = { [b.id]: tracker };
|
|
const access = accessFor(system, env, warn);
|
|
if (access) config.access = access;
|
|
return config;
|
|
});
|
|
}
|