diff --git a/backend/src/Configuration.ts b/backend/src/Configuration.ts index 2cd8f15..541619a 100644 --- a/backend/src/Configuration.ts +++ b/backend/src/Configuration.ts @@ -1,3 +1,4 @@ +import { AssignmentData } from "./database/Persister"; import { EnvironmentDescription } from "./Environment"; // Configuration.ts holds the main configuration regarding the assignments that can be deployed, its contents can also be red from MongoDB, which would make this file obsolete @@ -1141,7 +1142,15 @@ environments.set("CC-Lab-1", { assignmentLabSheet: "/home/p4/labs/lab1/README.md", }); -export default environments; +function getEnvironments(): Map { + return new Map( + Array.from(environments.entries()) + .map(([key, value]) => { + updateEnvironment(value); + return [key, value]; + }) + ); +} export function updateEnvironments( updatedEnvironments: Map, @@ -1149,6 +1158,19 @@ export function updateEnvironments( environments.clear(); updatedEnvironments.forEach((value, key) => { + updateEnvironment(value) environments.set(key, value); }); } + +export function updateEnvironment(env: EnvironmentDescription | AssignmentData): void { + const bonusPoints = env.steps?.reduce((sum, step) => sum + (step.bonusPoints ?? 0), 0) ?? 0; + + if (env.maxBonusPoints !== undefined && bonusPoints > env.maxBonusPoints) { + env.maxBonusPoints = bonusPoints; + } else if (env.maxBonusPoints === undefined && bonusPoints > 0) { + env.maxBonusPoints = bonusPoints; + } +} + +export default getEnvironments(); \ No newline at end of file diff --git a/backend/src/Environment.ts b/backend/src/Environment.ts index be508da..78dfbcd 100644 --- a/backend/src/Environment.ts +++ b/backend/src/Environment.ts @@ -64,6 +64,12 @@ export interface AssignmentStep { name: string; label: string; tests: Array; + bonusPoints?: number; +} + +interface GradualAssistanceType { + failedCounter: number; + hint: string; } interface AssignmentStepTestSSHCommand { @@ -72,6 +78,7 @@ interface AssignmentStepTestSSHCommand { stdOutMatch: string; successMessage: string; errorHint: string; + gradualAssistance?: Array; } interface AssignmentStepTestTerminalBufferSearch { @@ -80,6 +87,7 @@ interface AssignmentStepTestTerminalBufferSearch { match: string; successMessage: string; errorHint: string; + gradualAssistance?: Array; } type AssignmentStepTestType = @@ -170,6 +178,23 @@ export default class Environment { private filehandler!: FileHandler | undefined; private groupNumber: number; private sessionId: string; + private testCounter: Map = new Map(); + + private getErrorHint(test: AssignmentStepTestType, stepIndex: string) { + if (test.gradualAssistance === undefined) { + return test.errorHint; + } + + const attempts = (this.testCounter.get(stepIndex) ?? 0) + 1; + this.testCounter.set(stepIndex, attempts); + + const assistances = test.gradualAssistance.filter(a => a.failedCounter <= attempts).sort((a, b) => b.failedCounter - a.failedCounter); + if (assistances.length === 0) { + return test.errorHint; + } + + return assistances[0].hint; + } public static getActiveEnvironment( environmentId: string, @@ -1072,8 +1097,9 @@ export default class Environment { } } } + if (testPassed !== true) - testOutput += "FAILED: " + test.errorHint + " "; + testOutput += "FAILED: " + this.getErrorHint(test, stepIndex); } } else if (test.type === "SSHCommand") { await this.runSSHCommand(test.command, test.stdOutMatch) @@ -1082,14 +1108,16 @@ export default class Environment { testPassed = true; }) .catch(() => { - testOutput += "FAILED: " + test.errorHint + " "; + testOutput += "FAILED: " + this.getErrorHint(test, stepIndex) + " "; }); } + // if any of the terminalStates matched if (testPassed === true && someTestsFailed !== true) someTestsFailed = false; else someTestsFailed = true; } + if (someTestsFailed !== undefined && someTestsFailed === false) return Promise.resolve({ code: 201, @@ -1189,12 +1217,23 @@ export default class Environment { } } + let bonusPoints = 0; + if (this.configuration.steps !== undefined) { + for (let i = 0; i < parseInt(stepIndex); i++) { + const step = this.configuration.steps[i]; + const testResult = await this.test(i.toString(), terminalStates); + if (testResult.code === 201) + bonusPoints += step.bonusPoints ?? 0; + } + } + await this.persister.SubmitUserEnvironment( this.username, this.groupNumber, this.environmentId, terminalStates, submittedFiles, + bonusPoints ); } diff --git a/backend/src/database/MemoryPersister.ts b/backend/src/database/MemoryPersister.ts index 3fb1a6c..3bc8526 100644 --- a/backend/src/database/MemoryPersister.ts +++ b/backend/src/database/MemoryPersister.ts @@ -135,6 +135,7 @@ export default class MemoryPersister implements Persister { environment: string, terminalStates: TerminalStateType[], submittedFiles: SubmissionFileType[], + bonusPoints: number ): Promise { return new Promise((resolve) => { console.log( @@ -171,6 +172,12 @@ export default class MemoryPersister implements Persister { ); } + fs.writeFileSync( + path.resolve(resultPath, "bonusPoints.txt"), + bonusPoints.toString(), + "utf8" + ); + resolve(); }); } @@ -208,9 +215,14 @@ export default class MemoryPersister implements Persister { assignmentName = submissionDir.substring(username.length + 1); } + let bonusPoints = Number.parseInt(fs.readFileSync(path.resolve(submissionDir, "bonusPoints.txt"), {encoding: "utf8"})); + if (isNaN(bonusPoints)) + bonusPoints = 0; + const submission = { assignmentName: assignmentName, lastChanged: lastMTime, + points: bonusPoints }; submissions.push(submission); diff --git a/backend/src/database/MongoDBPersister.ts b/backend/src/database/MongoDBPersister.ts index 27f5efc..8737b61 100644 --- a/backend/src/database/MongoDBPersister.ts +++ b/backend/src/database/MongoDBPersister.ts @@ -19,7 +19,7 @@ import { SubmissionFileType, TerminalStateType, } from "../Environment"; -import environments, { updateEnvironments } from "../Configuration"; +import environments, { updateEnvironments, updateEnvironment } from "../Configuration"; const saltRounds = 10; @@ -247,6 +247,7 @@ export default class MongoDBPersister implements Persister { environment: string, terminalStates: TerminalStateType[], submittedFiles: SubmissionFileType[], + bonusPoints: number ): Promise { console.log( "Storing assignment result for user: " + @@ -308,6 +309,7 @@ export default class MongoDBPersister implements Persister { submissionCreated: now, terminalStatus: terminalStates, submittedFiles: submittedFiles, + points: bonusPoints }) .catch((err) => { throw new Error("Failed to store submissions in mongodb.\n" + err); @@ -547,8 +549,12 @@ export default class MongoDBPersister implements Persister { return client .db() .collection("assignments") - .find({}, { projection: { _id: 1, name: 1, maxBonusPoints: 1 } }) - .toArray(); + .find({}, { projection: { _id: 1, name: 1, steps: 1, maxBonusPoints: 1 } }) + .toArray() + .then(assignments => { + assignments.forEach(a => updateEnvironment(a)); + return assignments; + }); } async UpdateAssignementsForCourse( @@ -588,7 +594,11 @@ export default class MongoDBPersister implements Persister { { $unwind: "$assignments" }, { $replaceRoot: { newRoot: "$assignments" } }, ]) - .toArray(); + .toArray() + .then(assignments => { + assignments.forEach(a => updateEnvironment(a)); + return assignments; + }); } async GetAllSubmissions(): Promise { diff --git a/backend/src/database/Persister.ts b/backend/src/database/Persister.ts index 57adead..2c29c10 100644 --- a/backend/src/database/Persister.ts +++ b/backend/src/database/Persister.ts @@ -1,4 +1,5 @@ import { + AssignmentStep, Submission, SubmissionAdminOverviewEntry, SubmissionFileType, @@ -62,6 +63,7 @@ export interface ResponseObject { export interface AssignmentData { _id: string; name: string; + steps?: Array; maxBonusPoints?: number; } @@ -98,6 +100,7 @@ export interface Persister { environment: string, terminalStates: TerminalStateType[], submittedFiles: SubmissionFileType[], + bonusPoints: number ) => Promise; GetUserSubmissions: ( username: string, diff --git a/backend/src/providers/DockerProvider.ts b/backend/src/providers/DockerProvider.ts index 1ae3f8a..5c5ef92 100644 --- a/backend/src/providers/DockerProvider.ts +++ b/backend/src/providers/DockerProvider.ts @@ -229,9 +229,11 @@ export default class DockerProvider implements InstanceProvider { ExposedPorts: exposedPorts, HostConfig: { PortBindings: portBindings, - Privileged: true, + //Privileged: true, + Privileged: false, AutoRemove: true, Binds: hostConfigBinds, + CapAdd: ["NET_ADMIN"] }, Env: envs, }; diff --git a/backend/src/routes/environment.ts b/backend/src/routes/environment.ts index 23489c0..12942a3 100644 --- a/backend/src/routes/environment.ts +++ b/backend/src/routes/environment.ts @@ -144,7 +144,7 @@ export default (persister: Persister, provider: InstanceProvider): Router => { .json({ status: "error", message: "Environment not found" }); return; } - + Environment.createEnvironment( reqWithUser.user.username, reqWithUser.user.groupNumber, diff --git a/frontend/src/Theme.tsx b/frontend/src/Theme.tsx index 7cd3012..df12b06 100644 --- a/frontend/src/Theme.tsx +++ b/frontend/src/Theme.tsx @@ -47,7 +47,7 @@ export default function Theme(): JSX.Element {