@aptos-labs/ts-sdk
Version:
Aptos TypeScript SDK
186 lines (163 loc) • 5.81 kB
text/typescript
import { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { appendFileSync } from "node:fs";
import { join } from "node:path";
import kill from "tree-kill";
import { platform } from "node:os";
import { sleep } from "../utils/helpers.js";
import { sanitizeCliArgs } from "./spawnArgs.js";
/**
* 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<void>} 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<void> {
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<void>} A promise that resolves when the process is up.
* @group Implementation
* @category CLI
*/
async run(): Promise<void> {
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 {
// Reject shell-metacharacter-bearing extras up front and pass the scrubbed
// copy to `spawn`. On Windows we have to spawn with `shell: true` (npx is a
// .cmd shim and Node refuses to spawn .cmd/.bat without the shell since
// CVE-2024-27980), so unsafe characters in extraArgs would otherwise be
// interpreted by cmd.exe.
const cliCommand = "npx";
const cliArgs = sanitizeCliArgs([
"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);
// CI prints `${{ runner.temp }}/local-testnet-logs.txt` on failure (TMPDIR).
const logPath = process.env.TMPDIR ? join(process.env.TMPDIR, "local-testnet-logs.txt") : undefined;
const handleOutput = (data: any) => {
const str = data.toString();
if (this.showStdout) {
console.log(str);
}
if (logPath) {
try {
appendFileSync(logPath, str);
} catch {
// Best-effort logging only — never fail the node start on log I/O.
}
}
};
this.process.stdout?.on("data", handleOutput);
this.process.stderr?.on("data", handleOutput);
}
/**
* 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<boolean> - Resolves to true if the process is up, otherwise throws an error.
* @group Implementation
* @category CLI
*/
async waitUntilProcessIsUp(): Promise<boolean> {
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<boolean> - A promise that resolves to true if the localnet is up, otherwise false.
* @group Implementation
* @category CLI
*/
async checkIfProcessIsUp(): Promise<boolean> {
try {
// Query readiness endpoint
const data = await fetch(this.READINESS_ENDPOINT);
if (data.status === 200) {
return true;
}
return false;
} catch {
return false;
}
}
}