@dodona/papyros
Version:
Scratchpad for multiple programming languages in the browser.
433 lines • 16.8 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { proxy } from "comlink";
import { RunMode } from "../../backend/Backend";
import { BackendEventType } from "../../communication/BackendEvent";
import { BackendManager } from "../../communication/BackendManager";
import { arrayBufferToBase64, isTextMimeType, isValidFileName, parseData } from "../../util/Util";
import { State, stateProperty } from "@dodona/lit-state";
import { ProgrammingLanguage } from "../../ProgrammingLanguage";
/**
* Enum representing the possible states while processing code
*/
export var RunState;
(function (RunState) {
RunState["Loading"] = "loading";
RunState["Running"] = "running";
RunState["AwaitingInput"] = "awaiting_input";
RunState["Stopping"] = "stopping";
RunState["Ready"] = "ready";
})(RunState || (RunState = {}));
/**
* Helper component to manage and visualize the current RunState
*/
export class Runner extends State {
get programmingLanguage() {
return this._programmingLanguage;
}
set programmingLanguage(value) {
if (this._programmingLanguage !== value) {
this._programmingLanguage = value;
this.launch();
}
}
get code() {
return this._code;
}
set code(value) {
if (this._code !== value) {
this._code = value;
this.updateRunModes();
}
}
get effectiveCode() {
let result = this.code;
if (this.papyros.test.testCode !== undefined) {
result += `${Runner.CODE_SEPARATOR}${this.papyros.test.testCode}`;
}
return result;
}
set effectiveCode(value) {
let codeWithoutTest = value;
if (this.papyros.test.testCode !== undefined) {
codeWithoutTest = codeWithoutTest.slice(0, -(Runner.CODE_SEPARATOR.length + this.papyros.test.testCode.length));
}
this.code = codeWithoutTest;
}
/**
* Async getter for the linting diagnostics of the current code
*/
lintSource() {
return __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
const proxy = backend.workerProxy;
if (!proxy) {
return [];
}
return yield proxy.lintCode(this.code);
});
}
constructor(papyros) {
super();
/**
* The currently used programming language
*/
this._programmingLanguage = ProgrammingLanguage.Python;
/**
* Current state of the program
*/
this.state = RunState.Ready;
/**
* An explanatory message about the current state
*/
this.stateMessage = "";
/**
* Previous state to restore when loading is done
*/
this.previousState = RunState.Ready;
/**
* Array of packages that are being installed
*/
this.loadingPackages = [];
/**
* Time at which the setState call occurred
*/
this.runStartTime = new Date().getTime();
/**
* The code we are working with
*/
this._code = "";
/**
* available run modes for the current code
*/
this.runModes = [RunMode.Debug];
this.papyros = papyros;
this.backend = Promise.resolve({});
BackendManager.subscribe(BackendEventType.Input, () => this.setState(RunState.AwaitingInput));
BackendManager.subscribe(BackendEventType.Loading, (e) => this.onLoad(e));
BackendManager.subscribe(BackendEventType.Start, (e) => this.onStart(e));
BackendManager.subscribe(BackendEventType.End, (e) => this.onEnd(e));
BackendManager.subscribe(BackendEventType.Error, () => this.onError());
BackendManager.subscribe(BackendEventType.Stop, () => this.stop());
}
/**
* Stops the current run and resets the state of the program
* Regular and debug output is cleared
* @return {Promise<void>} Returns when the program has been reset
*/
reset() {
return __awaiter(this, void 0, void 0, function* () {
if (![RunState.Ready, RunState.Loading].includes(this.state)) {
yield this.stop();
}
this.papyros.debugger.active = false;
});
}
/**
* Start the backend to enable running code
*/
launch() {
return __awaiter(this, void 0, void 0, function* () {
this.setState(RunState.Loading);
const backend = BackendManager.getBackend(this.programmingLanguage);
// Use a Promise to immediately enable running while downloading
// eslint-disable-next-line no-async-promise-executor
this.backend = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
const workerProxy = backend.workerProxy;
// Allow passing messages between worker and main thread
yield workerProxy.launch(proxy((e) => BackendManager.publish(e)));
this.updateRunModes();
return resolve(backend);
}));
this.setState(RunState.Ready);
});
}
/**
* Execute the code in the editor
* @param {RunMode} mode The mode to run with
* @return {Promise<void>} Promise of running the code
*/
start(mode) {
return __awaiter(this, void 0, void 0, function* () {
this.papyros.debugger.active = mode === RunMode.Debug;
// Setup pre-run
this.setState(RunState.Loading);
// Ensure we go back to Loading after finishing any remaining installs
this.previousState = RunState.Loading;
BackendManager.publish({
type: BackendEventType.Start,
data: "StartClicked",
contentType: "text/plain",
});
let interrupted = false;
let terminated = false;
const backend = yield this.backend;
this.runStartTime = new Date().getTime();
try {
yield backend.call(backend.workerProxy.runCode, this.effectiveCode, mode);
}
catch (error) {
if (error.type === "InterruptError") {
// Error signaling forceful interrupt
interrupted = true;
terminated = true;
}
else {
this.papyros.io.logError(error);
BackendManager.publish({
type: BackendEventType.End,
data: "RunError",
contentType: "text/plain",
});
}
}
finally {
if (this.state === RunState.Stopping) {
// Was interrupted, End message already published
interrupted = true;
}
if (terminated) {
yield this.launch();
}
if (interrupted || terminated) {
this.setState(RunState.Ready, this.papyros.i18n.t("Papyros.interrupted", {
time: (new Date().getTime() - this.runStartTime) / 1000,
}));
}
}
});
}
/**
* Interrupt the currently running code
* @return {Promise<void>} Returns when the code has been interrupted
*/
stop() {
return __awaiter(this, void 0, void 0, function* () {
this.setState(RunState.Stopping);
BackendManager.publish({
type: BackendEventType.End,
data: "User cancelled run",
contentType: "text/plain",
});
const backend = yield this.backend;
yield backend.interrupt();
const startTime = new Date().getTime();
while (this.state === RunState.Stopping && new Date().getTime() - startTime < 5000) {
yield new Promise((resolve) => setTimeout(resolve, 100));
}
if (this.state === RunState.Stopping) {
console.warn("Deadlock while stopping, restarting backend");
yield this.launch();
this.setState(RunState.Ready, this.papyros.i18n.t("Papyros.interrupted", { time: (new Date().getTime() - this.runStartTime) / 1000 }));
}
});
}
provideInput(input) {
return __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
this.setState(RunState.Running);
yield backend.writeMessage(input);
});
}
deleteFile(name) {
return __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
yield backend.workerProxy.deleteFile(name);
});
}
updateFile(name, content, binary) {
return __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
yield backend.workerProxy.updateFile(name, content, binary);
});
}
renameFile(oldName, newName) {
return __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
yield backend.workerProxy.renameFile(oldName, newName);
});
}
upsertFile(name, content, binary) {
this.papyros.io.upsertFile(name, content, binary);
void this.updateFile(name, content, binary);
}
fetchAndAddUrl(rawUrl) {
return __awaiter(this, void 0, void 0, function* () {
try {
const url = new URL(rawUrl);
const response = yield fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
const name = this.filenameFromUrl(url);
const contentType = response.headers.get("Content-Type");
if (isTextMimeType(contentType)) {
this.upsertFile(name, yield response.text(), false);
}
else {
this.upsertFile(name, arrayBufferToBase64(yield response.arrayBuffer()), true);
}
}
catch (err) {
console.warn("Failed to fetch dropped URL:", rawUrl, err);
alert(this.papyros.i18n.t("Papyros.url_fetch_error", { url: rawUrl }));
}
});
}
filenameFromUrl(url) {
var _a;
const segments = url.pathname.split("/").filter((s) => s.length > 0);
let candidate = (_a = segments[segments.length - 1]) !== null && _a !== void 0 ? _a : "";
try {
candidate = decodeURIComponent(candidate);
}
catch (_b) {
// Leave as-is if decoding fails
}
if (isValidFileName(candidate))
return candidate;
if (isValidFileName(url.hostname))
return url.hostname;
return "download";
}
provideFiles(inlinedFiles, hrefFiles) {
return __awaiter(this, void 0, void 0, function* () {
const fileNames = [...Object.keys(inlinedFiles), ...Object.keys(hrefFiles)];
if (fileNames.length === 0) {
return;
}
BackendManager.publish({
type: BackendEventType.Loading,
data: JSON.stringify({
modules: fileNames,
status: "loading",
}),
});
const backend = yield this.backend;
yield backend.workerProxy.provideFiles(inlinedFiles, hrefFiles);
});
}
/**
* Show the current state of the program to the user
* @param {RunState} state The current state of the run
* @param {string} message Optional message to indicate the state
*/
setState(state, message) {
this.stateMessage = message || this.papyros.i18n.t(`Papyros.states.${state}`);
if (state !== this.state) {
this.previousState = this.state;
this.state = state;
}
}
/**
* Callback to handle loading events
* @param {BackendEvent} e The loading event
*/
onLoad(e) {
const loadingData = parseData(e.data, e.contentType);
if (loadingData.status === "loading") {
loadingData.modules.forEach((m) => {
if (!this.loadingPackages.includes(m)) {
this.loadingPackages.push(m);
}
});
}
else if (loadingData.status === "loaded") {
loadingData.modules.forEach((m) => {
const index = this.loadingPackages.indexOf(m);
if (index !== -1) {
this.loadingPackages.splice(index, 1);
}
});
}
else {
// failed
// If it is a true module, an Exception will be raised when running
// So this does not need to be handled here, as it is often an incomplete package-name
// that causes micropip to not find the correct wheel
this.loadingPackages = [];
}
if (this.loadingPackages.length > 0) {
const packageMessage = this.papyros.i18n.t("Papyros.loading", {
// limit amount of package names shown
packages: this.loadingPackages.slice(0, 3).join(", "),
});
this.setState(RunState.Loading, packageMessage);
}
else {
this.setState(this.previousState);
}
}
onStart(e) {
const startData = parseData(e.data, e.contentType);
if (startData.includes("RunCode")) {
this.runStartTime = new Date().getTime();
this.setState(RunState.Running);
}
}
onEnd(e) {
const endData = parseData(e.data, e.contentType);
if (endData.includes("CodeFinished")) {
this.setState(RunState.Ready, this.papyros.i18n.t("Papyros.finished", { time: (new Date().getTime() - this.runStartTime) / 1000 }));
}
}
onError() {
this.setState(RunState.Ready, this.papyros.i18n.t("Papyros.finished", { time: (new Date().getTime() - this.runStartTime) / 1000 }));
}
updateRunModes() {
this.backend.then((backend) => __awaiter(this, void 0, void 0, function* () {
const proxy = backend.workerProxy;
if (proxy) {
this.runModes = yield proxy.runModes(this.effectiveCode);
}
}));
}
}
Runner.CODE_SEPARATOR = "\n\n";
__decorate([
stateProperty
], Runner.prototype, "_programmingLanguage", void 0);
__decorate([
stateProperty
], Runner.prototype, "programmingLanguage", null);
__decorate([
stateProperty
], Runner.prototype, "backend", void 0);
__decorate([
stateProperty
], Runner.prototype, "state", void 0);
__decorate([
stateProperty
], Runner.prototype, "stateMessage", void 0);
__decorate([
stateProperty
], Runner.prototype, "loadingPackages", void 0);
__decorate([
stateProperty
], Runner.prototype, "runStartTime", void 0);
__decorate([
stateProperty
], Runner.prototype, "_code", void 0);
__decorate([
stateProperty
], Runner.prototype, "code", null);
__decorate([
stateProperty
], Runner.prototype, "effectiveCode", null);
__decorate([
stateProperty
], Runner.prototype, "runModes", void 0);
//# sourceMappingURL=Runner.js.map