Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ jobs:
- name: Import resolution check
run: node -e "import('./src/routes/routes.js')"

server-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: server

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: server/yarn.lock

- run: |
corepack enable
yarn install --frozen-lockfile

- name: Run unit tests (node:test)
run: node --test test/

docker:
runs-on: ubuntu-latest

Expand All @@ -87,3 +109,6 @@ jobs:

- name: Build Java executor Docker image
run: docker build -t algojunction-java-executor server/src/docker

- name: Build C++ executor Docker image
run: docker build -t algojunction-cpp-executor server/src/docker/cpp
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "node --test test/",
"start": "nodemon src/index.js",
"lint": "eslint src/ --ext .js"
},
Expand Down
198 changes: 198 additions & 0 deletions server/src/controllers/runCppController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import fs from "fs/promises";
import path from "path";

import {
getQuestionByIdFromDB,
addOrUpdateUser,
insertNew,
DBConnectionError,
} from "../db/mongooseClient.js";
import { runSandbox, mapExitCodeToError } from "../services/sandboxRunner.js";

const CPP_IMAGE = "algojunction-cpp-executor";
// Compile then run, reading input.txt from the workdir. `2>&1` merges the
// compiler diagnostics into stdout so a compile failure surfaces the real
// compiler stderr as the per-case error (a stub returning "" would fail AC2).
const CPP_COMMAND =
"g++ -o solution Solution.cpp 2>&1 && timeout --signal=KILL 10s ./solution";

// Factory form so tests can inject the runner + DB boundaries (the
// route -> controller -> sandbox -> DB seam, AC4) without ESM module mocking.
// Production wiring uses the real implementations via the default `runCpp`.
export function createRunCpp(deps = {}) {
const runSandboxFn = deps.runSandbox ?? runSandbox;
const getQuestionFn = deps.getQuestionByIdFromDB ?? getQuestionByIdFromDB;
const insertNewFn = deps.insertNew ?? insertNew;
const addOrUpdateUserFn = deps.addOrUpdateUser ?? addOrUpdateUser;
const mkdir = deps.mkdir ?? fs.mkdir;
const writeFile = deps.writeFile ?? fs.writeFile;
const chmod = deps.chmod ?? fs.chmod;
const rm = deps.rm ?? fs.rm;

return async (req, res) => {
const result = [];
let output = null;

const { quesid, cppCode } = req.body;
const username = req.user.name;
const email = req.user.email;
console.log(
`${new Date().toLocaleString()}: Executing C++ code for question id:${quesid} by user:${username}`,
);

if (!cppCode) {
return res
.status(400)
.json({ error: "Missing or invalid cppCode in the request body" });
} else if (!quesid) {
return res
.status(400)
.json({ error: "Missing or invalid question id in the request body" });
}

const tempDir = path.join(
process.cwd(),
"tmp",
`algojunction-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);

try {
await mkdir(tempDir, { recursive: true });
await writeFile(path.join(tempDir, "Solution.cpp"), cppCode, "utf-8");
await chmod(tempDir, 0o777);

// Fetch the question from DB instead of scanning a static array
const question = await getQuestionFn(quesid);
if (!question) {
// Clean up temp dir before returning
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
return res
.status(404)
.json({ error: `Question with id ${quesid} not found` });
}

// running the code for each input
for (const [
index,
{ input, expectedOutput },
] of question.inputs.entries()) {
// copying the input to input.txt in the workdir (read by ./solution)
const inputFilePath = path.join(tempDir, "input.txt");
try {
await writeFile(inputFilePath, input, "utf-8");
} catch (writeError) {
console.log(
`${new Date().toLocaleString()}: file write failed with error: ${writeError}`,
);
result.push({
index,
output: null,
error: writeError,
success: false,
});
continue;
}

// compile and run the code in the pre-built image via the shared sandbox runner
try {
output = await runSandboxFn({
image: CPP_IMAGE,
command: CPP_COMMAND,
tempDir,
});
console.log(`${new Date().toLocaleString()}: C++ code run done`);
const passed =
String(output ?? "").trim() === String(expectedOutput ?? "").trim();
result.push({
index,
output: String(output ?? "").trim(),
expectedOutput: String(expectedOutput ?? "").trim(),
error: null,
success: passed,
});
} catch (error) {
const exitCode = error?.code;
const errMsg = error?.message || error;
const userError = mapExitCodeToError(exitCode, errMsg);
console.log(
`${new Date().toLocaleString()}: C++ code run failed with error: ${errMsg} (exit code: ${exitCode})`,
);
result.push({
index,
output: null,
error: userError,
success: false,
});
}
}

const allPassed =
result.length > 0 && result.every((r) => r.success === true);
const overallStatus = allPassed ? "accepted" : "failed";
const data = {
username,
quesid,
cppCode,
language: "cpp",
status: { status: overallStatus, output: output, error: null },
result,
email,
};

await handleDatabaseUpdates(data, { insertNewFn, addOrUpdateUserFn });
} catch (error) {
const errMsg = error?.message || String(error);
console.log(
`${new Date().toLocaleString()}: C++ code execution failed with error: ${errMsg}`,
);
// fallback DB save on unexpected errors
const data = {
username,
quesid,
cppCode,
language: "cpp",
status: { status: "failed", output: null, error: errMsg },
result,
email,
};
await handleDatabaseUpdates(data, { insertNewFn, addOrUpdateUserFn });
} finally {
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
console.log(`${new Date().toLocaleString()}: Temp directory cleaned up`);
}

res.json(result);
};
}

const handleDatabaseUpdates = async (
{ username, quesid, cppCode, language, status, result, email },
{ insertNewFn, addOrUpdateUserFn },
) => {
try {
const submission_id = await insertNewFn(
username,
quesid,
cppCode,
language,
status,
result,
);

await addOrUpdateUserFn(username, email, submission_id);
} catch (error) {
if (error instanceof DBConnectionError) {
console.warn(
`${new Date().toLocaleString()}: Database updates skipped — DB unavailable (${error.message})`,
);
// No rollback needed — the data never reached the DB
} else {
console.error(
`${new Date().toLocaleString()}: Database update failed:`,
error,
);
}
}
};

export const runCpp = createRunCpp();
Loading
Loading