const config = require("./pluginConfig.json");
const vscode = require("vscode");
const request = require("request");
const { initParams } = require("request");
const path = require("path");
const os = require("os");
const { exec } = require("child_process");
const { throws, rejects, fail } = require("assert");
const { prependOnceListener } = require("process");
const { fstat } = require("fs");
const { time } = require("console");
const { privateEncrypt } = require("crypto");
const { win32 } = require("path");
const _serverURL = config.serverURL;
let __userID = undefined;
let __problemID = undefined;
let __problem = undefined;
let language;
let isActive = false;
let failingTestID = [];
const pyvers = os.platform() === "win32" ? "python" : "python3";
var startTime;
var endTime;
let current = 0;
let total = 0;
let rightWindow;
let timer;
const log = true;
const start = Date.now();
let events = [];
const register = (email) => {
let res = request.post(
`${_serverURL}/register`,
{
json: { email },
},
function (error, response) {
if (!error && response.statusCode == 200) {
return response.body.userId;
} else {
return undefined;
}
}
);
return res;
};
function isAuthenticated(email) {
return new Promise((res, rej) => {
request.post(
`${_serverURL}/login`,
{
json: { email },
},
function (error, response) {
if (!error && response.statusCode == 200) {
let id = response.body;
if (id.userid) {
res(id);
} else {
res(undefined);
}
} else {
rej(undefined);
}
}
);
});
}
/**
* This function is called when the extension starts.
* It begins with the command Start Testing.
* and ends with the command Stop Testing in the Command Palette.
* @param {vscode.ExtensionContext} context
* @inner
*/
function activate(context) {
let disposable = vscode.commands.registerCommand(
"keylogger-mvp.startTesting",
// When the "Start Testing" command is run this arrow function gets run
async () => {
// Calls the function to authenticate the email
isActive = true;
authenticate();
}
);
let next = vscode.commands.registerCommand(
"keylogger-mvp.nextTest",
async () => {
events = [];
nextTest();
runTest();
}
);
let closing = vscode.commands.registerCommand(
// When the "Stop Testing" command is run this arrow function gets run
"keylogger-mvp.stopTesting",
() => {
isActive = false;
clearTimeout(timer)
writeState();
finishTesting();
survey();
}
);
// Listen to the provided commands
context.subscriptions.push(next);
context.subscriptions.push(disposable);
context.subscriptions.push(closing);
}
function end() {
vscode.window.showInformationMessage("You have run out of time");
writeState();
finishTesting();
survey();
}
/**
* Once the program finishes testing this function is called to prompt the user to fill out a survey.
* @inner
*/
function survey() {
vscode.window.showInformationMessage(
`Please fill out a survey here: ${config.surveyLink}`
);
}
/**
* Displays a text box for user input
* @param {boolean} triedBefore
* @inner
*/
async function authenticate(triedBefore = false) {
let title = "Enter Your email";
let prompt = "Enter your email";
if (triedBefore) {
title = "Incorrect information... Try again";
}
vscode.window
.showInputBox({
title,
prompt,
})
.then(async (a) => {
// If the email is correct begin testing.
let isAuth;
try {
isAuth = await isAuthenticated(a);
} catch (e) {
console.log(e);
}
if (isAuth && isAuth.userid) {
(__userID = isAuth.userid),
setProblem(await fetchProblem()),
(rightWindow = init()),
recordKeyPresses(),
recordCursorMovements();
runTest();
// If email is wrong have them restart and try again
} else {
authenticate(true);
}
});
}
/**
* Runs tests on a users local machine to check to see if they are passing
* the test they are on.
* @inner
*/
function runTest() {
if (isActive) {
let pathOfPy = (os.platform() === 'win32')? `${__dirname}\\exec\\`:`${__dirname}/exec/`;
if (os.platform() == 'win32')
pathOfPy.replace(/\//g, "\\")
const fs = require("fs");
let json = JSON.stringify({ problem: __problem });
fs.writeFileSync(`${pathOfPy}/prob.json`, json)
let uri = decodeURIComponent(vscode.window.activeTextEditor.document.uri.toString())
.toString()
.substring(7);
uri = (os.platform() === "win32")? uri.substring(1).replace(/\//g, "\\"):uri
if (language.toLowerCase() === "python") {
exec(
`cd ${pathOfPy}; ${pyvers} replacer.py ${uri}; ${pyvers} exec.py`,
(os.platform() === "win32")? {'shell':'powershell.exe'}: {},
(err, stdout, stderr) => {
if (err || stderr) {
console.log(err);
current = 0;
failingTestID = [];
for (let i = 0; i < __problem.testCases.length; i++) {
failingTestID.push(i);
}
} else {
failingTestID = stdout.split("\n");
failingTestID.pop();
current = total - failingTestID.length;
if (current == total) {
writeState();
finishTesting();
survey();
}
}
}
);
} else if (language.toLowerCase() === "coq") {
exec(
`cd ${pathOfPy}; ${pyvers} replacer.py ${uri}`,
(err, stdout, stderr) => {}
);
exec(`coqc ${pathOfPy}run.v`, (err, stdout, stderr) => {
if (err || stderr) {
current = 0;
} else current = 1;
});
}
updateStatus();
}
}
/**
* This method is called when the extension is deactivated, it is unreliable and most cleanup should be done on "Stop Testing"
* @inner
*/
function deactivate() {}
module.exports = {
activate,
deactivate,
};
/**
* This function prompts the user to choose a langugae from a predetermined set of languages in a dropdown bar.
* It stores the selected option in a global variable.
* @inner
*/
function languageOptions() {
// displays the possible languages the user can choose from
vscode.window
.showQuickPick(["Python", "C", "Coq", "Java"], {
title: "Language Selector",
placeHolder: "Pick your language from the dropdown box.",
})
.then((a) => {
// once selected the langugae is stored and calls the test options function to list the options
language = a;
testOptions();
});
}
/**
* This function prompts the user to choose a problem set from the options listed.
* It can have different amounts of possible problem sets for each language.
* It then stores the selected problem set in a variable.
* @inner
*/
function testOptions() {
// Stores the possible problem sets to be selected depending on the language chosen
const python = [
"Problem Set 1",
"Problem Set 2",
"Problem Set 3",
"Problem Set 4",
];
const c = ["Problem Set 1", "Problem Set 2"];
const coq = ["Problem Set 1", "Problem Set 2", "Problem Set 3"];
const java = ["Problem Set 1", "Problem Set 2"];
let select;
// depending on which language is chosen it matches the language to a list of problem sets
switch (language) {
case "Python":
select = python;
break;
case "C":
select = c;
break;
case "Coq":
select = coq;
break;
case "Java":
select = java;
break;
}
// displays the problems to then be chosen by the user depending on which language was selected
vscode.window
.showQuickPick(select, {
title: "Problem Selector",
placeHolder: "Pick your Problem Set from the dropdown box.",
})
.then((a) => {
init();
recordKeyPresses();
recordCursorMovements();
});
}
/**
* Initializes the panels that display the test question and the amount of tests passed.
* @returns a vscode webViewPanel
* @inner
*/
function init() {
const panel = vscode.window.createWebviewPanel(
"CodeCheck",
"Status",
vscode.ViewColumn.Two,
{}
);
const panel2 = vscode.window.createWebviewPanel(
"Testview",
"Problem",
vscode.ViewColumn.Three
);
panel.webview.html =
"<h2>Start typing your solution and tests will be executed automatically</h2>";
panel2.webview.html = __problem["html"];
return panel;
}
/**
* Records a users key press for any text change to the document.
* @inner
*/
function recordKeyPresses() {
// On document change handle event
if (isActive) {
vscode.workspace.onDidChangeTextDocument((event) => {
// For each content change store the location in file, changed text and time of event
event.contentChanges.forEach((contentChange) => {
const e = {
startLine: contentChange.range.start.line,
startChar: contentChange.range.start.character,
endLine: contentChange.range.start.line,
endChar: contentChange.range.end.character,
textChange: contentChange.text,
testsPassed: [],
time: Date.now(),
};
events.push(e);
vscode.workspace.saveAll(true);
runTest();
});
});
}
}
/**
* Records the position of the cursor inside the text box
* @inner
*/
function recordCursorMovements() {
if (isActive) {
vscode.window.onDidChangeTextEditorSelection((event) => {
event.selections.forEach((selection) => {
const e = {
active: selection.active,
anchor: selection.anchor,
end: selection.end,
isReversed: selection.isReversed,
start: selection.start,
};
// Push events to queue
events.push(e);
});
});
}
}
/**
* This function updates the status of the passing tests window.
* Every time a test passes it increases the amount of tests passed.
* If a test fails it decreases the amount of tests passed.
* @inner
*/
function updateStatus() {
rightWindow.webview.html = getWebViewContent(current, total);
}
async function fetchProblem(userID, problemName) {
let body = {};
let param = "";
if (userID) {
param = `?userid=${userID}`;
} else if (problemName) {
param = `?name=${problemName}`;
}
var out = {};
return new Promise((res, rej) => {
request.get(
{
url: `${_serverURL}/problem${param}`,
json: true,
},
(error, response) => {
if (!error && response.statusCode == 200) {
res(response.body.problem);
} else {
rej(response);
}
}
);
});
}
/**
* This is used to create an empty event upon the end of testing.
* @inner
*/
function finishTesting() {
events = [];
}
/**
* This sets the problem to be displayed as well as creates a timer that will be
* set the amount of time allotted to finish the test. The timer resets every time
* a new problem appears.
* @param {any} problem
* @inner
*/
async function setProblem(problem) {
startTime = new Date();
endTime = new Date();
endTime.setMinutes(endTime.getMinutes() + 20);
var t =
startTime.getHours() +
"hr " +
startTime.getMinutes() +
"min " +
startTime.getSeconds() +
"sec";
var te =
endTime.getHours() +
"hr " +
endTime.getMinutes() +
"min " +
endTime.getSeconds() +
"sec";
timer = setTimeout(end, config.timerLength);
vscode.window.showInformationMessage("You started at " + t);
vscode.window.showInformationMessage(
"You have until " + te + " to complete all tests"
);
current = 0;
__problem = problem;
__problemID = problem._id;
language = problem.lang;
if (__problem.lang.toLowerCase() === "coq") total = 1;
else total = __problem.testCases.length;
}
/**
* A user inputs into an Input Box the next problem they will do. If they
* do not specify a test a random one is chosen.
* @inner
*/
async function nextTest() {
writeState();
let problemName = await vscode.window.showInputBox({
title: "Choose Problem Name",
prompt: "Not providing a name will result in a random problem",
});
clearTimeout(timer)
setProblem(await fetchProblem(problemName));
rightWindow = init();
}
/**
* Creates the window to display the tests and if they are passing or not.
* @param {any} passing
* @param {any} tests
* @returns a vscode web panel
* @inner
*/
function getWebViewContent(passing, tests) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Problem</title>
</head>
<body>
<h1> Passing ${passing}/${tests} tests! </h1>
${getFailingTestDetails(failingTestID)}
</body>
</html>`;
}
/**
* Returns the details of all the tests that are failing
* @param {any} failingTestID
* @returns result of failed tests
* @inner
*/
function getFailingTestDetails(failingTestID) {
if (failingTestID.length === 0) {
return "";
}
let result = "<h2>Failed Tests:</h2><ul>";
for (let i = 0; i < failingTestID.length; i++) {
result += `<li>Input: ${
__problem.testCases[failingTestID[i]]
} <br>Expected Output: ${__problem.answers[failingTestID[i]]}`;
}
result += "</ul>";
return result;
}
/**
* Writes the state in a post request to the server.
* @inner
*/
function writeState() {
if (!log) return;
request.post(
`${_serverURL}/save`,
{
json: {
userID: __userID,
problemID: __problemID,
start,
end: Date.now(),
events,
},
},
function (error, response) {
if (!error && response.statusCode == 200) {
console.log(response.statusCode);
} else {
console.log(response);
}
}
);
}