@hpcc-js/comms
Version:
hpcc-js - Communications
629 lines (567 loc) • 23.3 kB
text/typescript
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);
});
}