UNPKG

@dodona/papyros

Version:

Scratchpad for multiple programming languages in the browser.

433 lines 16.8 kB
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