@moonwall/cli
Version:
Testing framework for the Moon family of projects
349 lines (346 loc) • 10.9 kB
JavaScript
// src/internal/localNode.ts
import { exec, spawn, spawnSync } from "child_process";
import fs2 from "fs";
import path2 from "path";
import WebSocket from "ws";
// src/internal/fileCheckers.ts
import fs from "fs";
import { execSync } from "child_process";
import chalk from "chalk";
import os from "os";
import path from "path";
import { select } from "@inquirer/prompts";
async function checkExists(path3) {
const binPath = path3.split(" ")[0];
const fsResult = fs.existsSync(binPath);
if (!fsResult) {
throw new Error(
`No binary file found at location: ${binPath}
Are you sure your ${chalk.bgWhiteBright.blackBright(
"moonwall.config.json"
)} file has the correct "binPath" in launchSpec?`
);
}
const binArch = await getBinaryArchitecture(binPath);
const currentArch = os.arch();
if (binArch !== currentArch && binArch !== "unknown") {
throw new Error(
`The binary architecture ${chalk.bgWhiteBright.blackBright(
binArch
)} does not match this system's architecture ${chalk.bgWhiteBright.blackBright(
currentArch
)}
Download or compile a new binary executable for ${chalk.bgWhiteBright.blackBright(
currentArch
)} `
);
}
return true;
}
function checkAccess(path3) {
const binPath = path3.split(" ")[0];
try {
fs.accessSync(binPath, fs.constants.X_OK);
} catch (err) {
console.error(`The file ${binPath} is not executable`);
throw new Error(`The file at ${binPath} , lacks execute permissions.`);
}
}
async function getBinaryArchitecture(filePath) {
return new Promise((resolve, reject) => {
const architectureMap = {
0: "unknown",
3: "x86",
62: "x64",
183: "arm64"
};
fs.open(filePath, "r", (err, fd) => {
if (err) {
reject(err);
return;
}
const buffer = Buffer.alloc(20);
fs.read(fd, buffer, 0, 20, 0, (err2, bytesRead, buffer2) => {
if (err2) {
reject(err2);
return;
}
const e_machine = buffer2.readUInt16LE(18);
const architecture = architectureMap[e_machine] || "unknown";
resolve(architecture);
});
});
});
}
// src/internal/localNode.ts
import { createLogger } from "@moonwall/util";
import { setTimeout as timer } from "timers/promises";
import util from "util";
import Docker from "dockerode";
import invariant from "tiny-invariant";
var execAsync = util.promisify(exec);
var logger = createLogger({ name: "localNode" });
var debug = logger.debug.bind(logger);
async function launchDockerContainer(imageName, args, name, dockerConfig) {
const docker = new Docker();
const port = args.find((a) => a.includes("port"))?.split("=")[1];
debug(`\x1B[36mStarting Docker container ${imageName} on port ${port}...\x1B[0m`);
const dirPath = path2.join(process.cwd(), "tmp", "node_logs");
const logLocation = path2.join(dirPath, `${name}_docker_${Date.now()}.log`);
const fsStream = fs2.createWriteStream(logLocation);
process.env.MOON_LOG_LOCATION = logLocation;
const portBindings = dockerConfig?.exposePorts?.reduce(
(acc, { hostPort, internalPort }) => {
acc[`${internalPort}/tcp`] = [{ HostPort: hostPort.toString() }];
return acc;
},
{}
);
const rpcPort = args.find((a) => a.includes("rpc-port"))?.split("=")[1];
invariant(rpcPort, "RPC port not found, this is a bug");
const containerOptions = {
Image: imageName,
platform: "linux/amd64",
Cmd: args,
name: dockerConfig?.containerName || `moonwall_${name}_${Date.now()}`,
ExposedPorts: {
...Object.fromEntries(
dockerConfig?.exposePorts?.map(({ internalPort }) => [`${internalPort}/tcp`, {}]) || []
),
[`${rpcPort}/tcp`]: {}
},
HostConfig: {
PortBindings: {
...portBindings,
[`${rpcPort}/tcp`]: [{ HostPort: rpcPort }]
}
},
Env: dockerConfig?.runArgs?.filter((arg) => arg.startsWith("env:")).map((arg) => arg.slice(4))
};
try {
await pullImage(imageName, docker);
const container = await docker.createContainer(containerOptions);
await container.start();
const containerInfo = await container.inspect();
if (!containerInfo.State.Running) {
const errorMessage = `Container failed to start: ${containerInfo.State.Error}`;
console.error(errorMessage);
fs2.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
for (let i = 0; i < 300; i++) {
if (await checkWebSocketJSONRPC(Number.parseInt(rpcPort))) {
break;
}
await timer(100);
}
return { runningNode: container, fsStream };
} catch (error) {
if (error instanceof Error) {
console.error(`Docker container launch failed: ${error.message}`);
fs2.appendFileSync(logLocation, `Docker launch error: ${error.message}
`);
}
throw error;
}
}
async function launchNode(options) {
const { command: cmd, args, name, launchSpec: config } = options;
if (config?.useDocker) {
return launchDockerContainer(cmd, args, name, config.dockerConfig);
}
if (cmd.includes("moonbeam")) {
await checkExists(cmd);
checkAccess(cmd);
}
const port = args.find((a) => a.includes("port"))?.split("=")[1];
debug(`\x1B[36mStarting ${name} node on port ${port}...\x1B[0m`);
const dirPath = path2.join(process.cwd(), "tmp", "node_logs");
const runningNode = spawn(cmd, args);
const logLocation = path2.join(
dirPath,
`${path2.basename(cmd)}_node_${args.find((a) => a.includes("port"))?.split("=")[1]}_${runningNode.pid}.log`
).replaceAll("node_node_undefined", "chopsticks");
process.env.MOON_LOG_LOCATION = logLocation;
const fsStream = fs2.createWriteStream(logLocation);
runningNode.on("error", (err) => {
if (err.errno === "ENOENT") {
console.error(`\x1B[31mMissing Local binary at(${cmd}).
Please compile the project\x1B[0m`);
}
throw new Error(err.message);
});
const logHandler = (chunk) => {
if (fsStream.writable) {
fsStream.write(chunk, (err) => {
if (err) console.error(err);
else fsStream.emit("drain");
});
}
};
runningNode.stderr?.on("data", logHandler);
runningNode.stdout?.on("data", logHandler);
runningNode.once("exit", (code, signal) => {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
let message;
const moonwallNode = runningNode;
if (moonwallNode.isMoonwallTerminating) {
message = `${timestamp} [moonwall] process killed. reason: ${moonwallNode.moonwallTerminationReason || "unknown"}`;
} else if (code !== null) {
message = `${timestamp} [moonwall] process exited with status code ${code}`;
} else if (signal !== null) {
message = `${timestamp} [moonwall] process terminated by signal ${signal}`;
} else {
message = `${timestamp} [moonwall] process terminated unexpectedly`;
}
if (fsStream.writable) {
fsStream.write(`${message}
`, (err) => {
if (err) console.error(`Failed to write exit message to log: ${err}`);
fsStream.end();
});
} else {
try {
fs2.appendFileSync(logLocation, `${message}
`);
} catch (err) {
console.error(`Failed to append exit message to log file: ${err}`);
}
fsStream.end();
}
runningNode.stderr?.removeListener("data", logHandler);
runningNode.stdout?.removeListener("data", logHandler);
});
if (!runningNode.pid) {
const errorMessage = "Failed to start child process";
console.error(errorMessage);
fs2.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
if (runningNode.exitCode !== null) {
const errorMessage = `Child process exited immediately with code ${runningNode.exitCode}`;
console.error(errorMessage);
fs2.appendFileSync(logLocation, `${errorMessage}
`);
throw new Error(errorMessage);
}
const isRunning = await isPidRunning(runningNode.pid);
if (!isRunning) {
const errorMessage = `Process with PID ${runningNode.pid} is not running`;
spawnSync(cmd, args, { stdio: "inherit" });
throw new Error(errorMessage);
}
probe: for (let i = 0; ; i++) {
try {
const ports = await findPortsByPid(runningNode.pid);
if (ports) {
for (const port2 of ports) {
try {
await checkWebSocketJSONRPC(port2);
break probe;
} catch {
}
}
}
} catch {
if (i === 300) {
throw new Error("Could not find ports for node after 30 seconds");
}
await timer(100);
continue;
}
await timer(100);
}
return { runningNode, fsStream };
}
function isPidRunning(pid) {
return new Promise((resolve) => {
exec(`ps -p ${pid} -o pid=`, (error, stdout, stderr) => {
if (error) {
resolve(false);
} else {
resolve(stdout.trim() !== "");
}
});
});
}
async function checkWebSocketJSONRPC(port) {
try {
const ws = new WebSocket(`ws://localhost:${port}`);
const result = await new Promise((resolve) => {
ws.on("open", () => {
ws.send(
JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "system_chain",
params: []
})
);
});
ws.on("message", (data) => {
try {
const response = JSON.parse(data.toString());
if (response.jsonrpc === "2.0" && response.id === 1) {
resolve(true);
} else {
resolve(false);
}
} catch (e) {
resolve(false);
}
});
ws.on("error", () => {
resolve(false);
});
});
ws?.close();
return result;
} catch {
return false;
}
}
async function findPortsByPid(pid, retryCount = 600, retryDelay = 100) {
for (let i = 0; i < retryCount; i++) {
try {
const { stdout } = await execAsync(`lsof -p ${pid} -n -P | grep LISTEN`);
const ports = [];
const lines = stdout.split("\n");
for (const line of lines) {
const regex = /(?:.+):(\d+)/;
const match = line.match(regex);
if (match) {
ports.push(Number(match[1]));
}
}
if (ports.length) {
return ports;
}
throw new Error("Could not find any ports");
} catch (error) {
if (i === retryCount - 1) {
throw error;
}
}
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
return [];
}
async function pullImage(imageName, docker) {
console.log(`Pulling Docker image: ${imageName}`);
const pullStream = await docker.pull(imageName);
await new Promise((resolve, reject) => {
docker.modem.followProgress(pullStream, (err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
export {
launchNode
};