import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import kill from "tree-kill"; import { platform } from "node:os"; import { sleep } from "../utils/helpers"; /** * Represents a local node for running a localnet environment. * This class provides methods to start, stop, and check the status of the localnet process. * It manages the lifecycle of the node process and ensures that it is operational before executing tests. * @group Implementation * @category CLI */ export class LocalNode { readonly MAXIMUM_WAIT_TIME_SEC = 75; readonly READINESS_ENDPOINT = "http://127.0.0.1:8070/"; showStdout: boolean = true; process: ChildProcessWithoutNullStreams | null = null; extraArgs: string[] = []; constructor(args?: { showStdout?: boolean; extraArgs?: string[] }) { this.showStdout = args?.showStdout ?? true; this.extraArgs = args?.extraArgs ?? []; } /** * Kills the current process and all its descendant processes. * * @returns {Promise} A promise that resolves to true if the process was successfully killed. * @throws {Error} If there is an error while attempting to kill the process. * @group Implementation * @category CLI */ async stop(): Promise { await new Promise((resolve, reject) => { if (!this.process?.pid) return; /** * Terminates the process associated with the given process ID. * * @param pid - The process ID of the process to be terminated. * @param callback - A function that is called after the termination attempt is complete. * @param callback.err - An error object if the termination failed; otherwise, null. * @param callback.resolve - A boolean indicating whether the termination was successful. * @group Implementation * @category CLI */ kill(this.process.pid, (err) => { if (err) { reject(err); } else { resolve(true); } }); }); } /** * Runs a localnet and waits for the process to be up. * If the local node process is already running, it returns without starting the process. * * @returns {Promise} A promise that resolves when the process is up. * @group Implementation * @category CLI */ async run(): Promise { const nodeIsUp = await this.checkIfProcessIsUp(); if (nodeIsUp) { return; } this.start(); await this.waitUntilProcessIsUp(); } /** * Starts the localnet by running the Aptos node with the specified command-line arguments. * * @returns {void} * * @throws {Error} If there is an issue starting the localnet. * @group Implementation * @category CLI */ start(): void { const cliCommand = "npx"; const cliArgs = [ "aptos", "node", "run-localnet", "--force-restart", "--assume-yes", "--with-indexer-api", ...this.extraArgs, ]; const currentPlatform = platform(); const spawnConfig = { env: { ...process.env, ENABLE_KEYLESS_DEFAULT: "1" }, ...(currentPlatform === "win32" && { shell: true }), }; this.process = spawn(cliCommand, cliArgs, spawnConfig); this.process.stdout?.on("data", (data: any) => { const str = data.toString(); // Print local node output log if (this.showStdout) { console.log(str); } }); } /** * Waits for the localnet process to be operational within a specified maximum wait time. * This function continuously checks if the process is up and will throw an error if it fails to start. * * @returns Promise - Resolves to true if the process is up, otherwise throws an error. * @group Implementation * @category CLI */ async waitUntilProcessIsUp(): Promise { let operational = await this.checkIfProcessIsUp(); const start = Date.now() / 1000; let last = start; while (!operational && start + this.MAXIMUM_WAIT_TIME_SEC > last) { await sleep(1000); operational = await this.checkIfProcessIsUp(); last = Date.now() / 1000; } // If we are here it means something blocks the process to start. // Might worth checking if another process is running on port 8080 if (!operational) { throw new Error("Process failed to start"); } return true; } /** * Checks if the localnet is up by querying the readiness endpoint. * * @returns Promise - A promise that resolves to true if the localnet is up, otherwise false. * @group Implementation * @category CLI */ async checkIfProcessIsUp(): Promise { try { // Query readiness endpoint const data = await fetch(this.READINESS_ENDPOINT); if (data.status === 200) { return true; } return false; } catch { return false; } } }