171 lines
6.1 KiB
JavaScript
171 lines
6.1 KiB
JavaScript
|
|
import { spawnSync } from 'node:child_process';
|
||
|
|
import { createWriteStream, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
||
|
|
import { chmod, copyFile } from 'node:fs/promises';
|
||
|
|
import { get } from 'node:https';
|
||
|
|
import { dirname, join, resolve } from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
export const NATS_SERVER_VERSION = '2.14.1';
|
||
|
|
|
||
|
|
const thisFile = fileURLToPath(import.meta.url);
|
||
|
|
export const repoRoot = resolve(dirname(thisFile), '..', '..');
|
||
|
|
|
||
|
|
function platformName() {
|
||
|
|
if (process.platform === 'linux') return 'linux';
|
||
|
|
if (process.platform === 'darwin') return 'darwin';
|
||
|
|
if (process.platform === 'win32') return 'windows';
|
||
|
|
throw new Error(`Unsupported platform for managed nats-server install: ${process.platform}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
function archName() {
|
||
|
|
if (process.arch === 'x64') return 'amd64';
|
||
|
|
if (process.arch === 'arm64') return 'arm64';
|
||
|
|
throw new Error(`Unsupported architecture for managed nats-server install: ${process.arch}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
function executableName() {
|
||
|
|
return process.platform === 'win32' ? 'nats-server.exe' : 'nats-server';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function managedNatsServerPath(root = repoRoot) {
|
||
|
|
return join(root, '.tools', 'nats-server', `v${NATS_SERVER_VERSION}`, `${platformName()}-${archName()}`, 'bin', executableName());
|
||
|
|
}
|
||
|
|
|
||
|
|
function pathEntries() {
|
||
|
|
return (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
|
||
|
|
}
|
||
|
|
|
||
|
|
function executableExists(path) {
|
||
|
|
try {
|
||
|
|
return existsSync(path) && statSync(path).isFile();
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function findOnPath(name) {
|
||
|
|
for (const entry of pathEntries()) {
|
||
|
|
const candidate = join(entry, name);
|
||
|
|
if (executableExists(candidate)) return candidate;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveNatsServer(root = repoRoot) {
|
||
|
|
const explicit = process.env.MATRIX_NATS_SERVER;
|
||
|
|
if (explicit) {
|
||
|
|
const resolved = resolve(explicit);
|
||
|
|
if (!executableExists(resolved)) {
|
||
|
|
throw new Error(`MATRIX_NATS_SERVER points to a missing file: ${resolved}`);
|
||
|
|
}
|
||
|
|
return { path: resolved, source: 'MATRIX_NATS_SERVER' };
|
||
|
|
}
|
||
|
|
|
||
|
|
const managed = managedNatsServerPath(root);
|
||
|
|
if (executableExists(managed)) return { path: managed, source: 'repo-managed' };
|
||
|
|
|
||
|
|
const onPath = findOnPath(executableName()) ?? findOnPath('nats-server');
|
||
|
|
if (onPath) return { path: onPath, source: 'PATH' };
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function natsServerInstallUrl(version = NATS_SERVER_VERSION) {
|
||
|
|
const format = process.platform === 'win32' ? 'zip' : 'tar.gz';
|
||
|
|
const asset = `nats-server-v${version}-${platformName()}-${archName()}.${format}`;
|
||
|
|
return {
|
||
|
|
asset,
|
||
|
|
url: `https://github.com/nats-io/nats-server/releases/download/v${version}/${asset}`,
|
||
|
|
format,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function download(url, destination) {
|
||
|
|
return new Promise((resolveDownload, rejectDownload) => {
|
||
|
|
const request = get(url, (response) => {
|
||
|
|
if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) {
|
||
|
|
response.resume();
|
||
|
|
download(response.headers.location, destination).then(resolveDownload, rejectDownload);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (response.statusCode !== 200) {
|
||
|
|
response.resume();
|
||
|
|
rejectDownload(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const file = createWriteStream(destination);
|
||
|
|
response.pipe(file);
|
||
|
|
file.on('finish', () => {
|
||
|
|
file.close(resolveDownload);
|
||
|
|
});
|
||
|
|
file.on('error', rejectDownload);
|
||
|
|
});
|
||
|
|
request.on('error', rejectDownload);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function walkFiles(dir) {
|
||
|
|
const out = [];
|
||
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||
|
|
const path = join(dir, entry.name);
|
||
|
|
if (entry.isDirectory()) out.push(...walkFiles(path));
|
||
|
|
else out.push(path);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractArchive(archive, destination, format) {
|
||
|
|
mkdirSync(destination, { recursive: true });
|
||
|
|
if (format === 'tar.gz') {
|
||
|
|
const result = spawnSync('tar', ['-xzf', archive, '-C', destination], { stdio: 'inherit' });
|
||
|
|
if (result.error) throw result.error;
|
||
|
|
if (result.status !== 0) throw new Error(`tar extraction failed with exit ${result.status}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const script = `Expand-Archive -LiteralPath ${JSON.stringify(archive)} -DestinationPath ${JSON.stringify(destination)} -Force`;
|
||
|
|
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'inherit' });
|
||
|
|
if (result.error) throw result.error;
|
||
|
|
if (result.status !== 0) throw new Error(`zip extraction failed with exit ${result.status}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function installManagedNatsServer(root = repoRoot) {
|
||
|
|
const existing = resolveNatsServer(root);
|
||
|
|
if (existing?.source === 'repo-managed') {
|
||
|
|
return { ...existing, installed: false };
|
||
|
|
}
|
||
|
|
|
||
|
|
const target = managedNatsServerPath(root);
|
||
|
|
const installRoot = dirname(dirname(target));
|
||
|
|
const tmpRoot = join(root, '.tools', 'tmp', `nats-server-v${NATS_SERVER_VERSION}-${Date.now()}`);
|
||
|
|
const { asset, url, format } = natsServerInstallUrl();
|
||
|
|
const archive = join(tmpRoot, asset);
|
||
|
|
const extractDir = join(tmpRoot, 'extract');
|
||
|
|
|
||
|
|
rmSync(installRoot, { recursive: true, force: true });
|
||
|
|
mkdirSync(tmpRoot, { recursive: true });
|
||
|
|
|
||
|
|
try {
|
||
|
|
await download(url, archive);
|
||
|
|
extractArchive(archive, extractDir, format);
|
||
|
|
const binary = walkFiles(extractDir).find((path) => path.endsWith(executableName()) || path.endsWith('/nats-server'));
|
||
|
|
if (!binary) throw new Error(`Downloaded ${asset} did not contain ${executableName()}`);
|
||
|
|
|
||
|
|
mkdirSync(dirname(target), { recursive: true });
|
||
|
|
await copyFile(binary, target);
|
||
|
|
if (process.platform !== 'win32') await chmod(target, 0o755);
|
||
|
|
return { path: target, source: 'repo-managed', installed: true, url };
|
||
|
|
} finally {
|
||
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function readNatsServerVersion(binaryPath) {
|
||
|
|
const result = spawnSync(binaryPath, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
||
|
|
if (result.error) throw result.error;
|
||
|
|
if (result.status !== 0) throw new Error(`${binaryPath} --version failed with exit ${result.status}`);
|
||
|
|
return `${result.stdout}${result.stderr}`.trim();
|
||
|
|
}
|