202 lines
6.3 KiB
JavaScript
202 lines
6.3 KiB
JavaScript
/**
|
|
* 🐍 PYTHON LANGUAGE RUNNER ADAPTER (Pyodide WebAssembly) 🐍
|
|
* ==========================================================
|
|
* Implementiert das standardisierte LanguageRunnerAdapter-Interface:
|
|
* - Pyodide WASM Engine mit In-Memory Python 3.12
|
|
* - CODE_GUARD Sicherheits-Prüfung & Sandbox-Bootstrap
|
|
* - Timeout-Schutz gegen Endlosschleifen
|
|
* - Unittest-Runner für aufgabe.py & test_aufgabe.py
|
|
* - Fehler-Dolmetscher Anbindung
|
|
*/
|
|
|
|
(function () {
|
|
let pyodideInstance = null;
|
|
let pyodideLoading = false;
|
|
|
|
const PythonAdapter = {
|
|
id: "python",
|
|
displayName: "Python 3.12 (Pyodide WebAssembly)",
|
|
icon: "🐍",
|
|
monacoLanguage: "python",
|
|
fileExtension: ".py",
|
|
testFileExtension: "test_aufgabe.py",
|
|
|
|
/**
|
|
* Lädt und initialisiert die Pyodide WebAssembly Engine
|
|
*/
|
|
async loadEngine(logCallback) {
|
|
if (pyodideInstance) return pyodideInstance;
|
|
if (pyodideLoading) {
|
|
while (pyodideLoading) {
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
}
|
|
return pyodideInstance;
|
|
}
|
|
|
|
pyodideLoading = true;
|
|
if (logCallback) logCallback("⏳ Initialisiere Python WebAssembly Engine (Pyodide)... Bitte kurz warten.\n", "info");
|
|
|
|
try {
|
|
if (!window.loadPyodide) {
|
|
await new Promise((resolve, reject) => {
|
|
const script = document.createElement("script");
|
|
script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js";
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
pyodideInstance = await window.loadPyodide();
|
|
|
|
// Sandbox-Härtung im Pyodide-Interpreter aktivieren
|
|
if (window.CODE_GUARD && window.CODE_GUARD.getSandboxBootstrap) {
|
|
await pyodideInstance.runPythonAsync(window.CODE_GUARD.getSandboxBootstrap());
|
|
}
|
|
|
|
if (logCallback) logCallback("✅ Python WebAssembly Sandbox bereitgestellt!\n\n", "success");
|
|
} catch (err) {
|
|
if (logCallback) logCallback(`❌ Fehler beim Laden der Python Sandbox: ${err.message}\n`, "error");
|
|
throw err;
|
|
} finally {
|
|
pyodideLoading = false;
|
|
}
|
|
return pyodideInstance;
|
|
},
|
|
|
|
/**
|
|
* Sicherheitsprüfung vor Code-Ausführung
|
|
*/
|
|
validateSecurity(code) {
|
|
if (window.CODE_GUARD && window.CODE_GUARD.validate) {
|
|
return window.CODE_GUARD.validate(code);
|
|
}
|
|
return { safe: true };
|
|
},
|
|
|
|
/**
|
|
* Führt Python-Code aus (▶ Code ausführen)
|
|
*/
|
|
async runCode(code, logCallback) {
|
|
const securityCheck = this.validateSecurity(code);
|
|
if (!securityCheck.safe) {
|
|
logCallback(`\n${securityCheck.error}\n\n`, "error");
|
|
return { success: false, error: securityCheck.error };
|
|
}
|
|
|
|
const py = await this.loadEngine(logCallback);
|
|
if (!py) return { success: false, error: "Pyodide nicht verfügbar" };
|
|
|
|
// Umleitung von stdout & stderr
|
|
py.setStdout({ batched: (text) => logCallback(text + "\n", "stdout") });
|
|
py.setStderr({ batched: (text) => logCallback(text + "\n", "stderr") });
|
|
|
|
try {
|
|
let timeoutHandle;
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
timeoutHandle = setTimeout(() => {
|
|
reject(new Error("Zeitüberschreitung: Dein Code lief länger als 5 Sekunden (Endlosschleife vermutet)."));
|
|
}, 5000);
|
|
});
|
|
|
|
const execPromise = py.runPythonAsync(code);
|
|
await Promise.race([execPromise, timeoutPromise]);
|
|
clearTimeout(timeoutHandle);
|
|
|
|
return { success: true };
|
|
} catch (err) {
|
|
logCallback(`\n❌ Ausführungsfehler:\n${err.message}\n`, "error");
|
|
return { success: false, error: err.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Führt automatisierte Unittests aus (🧪 Tests starten)
|
|
*/
|
|
async runTests(userCode, testCode, logCallback) {
|
|
const securityCheck = this.validateSecurity(userCode);
|
|
if (!securityCheck.safe) {
|
|
logCallback(`\n${securityCheck.error}\n\n`, "error");
|
|
return { success: false, error: securityCheck.error };
|
|
}
|
|
|
|
const py = await this.loadEngine(logCallback);
|
|
if (!py) return { success: false, error: "Pyodide nicht verfügbar" };
|
|
|
|
try {
|
|
py.FS.writeFile("aufgabe.py", userCode);
|
|
|
|
const testRunnerScript = `
|
|
import unittest, io, sys
|
|
from unittest import TextTestRunner
|
|
|
|
for mod in list(sys.modules.keys()):
|
|
if mod in ('aufgabe', 'test_aufgabe'):
|
|
del sys.modules[mod]
|
|
|
|
import aufgabe
|
|
|
|
${testCode}
|
|
|
|
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
|
|
stream = io.StringIO()
|
|
runner = TextTestRunner(stream=stream, verbosity=2)
|
|
result = runner.run(suite)
|
|
|
|
output_text = stream.getvalue()
|
|
success = result.wasSuccessful()
|
|
total_runs = result.testsRun
|
|
failures = len(result.failures)
|
|
errors = len(result.errors)
|
|
(success, total_runs, failures, errors, output_text)
|
|
`;
|
|
|
|
let timeoutHandle;
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
timeoutHandle = setTimeout(() => {
|
|
reject(new Error("Zeitüberschreitung: Unittest lief länger als 6 Sekunden."));
|
|
}, 6000);
|
|
});
|
|
|
|
const [isSuccess, totalRuns, fails, errs, outputText] = await Promise.race([
|
|
py.runPythonAsync(testRunnerScript),
|
|
timeoutPromise
|
|
]);
|
|
clearTimeout(timeoutHandle);
|
|
|
|
logCallback(outputText + "\n", "stdout");
|
|
|
|
return {
|
|
success: Boolean(isSuccess),
|
|
total: totalRuns,
|
|
passed: totalRuns - (fails + errs),
|
|
failures: fails,
|
|
errors: errs,
|
|
rawOutput: outputText
|
|
};
|
|
} catch (err) {
|
|
logCallback(`\n❌ Fehler beim Ausführen der Testsuite:\n${err.message}\n`, "error");
|
|
return { success: false, error: err.message, rawOutput: err.message };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Übersetzt Python-Tracebacks
|
|
*/
|
|
translateError(traceback) {
|
|
if (window.INTERPRETER && window.INTERPRETER.translate) {
|
|
return window.INTERPRETER.translate(traceback);
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// In Registry registrieren
|
|
if (window.RunnerRegistry) {
|
|
window.RunnerRegistry.register(PythonAdapter);
|
|
} else {
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
if (window.RunnerRegistry) window.RunnerRegistry.register(PythonAdapter);
|
|
});
|
|
}
|
|
})();
|