"use strict";
const { spawnSync } = require("node:child_process");
const { homedir, tmpdir } = require("node:os");
const {
chmodSync, copyFileSync, existsSync, openSync, closeSync, fsyncSync, mkdirSync,
readFileSync, renameSync, rmSync, statSync, writeFileSync,
} = require("node:fs");
const { basename, dirname, join } = require("node:path");
const { hostTarget, resolveTarget, sha256File, verifyTarget } = require("./verify.js");
const sha256 = sha256File;
function syncFile(path) {
const handle = openSync(path, process.platform === "win32" ? "r+" : "r");
try { fsyncSync(handle); } finally { closeSync(handle); }
}
function syncDirectory(path) {
if (process.platform === "win32") return;
const handle = openSync(path, "r");
try { fsyncSync(handle); } finally { closeSync(handle); }
}
function durableRename(from, to, helper, replace) {
if (process.platform === "win32" && process.env.NODE_ENV !== "test") {
const result = spawnSync(helper, ["__installer-rename", from, to, replace ? "replace" : "no-replace"], {
stdio: "inherit",
});
if (result.error || result.status !== 0) {
throw new Error(`durable Acyclic rename failed${result.error ? `: ${result.error.message}` : ""}`);
}
return;
}
renameSync(from, to);
syncDirectory(dirname(to));
if (dirname(from) !== dirname(to)) syncDirectory(dirname(from));
}
function durableRemove(path, helper) {
if (!existsSync(path)) return;
if (process.platform === "win32") {
const tombstone = `${path}.delete-${process.pid}-${Date.now()}`;
durableRename(path, tombstone, helper, false);
rmSync(tombstone, { recursive: true, force: true });
return;
}
rmSync(path, { recursive: true, force: true });
syncDirectory(dirname(path));
}
function durableJson(path, value, helper) {
const next = `${path}.next`;
writeFileSync(next, `${JSON.stringify(value, null, 2)}\n`);
syncFile(next);
durableRename(next, path, helper, true);
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function durableStateDirectory() {
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
return join(process.env.LOCALAPPDATA, "Acyclic", "state-v5");
}
if (process.platform !== "win32" && process.env.XDG_STATE_HOME) {
return join(process.env.XDG_STATE_HOME, "acyclic", "state-v5");
}
const home = process.env.HOME || homedir();
if (process.platform !== "win32" && home) {
return join(home, ".local", "state", "acyclic", "state-v5");
}
return join(tmpdir(), "acyclic-state-v5");
}
function installCertification(packageVersion, helper) {
const target = hostTarget();
const platform = { linux: "linux", darwin: "macos", win32: "windows" }[process.platform];
const architecture = { x64: "x86_64", arm64: "aarch64" }[process.arch];
const backend = { linux: "linux-fuse", darwin: "macos-nfs", win32: "windows-projfs" }[process.platform];
if (!platform || !architecture || !backend) return;
const name = `native-mount-${target}.json`;
const source = join(__dirname, "..", "certification", name);
if (!existsSync(source)) return;
const receipt = readJson(source);
if (
receipt.schema !== "acyclic-native-mount-qualification-v2"
|| receipt.os !== platform
|| receipt.arch !== architecture
|| receipt.required_kind !== backend
|| receipt.release_version !== packageVersion
|| receipt.passed !== true
|| typeof receipt.executable_blake3 !== "string"
|| !/^[0-9a-f]{64}$/.test(receipt.executable_blake3)
|| receipt.capability?.kind !== backend
|| receipt.capability?.available !== true
|| receipt.capability?.writable !== true
|| receipt.capability?.provider_process_io_observable !== (platform !== "windows")
|| receipt.capability?.session_isolation !== "SharedProcess"
|| receipt.capability?.unavailable_reason !== null
) {
throw new Error(`invalid Acyclic platform certification receipt: ${name}`);
}
const testBypass = process.env.NODE_ENV === "test"
&& process.env.ACYCLIC_INSTALL_TEST_CERTIFICATION === "1";
if (!testBypass) {
const verified = spawnSync(helper, ["__verify-certification", source], { stdio: "inherit" });
if (verified.error || verified.status !== 0) {
throw new Error(`Acyclic platform certification does not match the installed binary${verified.error ? `: ${verified.error.message}` : ""}`);
}
}
const directory = join(durableStateDirectory(), "certification");
mkdirSync(directory, { recursive: true });
durableJson(join(directory, name), receipt, helper);
}
function processExists(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error?.code === "EPERM";
}
}
function wait(milliseconds) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
}
function acquireInstallLock(directory) {
const path = join(directory, "install.lock");
const ownerPath = join(path, "owner.json");
const deadline = Date.now() + 120_000;
while (true) {
let acquired = false;
try {
mkdirSync(path);
acquired = true;
writeFileSync(ownerPath, `${JSON.stringify({ pid: process.pid, createdAt: Date.now() })}\n`);
syncFile(ownerPath);
return () => rmSync(path, { recursive: true, force: true });
} catch (error) {
if (acquired) {
rmSync(path, { recursive: true, force: true });
throw error;
}
if (error?.code !== "EEXIST") throw error;
let owner;
try { owner = readJson(ownerPath); } catch { owner = null; }
let initialized = true;
if (!Number.isInteger(owner?.pid) || owner.pid <= 0) {
try { initialized = Date.now() - statSync(path).mtimeMs >= 5_000; } catch { initialized = false; }
}
if (initialized && (!Number.isInteger(owner?.pid) || owner.pid <= 0 || !processExists(owner.pid))) {
const stale = `${path}.stale-${process.pid}-${Date.now()}`;
try {
renameSync(path, stale);
rmSync(stale, { recursive: true, force: true });
} catch (claimError) {
if (!["ENOENT", "EACCES", "EPERM"].includes(claimError?.code)) throw claimError;
}
continue;
}
if (Date.now() >= deadline) {
throw new Error(`timed out waiting for Acyclic installer process ${owner.pid}`);
}
wait(25);
}
}
}
function testCrash(point) {
if (process.env.NODE_ENV === "test" && process.env.ACYCLIC_INSTALL_TEST_CRASH === point) {
process.exit(86);
}
}
function testFailure(point) {
if (process.env.NODE_ENV === "test" && process.env.ACYCLIC_INSTALL_TEST_FAIL === point) {
throw new Error(`injected Acyclic installer failure at ${point}`);
}
}
function recoverInterruptedInstall(directory, installed, identityPath, expectedSha256, helper) {
const journalPath = join(directory, "install-transaction.json");
if (!existsSync(journalPath)) return;
const journal = readJson(journalPath);
if (journal.version !== 1 || !["staged", "backed-up", "activated"].includes(journal.phase)) {
throw new Error("unsupported Acyclic install transaction journal");
}
const paths = [
[journal.staged, `${basename(installed)}.next-`],
[journal.backup, `${basename(installed)}.backup-`],
[journal.identityNext, `${basename(identityPath)}.next-`],
];
for (const [name, prefix] of paths) {
const nonce = typeof name === "string" && name.startsWith(prefix) ? name.slice(prefix.length) : "";
if (basename(name ?? "") !== name || !/^\d+-\d+$/.test(nonce)) {
throw new Error("unsafe Acyclic install transaction path");
}
}
const staged = join(directory, journal.staged);
const backup = join(directory, journal.backup);
const identityNext = join(directory, journal.identityNext);
const activated = existsSync(installed) && sha256(installed) === journal.sha256;
if (activated) {
if (journal.sha256 !== expectedSha256) {
throw new Error("interrupted install belongs to different release bytes");
}
if (existsSync(identityNext)) durableRename(identityNext, identityPath, helper, true);
else durableJson(identityPath, journal.identity, helper);
durableRemove(backup, helper);
} else if (existsSync(backup)) {
durableRemove(installed, helper);
durableRename(backup, installed, helper, false);
}
durableRemove(staged, helper);
durableRemove(identityNext, helper);
durableRemove(journalPath, helper);
}
function ensureInstalledLocked() {
const { source, manifest } = verifyTarget(__dirname);
const packageVersion = readJson(join(__dirname, "..", "package.json")).version;
const target = hostTarget();
const expectedSha256 = manifest.targets[target].sha256;
const installed = join(__dirname, process.platform === "win32" ? "acyclic.exe" : "acyclic");
const identityPath = join(__dirname, "installed-binary.json");
const journalPath = join(__dirname, "install-transaction.json");
recoverInterruptedInstall(__dirname, installed, identityPath, expectedSha256, source);
const identity = existsSync(identityPath) ? readJson(identityPath) : null;
if (identity) {
if (identity.version === packageVersion && identity.sha256 !== expectedSha256) {
throw new Error(`immutable Acyclic release ${packageVersion} has conflicting binary bytes`);
}
if (existsSync(installed) && sha256(installed) !== identity.sha256) {
throw new Error("installed Acyclic binary does not match its durable identity");
}
}
const nextIdentity = { version: packageVersion, target, sha256: expectedSha256 };
if (existsSync(installed) && sha256(installed) === expectedSha256) {
if (!identity || JSON.stringify(identity) !== JSON.stringify(nextIdentity)) {
durableJson(identityPath, nextIdentity, source);
}
return installed;
}
const nonce = `${process.pid}-${Date.now()}`;
const staged = `${installed}.next-${nonce}`;
const backup = `${installed}.backup-${nonce}`;
const identityNext = `${identityPath}.next-${nonce}`;
const journal = {
version: 1, phase: "staged", sha256: expectedSha256,
staged: basename(staged), backup: basename(backup),
identityNext: basename(identityNext), identity: nextIdentity,
};
let backedUp = false;
let identityPublished = false;
try {
copyFileSync(source, staged);
if (process.platform !== "win32") chmodSync(staged, 0o755);
syncFile(staged);
if (sha256(staged) !== expectedSha256) {
throw new Error("staged Acyclic binary checksum verification failed");
}
writeFileSync(identityNext, `${JSON.stringify(nextIdentity, null, 2)}\n`);
syncFile(identityNext);
durableJson(journalPath, journal, source);
testCrash("staged");
if (existsSync(installed)) {
durableRename(installed, backup, source, false);
backedUp = true;
}
journal.phase = "backed-up";
durableJson(journalPath, journal, source);
testCrash("backed-up");
durableRename(staged, installed, source, false);
journal.phase = "activated";
durableJson(journalPath, journal, source);
testCrash("activated");
durableRename(identityNext, identityPath, source, true);
identityPublished = true;
testFailure("cleanup-after-identity");
durableRemove(backup, source);
durableRemove(journalPath, source);
return installed;
} catch (error) {
if (identityPublished) throw error;
if (backedUp) {
durableRemove(installed, source);
durableRename(backup, installed, source, false);
}
durableRemove(staged, source);
durableRemove(identityNext, source);
durableRemove(journalPath, source);
throw error;
}
}
function ensureInstalled() {
const release = acquireInstallLock(__dirname);
try {
const installed = ensureInstalledLocked();
const packageVersion = readJson(join(__dirname, "..", "package.json")).version;
installCertification(packageVersion, installed);
return installed;
} finally {
release();
}
}
function installedExecutable() {
const { manifest } = resolveTarget(__dirname);
const packageVersion = readJson(join(__dirname, "..", "package.json")).version;
const target = hostTarget();
const expectedSha256 = manifest.targets[target].sha256;
const installed = join(__dirname, process.platform === "win32" ? "acyclic.exe" : "acyclic");
const identityPath = join(__dirname, "installed-binary.json");
if (!existsSync(identityPath)) {
throw new Error("Acyclic installation identity is missing; reinstall the package");
}
const identity = readJson(identityPath);
if (
identity.version !== packageVersion
|| identity.target !== target
|| identity.sha256 !== expectedSha256
) {
throw new Error("installed Acyclic binary has an invalid durable identity");
}
if (!existsSync(installed) || sha256(installed) !== expectedSha256) {
throw new Error("installed Acyclic binary does not match its durable identity");
}
return installed;
}
module.exports = { ensureInstalled, installedExecutable };
if (require.main === module) ensureInstalled();