UNPKG

genlayer

Version:
298 lines (246 loc) 8.9 kB
import Docker, {ContainerInfo} from "dockerode"; import * as fs from "fs"; import * as dotenv from "dotenv"; import * as path from "path"; import * as semver from "semver"; import updateCheck from "update-check"; import { fileURLToPath } from "url"; import pkg from '../../../package.json' import {rpcClient} from "../clients/jsonRpcClient"; import { DEFAULT_RUN_SIMULATOR_COMMAND, DEFAULT_RUN_DOCKER_COMMAND, STARTING_TIMEOUT_WAIT_CYLCE, STARTING_TIMEOUT_ATTEMPTS, AI_PROVIDERS_CONFIG, AiProviders, VERSION_REQUIREMENTS, CONTAINERS_NAME_PREFIX, IMAGES_NAME_PREFIX } from "../config/simulator"; import { checkCommand, getVersion, executeCommand, openUrl, } from "../clients/system"; import {MissingRequirementError} from "../errors/missingRequirement"; import { ISimulatorService, WaitForSimulatorToBeReadyResultType, } from "../interfaces/ISimulatorService"; import {VersionRequiredError} from "../errors/versionRequired"; function sleep(millliseconds: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, millliseconds)); } export class SimulatorService implements ISimulatorService { private composeOptions: string private docker: Docker; public location: string; constructor() { const __filename = fileURLToPath(import.meta.url); this.location = path.resolve(path.dirname(__filename), '..'); this.composeOptions = ""; this.docker = new Docker(); } private readEnvConfigValue(key: string): string { const envFilePath = path.join(this.location, ".env"); // Transform the config string to object const envConfig = dotenv.parse(fs.readFileSync(envFilePath, "utf8")); return envConfig[key]; } private async getGenlayerContainers(): Promise<ContainerInfo[]> { const containers = await this.docker.listContainers({ all: true }); return containers.filter(container => container.Names.some(name => name.startsWith(CONTAINERS_NAME_PREFIX) || name.includes("ollama") ) ); } private async stopAndRemoveContainers(remove: boolean = false): Promise<void> { const genlayerContainers = await this.getGenlayerContainers(); for (const containerInfo of genlayerContainers) { const container = this.docker.getContainer(containerInfo.Id); if (containerInfo.State === "running") { await container.stop(); } const isOllamaContainer = containerInfo.Names.some(name => name.includes("ollama")); if (remove && !isOllamaContainer) { await container.remove(); } } } public addConfigToEnvFile(newConfig: Record<string, string>): void { const envFilePath = path.join(this.location, ".env"); // Transform the config string to object const envConfig = dotenv.parse(fs.readFileSync(envFilePath, "utf8")); Object.keys(newConfig).forEach(key => { envConfig[key] = newConfig[key]; }); // Transform the updated config object back into a string const updatedConfig = Object.keys(envConfig) .map(key => { return `${key}=${envConfig[key]}`; }) .join("\n"); // Write the new .env file fs.writeFileSync(envFilePath, updatedConfig); } public setComposeOptions(headless: boolean): void { this.composeOptions = headless ? '--scale frontend=0' : ''; } public getComposeOptions(): string { return this.composeOptions; } public async checkCliVersion(): Promise<void> { const update = await updateCheck(pkg); if (update && update.latest !== pkg.version) { console.warn(`\nA new version (${update.latest}) is available! You're using version ${pkg.version}.\nRun npm install -g genlayer to update\n`); } } public async checkInstallRequirements(): Promise<Record<string, boolean>> { const requirementsInstalled = { docker: false, }; try { await checkCommand("docker --version", "docker"); requirementsInstalled.docker = true; } catch (error: any) { if (!(error instanceof MissingRequirementError)) { throw error; } } if (requirementsInstalled.docker) { try { await this.docker.ping() } catch (error: any) { await executeCommand(DEFAULT_RUN_DOCKER_COMMAND); } } return requirementsInstalled; } public async checkVersionRequirements(): Promise<Record<string, string>> { const missingVersions = { docker: "", node: "", }; try { await this.checkVersion(VERSION_REQUIREMENTS.node, "node"); } catch (error: any) { missingVersions.node = VERSION_REQUIREMENTS.node; if (!(error instanceof VersionRequiredError)) { throw error; } } try { await this.checkVersion(VERSION_REQUIREMENTS.docker, "docker"); } catch (error: any) { missingVersions.docker = VERSION_REQUIREMENTS.docker; if (!(error instanceof VersionRequiredError)) { throw error; } } return missingVersions; } public async checkVersion(minVersion: string, toolName: string): Promise<void> { const version = await getVersion(toolName); if (!semver.satisfies(version, `>=${minVersion}`)) { throw new VersionRequiredError(toolName, minVersion); } } public runSimulator(): Promise<{stdout: string; stderr: string}> { const commandsByPlatform = DEFAULT_RUN_SIMULATOR_COMMAND(this.location, this.getComposeOptions()); return executeCommand(commandsByPlatform); } public async waitForSimulatorToBeReady( retries: number = STARTING_TIMEOUT_ATTEMPTS, ): Promise<WaitForSimulatorToBeReadyResultType> { try { const response = await rpcClient.request({method: "ping", params: []}); //Compatibility with current simulator version if (response?.result === "OK" || response?.result?.status === "OK" || response?.result?.data?.status === "OK") { return { initialized: true }; } if (retries > 0) { await sleep(STARTING_TIMEOUT_WAIT_CYLCE); return this.waitForSimulatorToBeReady(retries - 1); } } catch (error: any) { if ( (error.name === "FetchError" || error.message.includes("Fetch Error") || error.message.includes("ECONNRESET") || error.message.includes("ECONNREFUSED") || error.message.includes("socket hang up")) && retries > 0 ) { await sleep(STARTING_TIMEOUT_WAIT_CYLCE * 2); return this.waitForSimulatorToBeReady(retries - 1); } return {initialized: false, errorCode: "ERROR", errorMessage: error.message}; } return {initialized: false, errorCode: "TIMEOUT"}; } public createRandomValidators(numValidators: number, llmProviders: AiProviders[]): Promise<any> { return rpcClient.request({ method: "sim_createRandomValidators", params: [numValidators, 1, 10, llmProviders], }); } public deleteAllValidators(): Promise<any> { return rpcClient.request({method: "sim_deleteAllValidators", params: []}); } public getAiProvidersOptions(withHint: boolean = true): Array<{name: string; value: string}> { return Object.values(AI_PROVIDERS_CONFIG).map(providerConfig => { return { name: `${providerConfig.name}${withHint ? ` ${providerConfig.hint}` : ""}`, value: providerConfig.cliOptionValue, }; }); } public getFrontendUrl(): string { const frontendPort = this.readEnvConfigValue("FRONTEND_PORT"); return `http://localhost:${frontendPort}`; } public async openFrontend(): Promise<boolean> { await openUrl(this.getFrontendUrl()); return true; } public async stopDockerContainers(): Promise<void> { await this.stopAndRemoveContainers(false); } public async resetDockerContainers(): Promise<void> { await this.stopAndRemoveContainers(true); } public async resetDockerImages(): Promise<void> { const images = await this.docker.listImages(); const genlayerImages = images.filter(image => image.RepoTags?.some(tag => tag.startsWith(IMAGES_NAME_PREFIX)) ); for (const imageInfo of genlayerImages) { const image = this.docker.getImage(imageInfo.Id); await image.remove({force: true}); } } public async cleanDatabase(): Promise<boolean> { try { await rpcClient.request({method: "sim_clearDbTables", params: [['current_state', 'transactions']]}); }catch (error) { console.error(error); } return true; } public normalizeLocalnetVersion(version: string) { if (!version.startsWith('v')) { version = 'v' + version; } const versionRegex = /^v(\d+)\.(\d+)\.(\d+)(-.+)?$/; const match = version.match(versionRegex); if (!match) { console.error('Invalid version format. Expected format: v0.0.0 or v0.0.0-suffix'); process.exit(1); } return version } } export default new SimulatorService();