Commit 6fc69131 authored by Daniel Smith's avatar Daniel Smith

First pass refactor and file splitting of NodeJS server

This commit breaks out and rewrites many of the main components
of the scheduler and webserver into modules which share
a global instance of variables and event emitters.
parent 10e5d060
This diff is collapsed.
{
"BASE_URL": "localhost",
"HTTP_PORT": 8080,
"SMTP_SERVER": "smtp.intra.qt.io",
"SMTP_PORT": "25",
"VS_BUILD_TOOLS": "C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools/Common7/Tools/VsDevCmd.bat",
"expectedDurations": {
"bisect": 14400000,
"singleCommit": 1350000,
"twoCommit": 2750000
},
"agents": []
}
\ No newline at end of file
This diff is collapsed.
...@@ -22,9 +22,7 @@ builddir = os.path.join(basedir, "QtBuild") ...@@ -22,9 +22,7 @@ builddir = os.path.join(basedir, "QtBuild")
installdir = os.path.join(builddir, "Install") installdir = os.path.join(builddir, "Install")
isWindowsOS = (platform.system() == 'Windows') isWindowsOS = (platform.system() == 'Windows')
compiler = ["BuildConsole", f"/COMMAND={os.path.join(basedir, 'JOM', 'jom.exe')}"] if isWindowsOS else ["make", f"-j{os.environ.get('BUILD_CORES') or os.cpu_count()}"] compiler = ["BuildConsole", f"/COMMAND={os.path.join(basedir, 'JOM', 'jom.exe')}"] if isWindowsOS else ["make", f"-j{os.environ.get('BUILD_CORES') or os.cpu_count()}"]
print(compiler)
installer = [compiler[0], compiler[1] + " install"] if isWindowsOS else compiler + ["install"] installer = [compiler[0], compiler[1] + " install"] if isWindowsOS else compiler + ["install"]
print(installer)
exeExt = '.exe' if isWindowsOS else '' exeExt = '.exe' if isWindowsOS else ''
goodBaselineScore = 0 goodBaselineScore = 0
badBaselineScore = 0 badBaselineScore = 0
...@@ -1063,7 +1061,6 @@ def reportStatusUpdate(status_msg=""): ...@@ -1063,7 +1061,6 @@ def reportStatusUpdate(status_msg=""):
if __name__ == "__main__": if __name__ == "__main__":
atexit.register(on_exit) atexit.register(on_exit)
args = parseArgs() args = parseArgs()
print(args.patches, type(args.patches))
prepareEnv() prepareEnv()
initRepositories() initRepositories()
if not validateCommits() or not validateTest(): # Validate inputs! if not validateCommits() or not validateTest(): # Validate inputs!
......
This diff is collapsed.
exports.id = "testHandler";
const { spawn } = require("child_process");
const moment = require("moment");
let momentDurationFormatSetup = require("moment-duration-format");
momentDurationFormatSetup(moment);
const os = require("os");
const myname = os.hostname();
let pathmodule = require("path");
let fs = require("fs");
const del = require("del");
const validator = require("email-validator");
class testHandler {
constructor(notifyJob, globals) {
this.notifyJob = notifyJob;
this.globals = globals;
this.runTest = this.runTest.bind(this);
this.notifyJob.on("jobDone", this.jobDone.bind(this));
}
runTest(test, callback) {
let _this = this;
// Run the given test on this host
console.log("Now executing test with params:");
console.log(test);
console.log(`Job ${test.jobHash.slice(0, 10)} is expected to take ${
moment.duration(test.expectedDuration).humanize()}`);
// Set arguments for regressionfinder.py to really do the build and run the test
let args = [
"./regressionFinder.py",
"--jobName", test.jobHash,
"--branch", test.branch,
"--moduleToTest", test.module,
"--patches", test.patches,
"--benchmark",
test.test_name ? test.test_name : test.custom_benchmark_file.tempFilePath.replace(/\\/gi, "/")
]
if (test.testType == "bisect") {
args.push(
"--knownBadRev", test.bad_commit,
"--knownGoodRev", test.good_commit,
"--regressionTarget", test.expected_regression
);
} else if (test.testType == "singleCommit") {
args.push("--testSingleCommit", test.commit);
if (test.firstCommitBuildOnHead)
args.push("--FirstBuildOnHead");
} else if (test.testType == "twoCommit") {
args.push("--testTwoCommit", test.firstCommit + "," + test.secondCommit);
if (test.firstCommitBuildOnHead)
args.push("--FirstBuildOnHead");
if (test.secondCommitBuildOnHead)
args.push("--SecondBuildOnHead");
} else if (test.testType == "STARTUP") {
args = ["./regressionFinder.py", "--setupEnv"];
} else {
console.log(`ERROR: Unrecognized test type of ${test.testType}! This should not happen.`);
}
// Set the OpenGL backend override if selected in the web interface and this system is Windows.
if (test.openGLBackend && test.platform == "win32")
args.push("--OpenGLBackend", test.openGLBackend);
if (test.environment) {
if (Array.isArray(test.environment))
args.push("--environment", test.environment.join(","));
else
args.push("--environment", test.environment);
}
// Python3 for windows will simply be 'python'.
let pythonexe = os.platform() == "win32" ? "python" : "python3";
console.log("Launching " + pythonexe + " " + args.join(" "));
test.status = "running";
const testProcess = spawn(
pythonexe, args,
os.platform() == "win32"
? { detached: false }
: { detached: true }
);
const logFile = fs.createWriteStream(`logs/${myname}/${test.jobHash}.txt`);
testProcess.stdout.on("data", (data) => {
logFile.write(data); // log to file
});
testProcess.stderr.on("data", (data) => {
logFile.write(data); // log to file
});
testProcess.on("close", (code) => {
console.log(`Process for job ${test.jobHash.slice(0, 10)} exited with code ${code}`);
test.logURL = `http://${_this.globals.baseURL}:${_this.globals.webPort}/logs/${
myname}/${test.jobHash}.txt`;
test.finishTime = Date.now();
test.actualDuration = test.finishTime - test.startTime;
if (!_this.globals.isPrimaryHost) {
// All logs should get copied to the scheduler host.
const { exec } = require("child_process");
exec(
`${os.platform() == "win32" ? "copy" : "cp"} ${pathmodule.join(
__dirname,
"logs",
myname,
test.jobHash + ".txt"
)} ${pathmodule.join(os.homedir(), "QMLBenchRegressionLogs", myname)}`,
(error, stdout, stderr) => {
if (error)
console.error(`Failed tocopy the log file: ${error}`);
}
);
exec(
`${os.platform() == "win32" ? "copy" : "cp"} ${pathmodule.join(
__dirname,
"logs",
myname,
"results",
test.jobHash + ".json"
)} ${pathmodule.join(os.homedir(), "QMLBenchRegressionLogs", myname, "results")}`,
(error, stdout, stderr) => {
if (error)
console.error(`Failed to copy the log file: ${error}`);
}
);
}
if (test.status != "cancelling") {
// If the test is in cancelling status, it means that someone pressed the cancel button.
// So provided it exited naturally, try to get test results from the run,
// even if the job failed and hard-exited.
try {
const f = fs.readFileSync(`logs/${test.test_hosts[0]}/results/${test.jobHash}.json`);
test.resultJSON = JSON.parse(f);
} catch (err) {
console.log("ERROR: Failed to read the results file. Maybe the test failed to finish...");
// console.log(err);
}
if (code != 0 || code == "null") {
console.log(test.title, "Failed: with code", code);
test.status = "failed";
} else {
console.log(test.title, "finished with code ", code);
test.status = "finished";
test.status_msg = "Job complete!";
// Update the new expected durations for this server/job type
_this.globals.expectedDurations[test.testType] =
Math.ceil((_this.globals.expectedDurations[test.testType] + test.actualDuration) / 2);
console.log(`New Duration for ${test.testType}: ${
_this.globals.expectedDurations[test.testType]}`);
}
} else {
// The job was in cancelling status when it exited. Set it to cancelled now.
test.status = "cancelled";
test.status_msg = "Cancelled";
}
if (test.custom_benchmark_file) {
if (test.custom_benchmark_file.tempFilePath) {
(async () => {
let re = /\\/gi;
// can't use path.normalize here. del@5.0.0 module doesn't
// seem to work with windows style paths at the moment.
const deletedPaths = await del(
[pathmodule.dirname(test.custom_benchmark_file.tempFilePath).replace(re, "/")],
{ force: true }
);
console.log("Deleted files and directories:\n", deletedPaths.join("\n"));
})((error) => {
console.log(error);
});
}
}
// Provide a callback option to do custom processing on job completion.
if (callback)
callback();
// Emit the JobDone signal to do some more tasks and prep for the next job in queue.
_this.notifyJob.emit("jobDone");
// saveTests(); //Backup the JSON file on disk for the status web page. // Not implemented!
});
return testProcess;
}
jobDone() {
let _this = this;
// A host will emit this signal when an active process completes, regardless of success
// Clean up and delete the child_process handle from the job.
// Attempting to convert the process handle to a string when updating the master
// host would result in an error, so it has to go anyway.
delete _this.globals.runningJob["processHandle"];
const job = _this.globals.runningJob; // make a shallow copy
console.log(`finished job ${job.jobHash}`);
_this.globals.runningJob = {}; // empty the currently running local job slot.
_this.globals.completedJobs.push(job); // Save the job to the finished list
_this.globals.locked = false; // unlock the job queue
if (job.remote_job) {
_this.notifyJob.emit("updateRemoteJob", job, "finished"); // Notify scheduler
if (_this.globals.jobQueue.length != 0) {
// Delay starting the next job for a few seconds to
// prevent race conditions and make a better UI experience.
setTimeout(function () {
_this.notifyJob.emit("newLocalJob");
}, 3000);
}
} else {
if (_this.globals.jobQueue.length != 0)
_this.notifyJob.emit("newLocalJob"); // If we're not acting as a remote host, we can just execute immediately.
}
if (validator.validate(job.owner)) {
// Send an email to the job owner if they provided a valid email address
_this.toolbox.sendEmail(job);
}
}
}
module.exports = testHandler;
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment