@moonwall/cli
Version:
Testing framework for the Moon family of projects
323 lines (319 loc) • 10.9 kB
JavaScript
// src/internal/launcherCommon.ts
import chalk2 from "chalk";
import { execSync as execSync2 } from "child_process";
import fs2 from "fs";
import path3 from "path";
// src/lib/configReader.ts
import "@moonbeam-network/api-augment";
import { readFile, access } from "fs/promises";
import { readFileSync, existsSync, constants } from "fs";
import JSONC from "jsonc-parser";
import path, { extname } from "path";
var cachedConfig;
async function parseConfig(filePath) {
let result;
const file = await readFile(filePath, "utf8");
switch (extname(filePath)) {
case ".json":
result = JSON.parse(file);
break;
case ".config":
result = JSONC.parse(file);
break;
default:
result = void 0;
break;
}
return result;
}
async function importAsyncConfig() {
if (cachedConfig) {
return cachedConfig;
}
const configPath = process.env.MOON_CONFIG_PATH;
if (!configPath) {
throw new Error("No moonwall config path set. This is a defect, please raise it.");
}
const filePath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
try {
const config = await parseConfig(filePath);
const replacedConfig = replaceEnvVars(config);
cachedConfig = replacedConfig;
return cachedConfig;
} catch (e) {
console.error(e);
throw new Error(`Error import config at ${filePath}`);
}
}
function replaceEnvVars(value) {
if (typeof value === "string") {
return value.replace(/\$\{([^}]+)\}/g, (match, group) => {
const envVarValue = process.env[group];
return envVarValue || match;
});
}
if (Array.isArray(value)) {
return value.map(replaceEnvVars);
}
if (typeof value === "object" && value !== null) {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, replaceEnvVars(v)]));
}
return value;
}
function parseZombieConfigForBins(zombieConfigPath) {
const config = JSON.parse(readFileSync(zombieConfigPath, "utf8"));
const commands = [];
if (config.relaychain?.default_command) {
commands.push(path.basename(config.relaychain.default_command));
}
if (config.parachains) {
for (const parachain of config.parachains) {
if (parachain.collator?.command) {
commands.push(path.basename(parachain.collator.command));
}
}
}
return [...new Set(commands)].sort();
}
// src/internal/fileCheckers.ts
import fs from "fs";
import { execSync } from "child_process";
import chalk from "chalk";
import os from "os";
import path2 from "path";
import { select } from "@inquirer/prompts";
async function downloadBinsIfMissing(binPath) {
const binName = path2.basename(binPath);
const binDir = path2.dirname(binPath);
const binPathExists = fs.existsSync(binPath);
if (!binPathExists && process.arch === "x64") {
const download = await select({
message: `The binary ${chalk.bgBlack.greenBright(
binName
)} is missing from ${chalk.bgBlack.greenBright(
path2.join(process.cwd(), binDir)
)}.
Would you like to download it now?`,
default: 0,
choices: [
{ name: `Yes, download ${binName}`, value: true },
{ name: "No, quit program", value: false }
]
});
if (!download) {
process.exit(0);
} else {
execSync(`mkdir -p ${binDir}`);
execSync(`pnpm moonwall download ${binName} latest ${binDir}`, {
stdio: "inherit"
});
}
} else if (!binPathExists) {
console.log(
`The binary: ${chalk.bgBlack.greenBright(
binName
)} is missing from: ${chalk.bgBlack.greenBright(path2.join(process.cwd(), binDir))}`
);
console.log(
`Given you are running ${chalk.bgBlack.yellowBright(
process.arch
)} architecture, you will need to build it manually from source \u{1F6E0}\uFE0F`
);
throw new Error("Executable binary not available");
}
}
function checkListeningPorts(processId) {
try {
const stdOut = execSync(`lsof -p ${processId} | grep LISTEN`, {
encoding: "utf-8"
});
const binName = stdOut.split("\n")[0].split(" ")[0];
const ports = stdOut.split("\n").filter(Boolean).map((line) => {
const port = line.split(":")[1];
return port.split(" ")[0];
});
const filtered = new Set(ports);
return { binName, processId, ports: [...filtered].sort() };
} catch (e) {
const binName = execSync(`ps -p ${processId} -o comm=`).toString().trim();
console.log(
`Process ${processId} is running which for binary ${binName}, however it is unresponsive.`
);
console.log(
"Running Moonwall with this in the background may cause unexpected behaviour. Please manually kill the process and try running Moonwall again."
);
console.log(`N.B. You can kill it with: sudo kill -9 ${processId}`);
throw new Error(e);
}
}
function checkAlreadyRunning(binaryName) {
try {
console.log(`Checking if ${chalk.bgWhiteBright.blackBright(binaryName)} is already running...`);
const stdout = execSync(`pgrep ${[binaryName.slice(0, 14)]}`, {
encoding: "utf8",
timeout: 2e3
});
const pIdStrings = stdout.split("\n").filter(Boolean);
return pIdStrings.map((pId) => Number.parseInt(pId, 10));
} catch (error) {
if (error.status === 1) {
return [];
}
throw error;
}
}
async function promptAlreadyRunning(pids) {
const alreadyRunning = await select({
message: `The following processes are already running:
${pids.map((pid) => {
const { binName, ports } = checkListeningPorts(pid);
return `${binName} - pid: ${pid}, listenPorts: [${ports.join(", ")}]`;
}).join("\n")}`,
default: 1,
choices: [
{ name: "\u{1FA93} Kill processes and continue", value: "kill" },
{ name: "\u27A1\uFE0F Continue (and let processes live)", value: "continue" },
{ name: "\u{1F6D1} Abort (and let processes live)", value: "abort" }
]
});
switch (alreadyRunning) {
case "kill":
for (const pid of pids) {
execSync(`kill ${pid}`);
}
break;
case "continue":
break;
case "abort":
throw new Error("Abort Signal Picked");
}
}
// src/internal/launcherCommon.ts
import Docker from "dockerode";
import { select as select2 } from "@inquirer/prompts";
async function commonChecks(env) {
const globalConfig = await importAsyncConfig();
if (env.foundation.type === "dev") {
await devBinCheck(env);
}
if (env.foundation.type === "zombie") {
await zombieBinCheck(env);
}
if (process.env.MOON_RUN_SCRIPTS === "true" && globalConfig.scriptsDir && env.runScripts && env.runScripts.length > 0) {
for (const scriptCommand of env.runScripts) {
await executeScript(scriptCommand);
}
}
}
async function zombieBinCheck(env) {
if (env.foundation.type !== "zombie") {
throw new Error("This function is only for zombie environments");
}
const bins = parseZombieConfigForBins(env.foundation.zombieSpec.configPath);
const pids = bins.flatMap((bin) => checkAlreadyRunning(bin));
pids.length === 0 || process.env.CI || await promptAlreadyRunning(pids);
}
async function devBinCheck(env) {
if (env.foundation.type !== "dev") {
throw new Error("This function is only for dev environments");
}
if (!env.foundation.launchSpec || !env.foundation.launchSpec[0]) {
throw new Error("Dev environment requires a launchSpec configuration");
}
if (env.foundation.launchSpec[0].useDocker) {
const docker = new Docker();
const imageName = env.foundation.launchSpec[0].binPath;
console.log(`Checking if ${imageName} is running...`);
const matchingContainers = (await docker.listContainers({
filters: { ancestor: [imageName] }
})).flat();
if (matchingContainers.length === 0) {
return;
}
if (!process.env.CI) {
await promptKillContainers(matchingContainers);
return;
}
const runningContainers = matchingContainers.map(({ Id, Ports }) => ({
Id: Id.slice(0, 12),
Ports: Ports.map(
({ PublicPort, PrivatePort }) => PublicPort ? `${PublicPort} -> ${PrivatePort}` : `${PrivatePort}`
).join(", ")
}));
console.table(runningContainers);
throw new Error(`${imageName} is already running, aborting`);
}
const binName = path3.basename(env.foundation.launchSpec[0].binPath);
const pids = checkAlreadyRunning(binName);
pids.length === 0 || process.env.CI || await promptAlreadyRunning(pids);
await downloadBinsIfMissing(env.foundation.launchSpec[0].binPath);
}
async function promptKillContainers(matchingContainers) {
const answer = await select2({
message: `The following containers are already running image ${matchingContainers[0].Image}: ${matchingContainers.map(({ Id }) => Id).join(", ")}
Would you like to kill them?`,
choices: [
{ name: "\u{1FA93} Kill containers", value: "kill" },
{ name: "\u{1F44B} Quit", value: "goodbye" }
]
});
if (answer === "goodbye") {
console.log("Goodbye!");
process.exit(0);
}
if (answer === "kill") {
const docker = new Docker();
for (const { Id } of matchingContainers) {
const container = docker.getContainer(Id);
await container.stop();
await container.remove();
}
const containers = await docker.listContainers({
filters: { ancestor: matchingContainers.map(({ Image }) => Image) }
});
if (containers.length > 0) {
console.error(
`The following containers are still running: ${containers.map(({ Id }) => Id).join(", ")}`
);
process.exit(1);
}
return;
}
}
async function executeScript(scriptCommand, args) {
const scriptsDir = (await importAsyncConfig()).scriptsDir;
if (!scriptsDir) {
throw new Error("No scriptsDir found in config");
}
const files = await fs2.promises.readdir(scriptsDir);
try {
const script = scriptCommand.split(" ")[0];
const ext = path3.extname(script);
const scriptPath = path3.join(process.cwd(), scriptsDir, scriptCommand);
if (!files.includes(script)) {
throw new Error(`Script ${script} not found in ${scriptsDir}`);
}
console.log(`========== Executing script: ${chalk2.bgGrey.greenBright(script)} ==========`);
const argsString = args ? ` ${args}` : "";
switch (ext) {
case ".js":
execSync2(`node ${scriptPath}${argsString}`, { stdio: "inherit" });
break;
case ".ts":
execSync2(`pnpm tsx ${scriptPath}${argsString}`, { stdio: "inherit" });
break;
case ".sh":
execSync2(`${scriptPath}${argsString}`, { stdio: "inherit" });
break;
default:
console.log(`${ext} not supported, skipping ${script}`);
}
} catch (err) {
console.error(`Error executing script: ${chalk2.bgGrey.redBright(err)}`);
throw new Error(err);
}
}
export {
commonChecks,
executeScript
};