UNPKG

@hpcc-js/comms

Version:
629 lines (567 loc) 23.3 kB
import * as cp from "node:child_process"; import * as crypto from "node:crypto"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { exists, scopedLogger, xml2json, XMLNode } from "@hpcc-js/util"; import { attachWorkspace, Workspace } from "./eclMeta.ts"; const logger = scopedLogger("clienttools/eclcc"); const exeExt = os.type() === "Windows_NT" ? ".exe" : ""; function tidyCRLF(inStr: string): string { return inStr.split("\r\n").join("\n").split("\r").join("\n"); } export class Version { readonly prefix: string = ""; readonly major: number = 0; readonly minor: number = 0; readonly patch: number = 0; readonly postfix: string = ""; constructor(build: string) { const parts = build.split(" "); if (parts.length) { const match = /(?:(\w+)_)?(\d+)\.(\d+)\.(\d+)(?:-(.*))?/.exec(parts[parts.length - 1]); if (match) { this.prefix = match[1] || ""; this.major = +match[2] || 0; this.minor = +match[3] || 0; this.patch = +match[4] || 0; this.postfix = match[5] || ""; } } } parse(build: string) { } exists(): boolean { return this.major !== 0 || this.minor !== 0 || this.patch !== 0 || this.postfix !== ""; } compare(other: Version): number { if (this.major > other.major) return 1; if (this.major < other.major) return -1; if (this.minor > other.minor) return 1; if (this.minor < other.minor) return -1; if (this.patch > other.patch) return 1; if (this.patch < other.patch) return -1; if (this.postfix === "" && other.postfix !== "") return 1; return this.postfix.localeCompare(other.postfix); } toString(): string { return `${this.prefix}_${this.major}.${this.minor}.${this.patch}-${this.postfix}`; } } interface IExecFile { code: number; stderr: string; stdout: string; } export interface IECLErrorWarning { filePath: string; line: number; col: number; msg: string; severity: string; } const ERROR = "error"; const WARN = "warning"; export class Errors { protected _checked: string[]; protected errWarn: IECLErrorWarning[] = []; protected errOther: string[] = []; constructor(checked: string[]) { this._checked = checked; } checked(): string[] { return this._checked; } all(): IECLErrorWarning[] { return this.errWarn; } errors(): IECLErrorWarning[] { return this.errWarn.filter(e => e.severity === ERROR); } hasError(): boolean { return this.errors().length > 0; } warnings(): IECLErrorWarning[] { return this.errWarn.filter(e => e.severity === WARN); } hasWarning(): boolean { return this.warnings().length > 0; } info(): IECLErrorWarning[] { return this.errWarn.filter(e => [ERROR, WARN].indexOf(e.severity) < 0); } hasOther(): boolean { return this.info().length > 0; } unknown(): string[] { return this.errOther; } hasUnknown(): boolean { return this.unknown().length > 0; } } export class EclccErrors extends Errors { constructor(stdErr: string, checked: string[]) { super(checked); if (stdErr && stdErr.length) { for (const errLine of stdErr.split(os.EOL)) { let match = /([a-zA-Z]:\\(?:[- \w\.\d]+\\)*(?:[- \w\.\d]+)?|(?:\/[\w\.\-]+)+)\((\d*),(\d*)\) ?: ?(error|warning|info) C(\d*) ?: ?(.*)/.exec(errLine); if (match) { const [, filePath, row, _col, severity, code, _msg] = match; const line: number = +row; const col: number = +_col; const msg = code + ": " + _msg; this.errWarn.push({ filePath, line, col, msg, severity }); continue; } match = /(error|warning|info): (.*)/i.exec(errLine); if (match) { const [, severity, msg] = match; this.errWarn.push({ filePath: "", line: 0, col: 0, msg, severity }); continue; } match = /\d error(s?), \d warning(s?)/.exec(errLine); if (match) { continue; } logger.warning(`parseECLErrors: Unable to parse "${errLine}"`); this.errOther.push(errLine); } } this._checked = checked; } } export class EnvchkErrors extends Errors { private _lines: string[]; constructor(filePath: string, stdErr: string, checked: string[]) { super(checked); let content: string = fs.readFileSync(filePath, "utf8"); content = content.replace(/\r\n/g, "\n"); this._lines = content.split("\n"); if (stdErr && stdErr.length) { for (const errLine of stdErr.split(os.EOL)) { const match = /(Warning|Error) : Path\=(\S*?)(\[\S*\])? Message\=(.*)/.exec(errLine); if (match) { const [, severity, _path, _attr, _msg] = match; const msg = `${_path} ${_attr ? _attr : ""}: ${_msg}`; const [line, col] = this.locate(_path); this.errWarn.push({ filePath, line, col, msg, severity }); continue; } if (match) { continue; } logger.warning(`parseECLErrors: Unable to parse "${errLine}"`); this.errOther.push(errLine); } } this._checked = checked; } locate(path: string): [number, number] { const pathParts = path.split("/"); if (pathParts.length && pathParts[0] === "") { pathParts.shift(); } if (pathParts.length > 0) { let lineIdx = 0; for (const line of this._lines) { const testStr = "<" + pathParts[0]; if (line.indexOf(testStr + " ") >= 0 || line.indexOf(testStr + ">") >= 0) { pathParts.shift(); if (pathParts.length === 0) { return [lineIdx + 1, line.indexOf(testStr) + 1]; } } ++lineIdx; } } return [0, 0]; } } export function walkXmlJson(node: any, callback: (key: string, childNode: any, stack: any[]) => void, stack?: any[]) { stack = stack || []; stack.push(node); for (const key in node) { if (node.hasOwnProperty(key)) { const childNode = node[key]; callback(key, childNode, stack); if (childNode instanceof Array) { childNode.forEach(child => { walkXmlJson(child, callback, stack); }); } else if (typeof childNode === "object") { walkXmlJson(childNode, callback, stack); } } } stack.pop(); } export class LocalWorkunit { jsonWU: any; constructor(jsonWU: any) { this.jsonWU = jsonWU; } bpGetValidLocations(filePath: any) { const retVal: any[] = []; if (exists("W_LOCAL.Graphs", this.jsonWU)) { let id = ""; walkXmlJson(this.jsonWU.W_LOCAL.Graphs, (key: string, item: any, _stack: any[]) => { if (key === "$" && item.id) { id = item.id; } if (key === "$" && item.name === "definition") { const match = /([a-z,A-Z]:\\(?:[-\w\.\d]+\\)*(?:[-\w\.\d]+)?|(?:\/[\w\.\-]+)+)\((\d*),(\d*)\)/.exec(item.value); if (match) { const [, file, row, _col] = match; const line: number = +row; const col: number = +_col; if (filePath === file) { retVal.push({ file, line, col, id }); } } } // console.log(`${key}: ` + JSON.stringify(item)); }); } return retVal; } } export interface IArchive { content: string; err: EclccErrors; } export interface IBundle { name: string; description: string; url: string; props?: { [key: string]: string | number | boolean }; } export class ClientTools { readonly eclccPath: string; readonly envchkPath: string; readonly eclBundlePath: string; readonly binPath: string; protected cwd: string; protected includeFolders: string[]; protected _legacyMode: boolean; protected _args: string[]; protected _version: Version; constructor(eclccPath: string, cwd?: string, includeFolders: string[] = [], legacyMode: boolean = false, args: string[] = [], version?: Version) { this.eclccPath = eclccPath; this.binPath = path.dirname(this.eclccPath); this.envchkPath = path.join(this.binPath, "envchk" + exeExt); this.eclBundlePath = path.join(this.binPath, "ecl-bundle" + exeExt); this.cwd = path.normalize(cwd || this.binPath); this.includeFolders = includeFolders; this._legacyMode = legacyMode; this._args = args; this._version = version!; } clone(cwd?: string, includeFolders?: string[], legacyMode: boolean = false, args: string[] = []) { return new ClientTools(this.eclccPath, cwd, includeFolders, legacyMode, args, this._version); } exists(filePath: string) { try { fs.accessSync(filePath); return true; } catch (e) { } return false; } args(additionalItems: string[] = []): string[] { const retVal: string[] = [...this._args]; if (this._legacyMode) { retVal.push("-legacy"); } return retVal.concat(this.includeFolders.map(includePath => { return "-I" + path.normalize(includePath); })).concat(additionalItems); } version(): Promise<Version> { if (this._version) { return Promise.resolve(this._version); } return this.execFile(this.eclccPath, this.binPath, this.args(["--version"]), "eclcc", `Cannot find ${this.eclccPath}`).then((response: IExecFile): Version => { this._version = new Version(response.stdout); return this._version; }); } versionSync(): Version { return this._version; } _paths = {}; paths() { return this.execFile(this.eclccPath, this.cwd, this.args(["-showpaths"]), "eclcc", `Cannot find ${this.eclccPath}`).then((response: IExecFile) => { if (response && response.stdout && response.stdout.length) { const paths = response.stdout.split(/\r?\n/); for (const path of paths) { const parts = path.split("="); if (parts.length === 2) { this._paths[parts[0]] = parts[1]; } } } return this._paths; }); } private loadXMLDoc(filePath: any, removeOnRead?: boolean): Promise<XMLNode> { return new Promise((resolve, _reject) => { const fileData = fs.readFileSync(filePath, "ascii"); const retVal = xml2json(fileData as any); if (removeOnRead) { fs.unlink(filePath, (err) => { }); } resolve(retVal); }); } createWU(filename: string): Promise<LocalWorkunit> { const tmpName = path.join(os.tmpdir(), `eclcc-wu-tmp-${crypto.randomBytes(8).toString("hex")}`); const args = ["-o" + tmpName, "-wu"].concat([filename]); return this.execFile(this.eclccPath, this.cwd, this.args(args), "eclcc", `Cannot find ${this.eclccPath}`).then((_response: IExecFile) => { const xmlPath = path.normalize(tmpName + ".xml"); const contentPromise = this.exists(xmlPath) ? this.loadXMLDoc(xmlPath, true) : Promise.resolve({}); return contentPromise.then((content) => { return new LocalWorkunit(content); }); }); } createArchive(filename: string): Promise<IArchive> { const args = ["-E"].concat([filename]); return this.execFile(this.eclccPath, this.cwd, this.args(args), "eclcc", `Cannot find ${this.eclccPath}`).then((response: IExecFile): IArchive => { return { content: response.stdout, err: new EclccErrors(response.stderr, []) }; }); } attachWorkspace(): Workspace { return attachWorkspace(this.cwd); } fetchMeta(filePath: string): Promise<Workspace> { return Promise.all([ attachWorkspace(this.cwd), this.execFile(this.eclccPath, this.cwd, this.args(["-M", filePath]), "eclcc", `Cannot find ${this.eclccPath}`) ]).then(([metaWorkspace, execFileResponse]: [Workspace, IExecFile]) => { try { if (execFileResponse && execFileResponse.stdout && execFileResponse.stdout.length) { metaWorkspace.parseMetaXML(execFileResponse.stdout); } } catch (e: any) { logger.error(`fetchMeta: Error parsing XML - ${e?.message ?? "unknown"}`); } return metaWorkspace; }); } syntaxCheck(filePath: string, args: string[] = ["-syntax"]): Promise<Errors> { return Promise.all([ attachWorkspace(this.cwd), this.execFile(this.eclccPath, this.cwd, this.args([...args, "-M", filePath]), "eclcc", `Cannot find ${this.eclccPath}`) ]).then(([metaWorkspace, execFileResponse]: [Workspace, IExecFile]) => { let checked: string[] = []; try { if (execFileResponse && execFileResponse.stdout && execFileResponse.stdout.length) { checked = metaWorkspace.parseMetaXML(execFileResponse.stdout); } } catch (e: any) { logger.error(`syntaxCheck: Error parsing XML - ${e?.message ?? "unknown"}`); } return new EclccErrors(execFileResponse ? execFileResponse.stderr : "", checked); }); } envCheck(filePath: string, args: string[] = []): Promise<Errors> { return Promise.all([ attachWorkspace(this.cwd), this.execFile(this.envchkPath, this.cwd, this.args([...args, filePath]), "envchk", `Cannot find ${this.envchkPath}`) ]).then(([metaWorkspace, execFileResponse]: [Workspace, IExecFile]) => { return new EnvchkErrors(filePath, execFileResponse ? execFileResponse.stderr : "", []); }); } bundleList(): Promise<IBundle[]> { const bundlesRegEx = /\|(.*)\|(.*)\|(.*)\|/g; return Promise.all([ fetch("https://raw.githubusercontent.com/hpcc-systems/ecl-bundles/master/README.rst") .then(response => response.text()) .then(readme => { const retVal: IBundle[] = []; let m = bundlesRegEx.exec(readme); while (m) { retVal.push({ name: m[1].trim(), description: m[2].trim(), url: m[3].trim() }); m = bundlesRegEx.exec(readme); } return retVal; }), this.execFile(this.eclBundlePath, this.cwd, this.args(["list"]), "ecl-bundle", `Cannot find ${this.eclBundlePath}`) .then(installedText => { return tidyCRLF(installedText.stdout).split("\n"); }).then(installedItems => { const allProps = {}; return Promise.all(installedItems.filter(ii => !!ii).map(ii => { return this.execFile(this.eclBundlePath, this.cwd, this.args(["info", ii]), "ecl-bundle", `Cannot find ${this.eclBundlePath}`) .then(infoText => { return tidyCRLF(infoText.stdout).split("\n"); }).then(info => { const props = {}; info.forEach(line => { const parts = line.split(":"); props[parts.shift().trim()] = parts.join(":").trim(); }); allProps[ii] = { name: ii, props }; }); })).then(() => allProps); }) ]).then(([bundles, installed]) => { bundles.forEach(b => { if (installed[b.name]) { b.props = installed[b.name].props; delete installed[b.name]; } }); for (const key in installed) { bundles.push({ name: key, url: "", description: "", props: installed[key].props }); } return bundles; }).catch(e => { return []; }); } bundleInstall(bundleUrl) { return Promise.all([ attachWorkspace(this.cwd), this.execFile(this.eclBundlePath, this.cwd, this.args(["install", bundleUrl]), "ecl-bundle", `Cannot find ${this.eclBundlePath}`) ]).then(([metaWorkspace, execFileResponse]: [Workspace, IExecFile]) => { return execFileResponse; }); } bundleUninstall(name) { return Promise.all([ attachWorkspace(this.cwd), this.execFile(this.eclBundlePath, this.cwd, this.args(["uninstall", name]), "ecl-bundle", `Cannot find ${this.eclBundlePath}`) ]).then(([metaWorkspace, execFileResponse]: [Workspace, IExecFile]) => { return execFileResponse; }); } private execFile(cmd: string, cwd: string, args: string[], _toolName: string, _notFoundError?: string): Promise<{ code: number, stdout: string, stderr: string }> { return new Promise((resolve, _reject) => { logger.debug(`${cmd} ${args.join(" ")}`); const child = cp.spawn(cmd, args, { cwd }); let stdOut = ""; let stdErr = ""; child.stdout.on("data", (data) => { stdOut += data.toString(); }); child.stderr.on("data", (data) => { stdErr += data.toString(); }); child.on("close", (_code, _signal) => { resolve({ code: _code, stdout: stdOut.trim(), stderr: stdErr.trim() }); }); }); } } function locateClientToolsInFolder(rootFolder: string, clientTools: ClientTools[]) { if (rootFolder) { const hpccSystemsFolder = path.join(rootFolder, "HPCCSystems"); if (fs.existsSync(hpccSystemsFolder) && fs.statSync(hpccSystemsFolder).isDirectory()) { if (os.type() !== "Windows_NT") { const eclccPath = path.join(hpccSystemsFolder, "bin", "eclcc"); if (fs.existsSync(eclccPath)) { clientTools.push(new ClientTools(eclccPath)); } } fs.readdirSync(hpccSystemsFolder).forEach((versionFolder) => { const eclccPath = path.join(hpccSystemsFolder, versionFolder, "clienttools", "bin", "eclcc" + exeExt); if (fs.existsSync(eclccPath)) { const name = path.basename(versionFolder); const version = new Version(name); if (version.exists()) { clientTools.push(new ClientTools(eclccPath)); } } }); } } } let allClientToolsCache: Promise<ClientTools[]>; export function clearAllClientToolsCache() { allClientToolsCache = undefined; } export function locateAllClientTools() { if (allClientToolsCache) return allClientToolsCache; const clientTools: ClientTools[] = []; switch (os.type()) { case "Windows_NT": const rootFolder86 = process.env["ProgramFiles(x86)"] || ""; if (rootFolder86) { locateClientToolsInFolder(rootFolder86, clientTools); } const rootFolder = process.env["ProgramFiles"] || ""; if (rootFolder) { locateClientToolsInFolder(rootFolder, clientTools); } if (!rootFolder86 && !rootFolder) { locateClientToolsInFolder("c:\\Program Files (x86)", clientTools); } break; case "Linux": case "Darwin": locateClientToolsInFolder("/opt", clientTools); break; default: break; } allClientToolsCache = Promise.all(clientTools.map(ct => ct.version())).then(() => { clientTools.sort((l: ClientTools, r: ClientTools) => { return r.versionSync().compare(l.versionSync()); }); return clientTools; }); return allClientToolsCache; } let eclccPathMsg = ""; function logEclccPath(eclccPath: string) { const msg = `Using eclccPath setting: ${eclccPath}`; if (eclccPathMsg !== msg) { logger.info(msg); eclccPathMsg = msg; } } export function locateClientTools(overridePath: string = "", build: string = "", cwd: string = ".", includeFolders: string[] = [], legacyMode: boolean = false, args: string[] = []): Promise<ClientTools> { if (overridePath && fs.existsSync(overridePath)) { logEclccPath(overridePath); return Promise.resolve(new ClientTools(overridePath, cwd, includeFolders, legacyMode, args)); } return locateAllClientTools().then((allClientToolsCache2) => { if (!allClientToolsCache2.length) { throw new Error("Unable to locate ECL Client Tools."); } const buildVersion = new Version(build); let latest: ClientTools | undefined; let bestMajor: ClientTools | undefined; for (const ct of allClientToolsCache2) { const ctVersion = ct.versionSync(); if (ctVersion.exists()) { if (!latest) latest = ct; if (!bestMajor && buildVersion.major === ctVersion.major) bestMajor = ct; if (buildVersion.major === ctVersion.major && buildVersion.minor === ctVersion.minor) return ct.clone(cwd, includeFolders, legacyMode, args); } } const best: ClientTools = bestMajor || latest!; logEclccPath(best.eclccPath); return best.clone(cwd, includeFolders, legacyMode, args); }); }