@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
229 lines • 8.87 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { spawn } from 'node:child_process';
import { KindExecutionException } from '../errors/kind-execution-exception.js';
import { KindParserException } from '../errors/kind-parser-exception.js';
/**
* Represents the execution of a kind command and is responsible for parsing the response.
*/
export class KindExecution {
/**
* The message for a timeout error.
*/
static MSG_TIMEOUT_ERROR = 'Timed out waiting for the process to complete';
/**
* The message for an error deserializing the output into a specified class.
*/
static MSG_DESERIALIZATION_ERROR = 'Failed to deserialize the output into the specified class: %s';
/**
* The message for an error reading the output from the process.
*/
static MSG_READ_OUTPUT_ERROR = 'Failed to read the output from the process';
/**
* The message for a deserialization error.
*/
static MSG_LIST_DESERIALIZATION_ERROR = 'Failed to deserialize the output into a list of the specified class: %s';
process;
output = [];
errOutput = [];
exitCodeValue = null;
/**
* Creates a new KindExecution instance.
* @param command The command array to execute
* @param environmentVariables The environment variables to set
*/
constructor(command, environmentVariables) {
this.process = spawn(command.join(' '), {
shell: true,
env: { ...process.env, ...environmentVariables },
});
}
/**
* Waits for the process to complete.
* @returns A promise that resolves when the process completes
*/
async waitFor() {
return new Promise((resolve, reject) => {
// const output: string[] = [];
this.process.stdout.on('data', (d) => {
const items = d.toString().split(/\r?\n/);
for (const item of items) {
if (item) {
this.output.push(item);
}
}
});
this.process.stderr.on('data', (d) => {
const items = d.toString().split(/\r?\n/);
for (const item of items) {
if (item) {
this.errOutput.push(item.trim());
}
}
});
this.process.on('close', (code) => {
this.exitCodeValue = code;
if (code === 0) {
resolve();
}
else {
reject(new KindExecutionException(code || 1, `Process exited with code ${code}: ${this.standardError()}`, this.standardOutput(), this.standardError()));
}
});
});
}
/**
* Waits for the process to complete with a timeout.
* @param timeout The maximum time to wait, or null to wait indefinitely
* @returns A promise that resolves with true if the process completed, or false if it timed out
*/
async waitForTimeout(timeout) {
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(false), timeout.toMillis());
});
const successPromise = new Promise((resolve) => {
this.process.on('close', (code) => {
resolve(code === 0);
});
});
return Promise.race([successPromise, timeoutPromise]);
}
/**
* Gets the exit code of the process.
* @returns The exit code or null if the process hasn't completed
*/
exitCode() {
return this.exitCodeValue;
}
/**
* Gets the standard output of the process.
* @returns concatenated standard output as a string
*/
standardOutput() {
return this.output.join('\n');
}
/**
* Gets the standard error of the process.
* @returns concatenated standard error as a string
*/
standardError() {
return this.errOutput.join('\n');
}
/**
* Gets the response as a parsed object.
* @param responseClass The class to parse the response into
* @returns A promise that resolves with the parsed response
*/
async responseAs(responseClass) {
return this.responseAsTimeout(responseClass, null);
}
/**
* Gets the response as a parsed object with a timeout.
* @param responseClass The class to parse the response into
* @param timeout The maximum time to wait, or null to wait indefinitely
* @returns A promise that resolves with the parsed response or rejects on timeout
*/
async responseAsTimeout(responseClass, timeout) {
if (timeout === null) {
await this.waitFor();
}
else {
const success = await this.waitForTimeout(timeout);
if (!success) {
throw new KindParserException(KindExecution.MSG_TIMEOUT_ERROR);
}
}
const exitCode = this.exitCode();
if (exitCode !== 0) {
const stdOut = this.standardOutput();
const stdError = this.standardError();
throw new KindExecutionException(exitCode, `Process exited with code ${exitCode}`, stdOut, stdError);
}
if (responseClass === undefined) {
return null;
}
const stdOut = this.standardOutput();
// Kind outputs to stdErr, so when the exit code is 0, we can assume stdErr is the expected output logs.
const stdLogs = this.standardError();
// If both stdOut and stdLogs are empty, we throw an error.
const output = stdOut || stdLogs;
if (!output) {
throw new KindParserException(KindExecution.MSG_READ_OUTPUT_ERROR);
}
try {
const parsed = output.split(/\r?\n/).filter((line) => line.trim() !== '');
return new responseClass(...parsed);
}
catch {
throw new KindParserException(KindExecution.MSG_DESERIALIZATION_ERROR.replace('%s', responseClass.name));
}
}
/**
* Gets the response as a list of parsed objects.
* @param responseClass The class to parse each item in the response into
* @returns A promise that resolves with the parsed response list
*/
async responseAsList(responseClass) {
return this.responseAsListTimeout(responseClass, null);
}
/**
* Gets the response as a list of parsed objects with a timeout.
* @param responseClass The class to parse each item in the response into
* @param timeout The maximum time to wait, or null to wait indefinitely
* @returns A promise that resolves with the parsed response list or rejects on timeout
*/
async responseAsListTimeout(responseClass, timeout) {
if (timeout === null) {
await this.waitFor();
}
else {
const success = await this.waitForTimeout(timeout);
if (!success) {
throw new KindParserException(KindExecution.MSG_TIMEOUT_ERROR);
}
}
const exitCode = this.exitCode();
if (exitCode !== 0) {
const stdOut = this.standardOutput();
const stdError = this.standardError();
throw new KindExecutionException(exitCode, `Process exited with code ${exitCode}`, stdOut, stdError);
}
const output = this.standardOutput();
try {
const splitOutput = output.split(/\r?\n/).filter((line) => line.trim() !== '');
return splitOutput.map(line => new responseClass(...line.split(',')));
}
catch {
throw new KindParserException(KindExecution.MSG_LIST_DESERIALIZATION_ERROR.replace('%s', responseClass.name));
}
}
/**
* Executes the command and waits for completion.
* @returns A promise that resolves when the command completes
*/
async call() {
await this.callTimeout(null);
}
/**
* Executes the command and waits for completion with a timeout.
* @param timeout The maximum time to wait, or null to wait indefinitely
* @returns A promise that resolves when the command completes or rejects on timeout
*/
async callTimeout(timeout) {
if (timeout === null) {
await this.waitFor();
}
else {
const success = await this.waitForTimeout(timeout);
if (!success) {
throw new KindParserException(KindExecution.MSG_TIMEOUT_ERROR);
}
}
const exitCode = this.exitCode();
if (exitCode !== 0) {
const stdOut = this.standardOutput();
const stdError = this.standardError();
throw new KindExecutionException(exitCode, `Process exited with code ${exitCode}`, stdOut, stdError);
}
}
}
//# sourceMappingURL=kind-execution.js.map