npm-groovy-lint
Version:
Lint, format and auto-fix your Groovy / Jenkinsfile / Gradle files
448 lines (408 loc) • 18.5 kB
JavaScript
// Call CodeNarc by server or java
import axios from "axios";
import * as cliProgress from "cli-progress";
import Debug from "debug";
const debug = Debug("npm-groovy-lint");
const trace = Debug("npm-groovy-lint-trace");
import { JavaCaller } from "java-caller";
import { optionsDefinition } from "./options.js";
import { performance } from "node:perf_hooks";
import c from "chalk";
import findJavaHome from "find-java-home";
import * as path from "path";
import { fileURLToPath } from "node:url";
// Request over IPv4 because Java typically prefers it.
import http from "http";
axios.defaults.httpAgent = new http.Agent({ family: 4, keepAlive: true });
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export class CodeNarcCaller {
args = [];
options;
codenarcArgs;
javaExecutable = "java";
additionalJavaArgs = ["-Xms256m", "-Xmx2048m"];
execOpts;
serverStatus;
bar;
barTimer;
execTimeout = 240000;
javaCallerOptions = {
codeNarcServer: {
minimumJavaVersion: 17,
maximumJavaVersion: 17,
rootPath: __dirname,
jar: "java/CodeNarcServer.jar",
},
codeNarcJava: {
minimumJavaVersion: 17,
maximumJavaVersion: 17,
rootPath: __dirname,
jar: "java/CodeNarcServer.jar",
},
};
constructor(codenarcArgs1, serverStatus1, args1, options1, execOpts1) {
this.args = args1;
this.codenarcArgs = codenarcArgs1;
this.options = options1;
this.serverStatus = serverStatus1;
this.execOpts = execOpts1;
this.javaExecutable = options1.javaexecutable || this.javaExecutable;
this.additionalJavaArgs = options1.javaoptions ? options1.javaoptions.split(",") : this.additionalJavaArgs;
}
// Call local CodeNarc server if running
async callCodeNarcServer(startServerTried = false) {
// If use of --codenarcargs, get default values for CodeNarcServer host & port
const requestUri = this.getCodeNarcServerUri() + "/request";
// Call CodeNarc server
const axiosConfig = {
method: "post",
url: requestUri,
data: {
codeNarcArgs: this.codenarcArgs,
codeNarcBaseDir: this.execOpts.codeNarcBaseDir,
codeNarcIncludes: this.execOpts.codeNarcIncludes,
codeNarcExcludes: this.execOpts.codeNarcExcludes,
parse: this.options.parse !== false && this.execOpts.onlyCodeNarc === false,
fileList: this.execOpts.groovyFileName ? [this.execOpts.groovyFileName] : this.execOpts.inputFileList,
requestKey: this.execOpts.requestKey || null,
},
timeout: 600000,
};
trace(`CALL CodeNarcServer with ${JSON.stringify(axiosConfig, null, 2)}`);
let response;
try {
const startCodeNarc = performance.now();
response = await axios.request(axiosConfig);
this.serverStatus = "running";
const elapsed = parseInt(performance.now() - startCodeNarc, 10);
debug(`CodeNarcServer call result: (${response.status}) ${elapsed}ms`);
} catch (e) {
// If server not started , start it and try again
debug(`callCodeNarcServer code: ${e.code} error: ${e.message}`);
if (
(startServerTried === false,
e.code && ["ECONNREFUSED", "ETIMEDOUT"].includes(e.code) && ["unknown", "running"].includes(this.serverStatus)) // running is here in case the Server auto-killed itself at its expiration time
) {
if ((await this.startCodeNarcServer()) && this.serverStatus === "running") {
return await this.callCodeNarcServer(true);
}
} else if (e.code === "ERR_BAD_REQUEST") {
// Bad request.
return {
status: e.response.data.statusCode,
error: {
msg: `exception: ${e.response.data.exceptionType} message: ${e.response.data.errorMessage}`,
stack: e.stack,
responseData: e.response.data.errorDtl,
},
};
} else if (e.code === "ECONNRESET") {
// The server was shutdown just retry.
if (startServerTried === false && (await this.startCodeNarcServer()) && this.serverStatus === "running") {
return await this.callCodeNarcServer(true);
}
// Should this really be cancelled, as the Groovy says it should return:
// respObj.status = 'cancelledByDuplicateRequest'
// respObj.statusCode = 444
return {
status: 9,
};
} else {
console.error(
c.red(
"CodeNarcServer unexpected error:\n" +
JSON.stringify(e, null, 2) +
"\n" +
JSON.stringify(e.response?.data?.errorDtl, null, 2),
),
);
}
this.serverStatus = "error";
return {
status: 2,
error: {
msg: e.message,
stack: e.stack,
responseData: e.response?.data?.errorDtl,
},
};
}
if (response.data.status === "success") {
// Success result
return {
codeNarcJsonResult: response.data.jsonResult,
fileList: response.data.fileList,
parseErrors: response.data.parseErrors,
codeNarcStdOut: response.data.stdout,
codeNarcStdErr: undefined,
status: 0,
};
}
if (response.data.status === "cancelledByDuplicateRequest") {
// Cancelled codeNarcAction (duplicate)
return {
codeNarcStdOut: undefined,
codeNarcStdErr: undefined,
status: 9,
};
}
// Codenarc error
return {
fileList: response.data.fileList,
parseErrors: response.data.parseErrors,
codeNarcJsonResult: response.data.jsonResult,
codeNarcStdOut: response.data.stdout,
codeNarcStdErr: response.data.errorDtl,
status: 1,
error: {
msg: `exception: ${response.data.exceptionType} message: ${response.data.errorMessage}`,
msgDtl: {
parseErrors: response.data.parseErrors,
stdout: response.data.stdout,
stderr: response.data.errorDtl,
},
},
};
}
// Call CodeNard java class
async callCodeNarcJava(secondAttempt = false) {
// Build java codenarc command (request to launch server for next call except if --noserver is sent)
const scriptArgs = [...this.codenarcArgs]; // Take a copy of the args so we can modify it.
if (this.options.parse !== false && this.execOpts.onlyCodeNarc === false) {
scriptArgs.unshift("--parse");
}
if (this.execOpts.groovyFileName) {
scriptArgs.unshift("--file", this.execOpts.groovyFileName);
} else if (this.execOpts.inputFileList) {
this.execOpts.inputFileList.forEach((file) => {
scriptArgs.unshift("--file", file);
});
}
// Start progress bar
trace(`CALL CodeNarcJava with ${scriptArgs.join(" ")}`);
this.bar = new cliProgress.SingleBar(
{
format: "[{bar}] Running CodeNarc for {duration_formatted}",
hideCursor: true,
clearOnComplete: true,
},
cliProgress.Presets.shades_classic,
);
this.bar.start(10, 1);
this.barTimer = setInterval(() => {
this.bar.increment();
if (this.bar.value === 9) {
this.bar.update(1);
}
}, 500);
const javaCallerMode = secondAttempt === false ? "codeNarcServer" : "codeNarcJava";
const javaCallerOpts = this.javaCallerOptions[javaCallerMode];
javaCallerOpts.javaExecutable = this.javaExecutable;
javaCallerOpts.additionalJavaArgs = this.additionalJavaArgs;
const javaCaller = new JavaCaller(javaCallerOpts);
const javaResult = await javaCaller.run(scriptArgs, { detached: false, windowsVerbatimArguments: false });
clearInterval(this.barTimer);
this.bar.stop();
if ([666, 1].includes(javaResult.status)) {
if (!secondAttempt) {
// If failure (missing class com.nvuillam.CodeNarcServer for example, it can happen on Linux, let's try the original org.codenarc.CodeNarc class)
trace(`Error calling CodeNarcServer via java: ${JSON.stringify(javaResult)}`);
return await this.callCodeNarcJava(true);
} else {
let reason = "Reason: unknown";
// Check if the reason is "node" missing in PATH
if (
javaResult.stderr &&
(/node(.*)is not recognized as an internal or external command/gm.test(javaResult.stderr) ||
/node: command not found/gm.test(javaResult.stderr))
) {
reason =
"It seems node.js has not been found on your computer. Please install a recent node.js: https://nodejs.org/en/download/\nIf node is already installed, make sure your PATH contains node installation folder: https://love2dev.com/blog/node-is-not-recognized-as-an-internal-or-external-command/";
} else {
await findJavaHome({ allowJre: true }, (err) => {
if (err) {
reason =
"Java is required to run npm-groovy-lint, as CodeNarc is written in Java/Groovy. Please install Java (version 8 minimum) https://www.java.com/download";
}
});
}
return {
codeNarcStdErr: javaResult.stderr,
status: 2,
error: {
msg: `Fatal error while calling CodeNarc\n${reason}\n${javaResult.stderr}`,
},
};
}
}
const response = await this.getCodeNarcServerJson(javaResult.stdout);
return {
codeNarcJsonResult: response.jsonResult,
fileList: response.fileList,
parseErrors: response.parseErrors,
codeNarcStdOut: javaResult.stdout,
codeNarcStdErr: javaResult.stderr,
status: 0,
};
}
// Start CodeNarc server so it can be called via Http just after
async startCodeNarcServer() {
this.serverStatus = "unknown";
const maxAttemptTimeMs = 10000;
const scriptArgs = ["--server"];
const serverPingUri = this.getCodeNarcServerUri() + "/ping";
debug(`ATTEMPT to start CodeNarcServer with ${scriptArgs.join(" ")}`);
// Start server using java (we don't care the promise result, as the following promise will poll the server)
const javaCallerOpts = this.javaCallerOptions["codeNarcServer"];
javaCallerOpts.javaExecutable = this.javaExecutable;
javaCallerOpts.additionalJavaArgs = this.additionalJavaArgs;
const javaCaller = new JavaCaller(javaCallerOpts);
const javaResult = await javaCaller.run(scriptArgs, { detached: true, waitForErrorMs: 500 });
// Store the process so we can stop it later.
this.codeNarcProcess = javaResult.childJavaProcess;
trace(`javaResult: ${JSON.stringify(javaResult)}`);
if ([666, 1].includes(javaResult.status)) {
console.error(c.red(`Unable to start CodeNarc server: ${JSON.stringify(javaResult)}`));
console.error(c.grey(JSON.stringify(scriptArgs)));
this.serverStatus = "error";
return false;
}
// Poll it until it is ready
const start = performance.now();
let notified = false;
let interval;
await new Promise((resolve) => {
interval = setInterval(() => {
debug(
`pinging CodeNarcServer at ${serverPingUri} notified: ${notified}, serverStatus: ${
this.serverStatus
}, since: ${performance.now() - start}, maxAttemptTimeMs: ${maxAttemptTimeMs}`,
);
axios
.get(serverPingUri)
.then((response) => {
if (response.status === 200) {
// Server is correctly started, as he replied to the ping request
this.serverStatus = "running";
if (notified === false) {
notified = true;
debug(`SUCCESS: CodeNarcServer is running`);
clearInterval(interval);
resolve();
}
} else if (notified === false && this.serverStatus === "unknown" && performance.now() - start > maxAttemptTimeMs) {
// Timeout has been reached.
let since = performance.now() - start;
debug(`Ping timeout after ${since}ms status: ${response.status}`);
this.declareServerError({ message: `Timeout after ${since}ms} status: ${response.status}` }, interval);
resolve();
}
})
.catch((e) => {
debug(`Ping code: ${e.code} message: ${e.message}`);
let since = performance.now() - start;
if (notified === false && this.serverStatus === "unknown" && since > maxAttemptTimeMs) {
// Timeout has been reached
debug(`Ping timeout after ${maxAttemptTimeMs}ms`);
this.declareServerError({ message: `Timeout after ${since}ms error: ${e}` }, interval);
resolve();
}
});
}, 400);
});
if (this.serverStatus === "running") {
debug(c.green(`GroovyLint: Started CodeNarc Server`));
return true;
}
return false;
}
// Kill CodeNarc process if running.
killCodeNarcProcess() {
if (this.codeNarcProcess) {
this.codeNarcProcess.kill("SIGKILL");
delete this.codeNarcProcess;
return "CodeNarcServer killed";
}
return "";
}
// Stop polling and log error
declareServerError(e, interval) {
// Kill off the process as it is not responding.
this.killCodeNarcProcess();
this.serverStatus = "error";
if (interval) {
clearInterval(interval);
}
const errMsg = "GroovyLint: Unable to start CodeNarc Server. Use --noserver if you do not even want to try";
debug(errMsg);
debug(e.message);
console.error(c.grey(errMsg));
}
// Kill CodeNarc server.
async killCodeNarcServer() {
// Try by process first as it's more reliable.
let outputString = this.killCodeNarcProcess();
if (outputString) {
return outputString;
}
// Process kill wasn't possible, so try sending a kill http request.
const killUri = this.getCodeNarcServerUri() + "/kill";
try {
const response = await axios.post(killUri, { timeout: 1000 });
if (response.data.status === "killed") {
outputString = "CodeNarcServer terminated";
} else {
outputString = "Error killing CodeNarcServer";
}
} catch (e) {
if (e.stack.includes("connResetException") || e.message.includes("socket hang up")) {
outputString = "CodeNarcServer terminated";
} else {
// This should be ECONNREFUSED.
debug(`CodeNarcServer kill request failed: ${e}`);
outputString = `CodeNarcServer was not running`;
}
}
// Wait for the server to stop otherwise when we try to start it
// again it it's likely to fail due to an port in use error.
const serverPingUri = this.getCodeNarcServerUri() + "/ping";
let interval;
await new Promise((resolve) => {
interval = setInterval(() => {
debug(`pinging CodeNarcServer at ${serverPingUri} serverStatus: ${this.serverStatus}`);
axios
.get(serverPingUri)
.then((response) => {
debug(`ping response: ${response.status}`);
})
.catch((e) => {
debug(`Ping code: ${e.code} message: ${e.message}`);
clearInterval(interval);
resolve();
});
}, 400);
});
trace(`killCodeNarcServer: ${outputString}`);
return outputString;
}
// Return CodeNarc server URI
getCodeNarcServerUri() {
// If use of --codenarcargs, get default values for CodeNarcServer host & port
const serverOptions = optionsDefinition.parse({});
return (this.options.serverhost || serverOptions.serverhost) + ":" + (this.options.serverport || serverOptions.serverport);
}
/**
* Parse JSON result from CodeNarcServer.
*
* @param {string} response the response from CodeNarcServer
* @returns {Promise<*>}
* @private
*/
async getCodeNarcServerJson(response) {
try {
return JSON.parse(response);
} catch (e) {
return { err: `Unable to parse ${response}: ${e.message}` };
}
}
}