From e0c9aba25ed92cc4f037c621e7bd43453aed2e9d Mon Sep 17 00:00:00 2001 From: j433866 Date: Tue, 2 Apr 2019 16:58:36 +0100 Subject: [PATCH] Improve WorkerWaiter and OutputWaiter. - Will run and display outputs in the output area as they're baked - Creates output tabs - Can change output tabs (only the first 4 at the moment) --- src/web/InputWaiter.mjs | 45 +- src/web/Manager.mjs | 38 +- src/web/OutputWaiter.mjs | 996 ++++++++++++++--------------- src/web/WorkerWaiter.mjs | 441 ++++++++----- src/web/stylesheets/layout/_io.css | 21 +- 5 files changed, 814 insertions(+), 727 deletions(-) diff --git a/src/web/InputWaiter.mjs b/src/web/InputWaiter.mjs index 6d032316..a803fc79 100644 --- a/src/web/InputWaiter.mjs +++ b/src/web/InputWaiter.mjs @@ -90,7 +90,7 @@ class InputWaiter { } /** - * Removes a loaderworker using inputNum + * Removes a loaderworker * * @param {Object} workerObj */ @@ -203,14 +203,6 @@ class InputWaiter { this.inputs.splice(i, 1); } } - // if (this.inputs.length === 0) { - // this.inputs.push({ - // inputNum: inputNum, - // data: "", - // status: "loaded", - // progress: 100 - // }); - // } } /** @@ -327,14 +319,11 @@ class InputWaiter { if (index === -1) { return null; } - if (this.inputs[index].inputNum === inputNum) { - if (typeof this.inputs[index].data === "string") { - return this.inputs[index].data; - } else { - return this.inputs[index].data.fileBuffer; - } + if (typeof this.inputs[index].data === "string") { + return this.inputs[index].data; + } else { + return this.inputs[index].data.fileBuffer; } - return null; } /** @@ -359,7 +348,8 @@ class InputWaiter { const value = (textArea.value !== undefined) ? textArea.value : ""; const inputNum = this.getActiveTab(); - if (this.getInput(inputNum) === null || typeof this.getInput(inputNum) === "string") { + const input = this.getInput(inputNum); + if (input === null || typeof input === "string") { this.updateInputValue(inputNum, value); } @@ -774,6 +764,8 @@ class InputWaiter { progress: 100 }); + this.manager.output.addOutput(inputNum, changeTab); + const tabsWrapper = document.getElementById("input-tabs"); const numTabs = tabsWrapper.children.length; @@ -831,6 +823,8 @@ class InputWaiter { } this.refreshTabs(activeTab); } + + this.manager.output.removeTab(inputNum); } /** @@ -883,6 +877,8 @@ class InputWaiter { } this.changeTab(activeTab); + + // MAKE THE OUTPUT REFRESH TOO } /** @@ -966,14 +962,8 @@ class InputWaiter { * @param {number} inputNum */ changeTab(inputNum) { - const inputIdx = this.getInputIndex(inputNum); - let currentIdx = -1; - try { - currentIdx = this.getActiveTab(); - } catch (err) {} - if (inputIdx === -1) { - return; - } + const currentNum = this.getActiveTab(); + if (this.getInputIndex(inputNum) === -1) return; const tabsWrapper = document.getElementById("input-tabs"); const tabs = tabsWrapper.children; @@ -990,7 +980,7 @@ class InputWaiter { if (!found) { // Shift the tabs here let direction = "right"; - if (currentIdx > inputIdx) { + if (currentNum > inputNum) { direction = "left"; } @@ -1122,9 +1112,8 @@ class InputWaiter { const activeTab = activeTabs.item(0); const tabNum = activeTab.getAttribute("inputNum"); return parseInt(tabNum, 10); - } else { - return -1; } + return -1; } /** diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 55da2475..68cea9ca 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -84,7 +84,7 @@ class Manager { setup() { this.input.addTab(); this.input.setupLoaderWorker(); - this.worker.setupChefWorkers(); + this.worker.setupChefWorker(); this.recipe.initialiseOperationDragNDrop(); this.controls.initComponents(); this.controls.autoBakeChange(); @@ -165,24 +165,24 @@ class Manager { this.addDynamicListener("#input-tabs li .input-tab-content", "click", this.input.changeTabClick, this.input); // Output - document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output)); - document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output)); - document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output)); - document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output)); - document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output)); - document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output)); - document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter)); - document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter)); - document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter)); - document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter)); - document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter)); - this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter); - this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter); - this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output); - this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output); - document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output)); - this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output); - // this.addDynamicListener("#output-tabs ul li .output-tab-content", "click", this.output.changeTabClick, this.output); + // document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output)); + // document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output)); + // document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output)); + // document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output)); + // document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output)); + // document.getElementById("magic").addEventListener("click", this.output.magicClick.bind(this.output)); + // document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter)); + // document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter)); + // document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter)); + // document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter)); + // document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter)); + // this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter); + // this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter); + // this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output); + // this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output); + // document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output)); + // this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output); + this.addDynamicListener("#output-tabs-wrapper #output-tabs li .output-tab-content", "click", this.output.changeTabClick, this.output); // Options document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options)); diff --git a/src/web/OutputWaiter.mjs b/src/web/OutputWaiter.mjs index 7572c5d1..ba9c4507 100755 --- a/src/web/OutputWaiter.mjs +++ b/src/web/OutputWaiter.mjs @@ -1,5 +1,6 @@ /** * @author n1474335 [n1474335@gmail.com] + * @author j433866 [j433866@gmail.com] * @copyright Crown Copyright 2016 * @license Apache-2.0 */ @@ -7,361 +8,315 @@ import Utils from "../core/Utils"; import FileSaver from "file-saver"; - /** - * Waiter to handle events related to the output. - */ + * Waiter to handle events related to the output + */ class OutputWaiter { /** * OutputWaiter constructor. * * @param {App} app - The main view object for CyberChef. - * @param {Manager} manager - The CyberChef event manager. + * @param {Manager} manager - The CyberChef event manager */ constructor(app, manager) { this.app = app; this.manager = manager; - this.dishBuffer = null; - this.dishStr = null; this.outputs = []; + this.maxTabs = 4; // Calculate this } - /** - * Gets the output string from the output textarea. + * Gets the output for the specified input number * - * @returns {string} + * @param {number} inputNum + * @returns {Object} */ - get() { - return document.getElementById("output-text").value; + getOutput(inputNum) { + const index = this.getOutputIndex(inputNum); + if (index === -1) return -1; + + if (typeof this.outputs[index].data.result === "string") { + return this.outputs[index].data.result; + } else { + return this.outputs[index].data.result || ""; + } } - /** - * Sets the output array for multiple outputs. - * Displays the active output in the output textarea + * Gets the index of the output for the specified input number * - * @param {Array} outputs + * @param {number} inputNum + * @returns {number} */ - async multiSet(outputs) { - log.debug("Received " + outputs.length + " outputs."); - this.outputs = outputs; - const activeTab = this.manager.input.getActiveTab(); - - for (let i = 0; i < outputs.length; i++) { - if (outputs[i].inputNum === activeTab) { - await this.set(outputs[i].data.result, outputs[i].data.type, outputs[0].data.duration); + getOutputIndex(inputNum) { + for (let i = 0; i < this.outputs.length; i++) { + if (this.outputs[i].inputNum === inputNum) { + return i; } } - // await this.set(this.outputs[0].data.result, this.outputs[0].data.type, this.outputs[0].data.duration); - - // Create tabs - - // Select active tab - - // Display active tab data in textarea + return -1; } + /** + * Gets the output string or FileBuffer for the active input + * + * @returns {string | ArrayBuffer} + */ + getActive() { + return this.getOutput(this.getActiveTab()).data; + } + + /** + * Adds a new output to the output array. + * Creates a new tab if we have less than maxtabs tabs open + * + * @param {number} inputNum + * @param {boolean} [changeTab=true] + */ + addOutput(inputNum, changeTab = true) { + const index = this.getOutputIndex(inputNum); + if (index !== -1) { + // Remove the output if it already exists + this.outputs.splice(index, 1); + } + const newOutput = { + data: null, + inputNum: inputNum, + // statusMessage: `Input ${inputNum} has not been baked yet.`, + statusMessage: "", + error: null, + status: "inactive" + }; + + this.outputs.push(newOutput); + + // add new tab + this.addTab(inputNum, changeTab); + return this.outputs.indexOf(newOutput); + } + + /** + * Updates the value for the output in the output array. + * If this is the active output tab, updates the output textarea + * + * @param {Object} data + * @param {number} inputNum + */ + updateOutputValue(data, inputNum) { + let index = this.getOutputIndex(inputNum); + if (index === -1) { + index = this.addOutput(inputNum); + } + + this.outputs[index].data = data; + + // set output here + this.set(inputNum); + } + + /** + * Updates the status message for the output in the output array. + * If this is the active output tab, updates the output textarea + * + * @param {string} statusMessage + * @param {number} inputNum + */ + updateOutputMessage(statusMessage, inputNum) { + // log.error(`MSG: ${statusMessage}; inputNum: ${inputNum}`); + const index = this.getOutputIndex(inputNum); + if (index === -1) return; + + this.outputs[index].statusMessage = statusMessage; + this.set(inputNum); + } + + /** + * Updates the error value for the output in the output array. + * If this is the active output tab, calls app.handleError. + * Otherwise, the error will be handled when the output is switched to + * + * @param {Error} error + * @param {number} inputNum + */ + updateOutputError(error, inputNum) { + const index = this.getOutputIndex(inputNum); + if (index === -1) return; + + this.outputs[index].error = error; + + // call handle error here + // or make the error handling part of set() + this.set(inputNum); + } + + /** + * Updates the status value for the output in the output array + * + * @param {string} status + * @param {number} inputNum + */ + updateOutputStatus(status, inputNum) { + const index = this.getOutputIndex(inputNum); + if (index === -1) return; + + this.outputs[index].status = status; + + this.set(inputNum); + } + + /** + * Removes an output from the output array. + * + * @param {number} inputNum + */ + removeOutput(inputNum) { + const index = this.getOutputIndex(inputNum); + if (index === -1) return; + + this.outputs.splice(index, 1); + } /** * Sets the output in the output textarea. * - * @param {string|ArrayBuffer} data - The output string/HTML/ArrayBuffer - * @param {string} type - The data type of the output - * @param {number} duration - The length of time (ms) it took to generate the output - * @param {boolean} [preserveBuffer=false] - Whether to preserve the dishBuffer + * @param {number} inputNum */ - async set(data, type, duration, preserveBuffer) { - log.debug("Output type: " + type); + set(inputNum) { + const outputIndex = this.getOutputIndex(inputNum); + if (outputIndex === -1) return; + const output = this.outputs[outputIndex]; const outputText = document.getElementById("output-text"); const outputHtml = document.getElementById("output-html"); const outputFile = document.getElementById("output-file"); const outputHighlighter = document.getElementById("output-highlighter"); const inputHighlighter = document.getElementById("input-highlighter"); - let scriptElements, lines, length; + // If inactive, show blank + // If pending or baking, show loader and status message + // If error, style the tab and handle the error + // If done, display the output if it's the active tab - if (!preserveBuffer) { - this.closeFile(); - this.dishStr = null; - document.getElementById("show-file-overlay").style.display = "none"; - } - - switch (type) { - case "html": - outputText.style.display = "none"; - outputHtml.style.display = "block"; - outputFile.style.display = "none"; - outputHighlighter.display = "none"; - inputHighlighter.display = "none"; - - outputText.value = ""; - outputHtml.innerHTML = data; - - // Execute script sections - scriptElements = outputHtml.querySelectorAll("script"); - for (let i = 0; i < scriptElements.length; i++) { - try { - eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval - } catch (err) { - log.error(err); - } - } - - await this.getDishStr(); - length = this.dishStr.length; - lines = this.dishStr.count("\n") + 1; - break; - case "ArrayBuffer": - outputText.style.display = "block"; - outputHtml.style.display = "none"; - outputHighlighter.display = "none"; - inputHighlighter.display = "none"; - - outputText.value = ""; - outputHtml.innerHTML = ""; - length = data.byteLength; - - this.setFile(data); - break; - case "string": - default: + if (output.status === "inactive") { + // An output is inactive when it has been created but has not been baked at all + // show a blank here + if (inputNum === this.getActiveTab()) { + this.toggleLoader(false); outputText.style.display = "block"; outputHtml.style.display = "none"; outputFile.style.display = "none"; outputHighlighter.display = "block"; inputHighlighter.display = "block"; - outputText.value = Utils.printable(data, true); + outputText.value = ""; outputHtml.innerHTML = ""; - lines = data.count("\n") + 1; - length = data.length; - this.dishStr = data; - break; - } + } + } else if (output.status === "pending" || output.status === "baking") { + // show the loader and the status message if it's being shown + // otherwise don't do anything + if (inputNum === this.getActiveTab()) { + this.toggleLoader(true); - this.manager.highlighter.removeHighlights(); - this.setOutputInfo(length, lines, duration); - this.backgroundMagic(); + document.querySelector("#output-loader .loading-msg").textContent = output.statusMessage; + } + + } else if (output.status === "error") { + // style the tab if it's being shown + // run app.handleError() + if (inputNum === this.getActiveTab()) { + this.toggleLoader(false); + } + } else if (output.status === "baked") { + // Display the output if it's the active tab + this.displayTabInfo(inputNum); + if (inputNum === this.getActiveTab()) { + this.toggleLoader(false); + this.closeFile(); + let scriptElements, lines, length; + + switch (output.data.type) { + case "html": + outputText.style.display = "none"; + outputHtml.style.display = "block"; + outputFile.style.display = "none"; + outputHighlighter.style.display = "none"; + inputHighlighter.style.display = "none"; + + outputText.value = ""; + outputHtml.innerHTML = output.data.result; + + // Execute script sections + scriptElements = outputHtml.querySelectorAll("script"); + for (let i = 0; i < scriptElements.length; i++) { + try { + eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval + } catch (err) { + log.error(err); + } + } + + break; + case "ArrayBuffer": + outputText.style.display = "block"; + outputHtml.style.display = "none"; + outputHighlighter.display = "none"; + inputHighlighter.display = "none"; + + outputText.value = ""; + outputHtml.innerHTML = ""; + length = output.data.result.byteLength; + + this.setFile(output.data.result); + break; + case "string": + default: + outputText.style.display = "block"; + outputHtml.style.display = "none"; + outputFile.style.display = "none"; + outputHighlighter.display = "block"; + inputHighlighter.display = "block"; + + outputText.value = Utils.printable(output.data.result, true); + outputHtml.innerHTML = ""; + + lines = output.data.result.count("\n") + 1; + length = output.data.result.length; + break; + } + } + } } - /** - * Shows file details. + * Shows file details * * @param {ArrayBuffer} buf */ setFile(buf) { - this.dishBuffer = buf; const file = new File([buf], "output.dat"); // Display file overlay in output area with details const fileOverlay = document.getElementById("output-file"), - fileSize = document.getElementById("output-file-size"); + fileSize = document.getElementById("output-file-size"), + outputText = document.getElementById("output-text"), + fileSlice = buf.slice(0, 4096); fileOverlay.style.display = "block"; fileSize.textContent = file.size.toLocaleString() + " bytes"; - // Display preview slice in the background - const outputText = document.getElementById("output-text"), - fileSlice = this.dishBuffer.slice(0, 4096); - outputText.classList.add("blur"); outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice)); } - /** - * Removes the output file and nulls its memory. + * Clears output file details */ closeFile() { - this.dishBuffer = null; document.getElementById("output-file").style.display = "none"; document.getElementById("output-text").classList.remove("blur"); } - - /** - * Handler for file download events. - */ - async downloadFile() { - this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat"); - await this.getDishBuffer(); - const file = new File([this.dishBuffer], this.filename); - if (this.filename) FileSaver.saveAs(file, this.filename, false); - } - - - /** - * Handler for file slice display events. - */ - displayFileSlice() { - const startTime = new Date().getTime(), - showFileOverlay = document.getElementById("show-file-overlay"), - sliceFromEl = document.getElementById("output-file-slice-from"), - sliceToEl = document.getElementById("output-file-slice-to"), - sliceFrom = parseInt(sliceFromEl.value, 10), - sliceTo = parseInt(sliceToEl.value, 10), - str = Utils.arrayBufferToStr(this.dishBuffer.slice(sliceFrom, sliceTo)); - - document.getElementById("output-text").classList.remove("blur"); - showFileOverlay.style.display = "block"; - this.set(str, "string", new Date().getTime() - startTime, true); - } - - - /** - * Handler for show file overlay events. - * - * @param {Event} e - */ - showFileOverlayClick(e) { - const outputFile = document.getElementById("output-file"), - showFileOverlay = e.target; - - document.getElementById("output-text").classList.add("blur"); - outputFile.style.display = "block"; - showFileOverlay.style.display = "none"; - this.setOutputInfo(this.dishBuffer.byteLength, null, 0); - } - - - /** - * Displays information about the output. - * - * @param {number} length - The length of the current output string - * @param {number} lines - The number of the lines in the current output string - * @param {number} duration - The length of time (ms) it took to generate the output - */ - setOutputInfo(length, lines, duration) { - let width = length.toString().length; - width = width < 4 ? 4 : width; - - const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " "); - const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " "); - - let msg = "time: " + timeStr + "
length: " + lengthStr; - - if (typeof lines === "number") { - const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " "); - msg += "
lines: " + linesStr; - } - - document.getElementById("output-info").innerHTML = msg; - document.getElementById("input-selection-info").innerHTML = ""; - document.getElementById("output-selection-info").innerHTML = ""; - } - - - /** - * Handler for save click events. - * Saves the current output to a file. - */ - saveClick() { - this.downloadFile(); - } - - - /** - * Handler for copy click events. - * Copies the output to the clipboard. - */ - async copyClick() { - await this.getDishStr(); - - // Create invisible textarea to populate with the raw dish string (not the printable version that - // contains dots instead of the actual bytes) - const textarea = document.createElement("textarea"); - textarea.style.position = "fixed"; - textarea.style.top = 0; - textarea.style.left = 0; - textarea.style.width = 0; - textarea.style.height = 0; - textarea.style.border = "none"; - - textarea.value = this.dishStr; - document.body.appendChild(textarea); - - // Select and copy the contents of this textarea - let success = false; - try { - textarea.select(); - success = this.dishStr && document.execCommand("copy"); - } catch (err) { - success = false; - } - - if (success) { - this.app.alert("Copied raw output successfully.", 2000); - } else { - this.app.alert("Sorry, the output could not be copied.", 3000); - } - - // Clean up - document.body.removeChild(textarea); - } - - - /** - * Handler for switch click events. - * Moves the current output into the input textarea. - */ - async switchClick() { - this.switchOrigData = this.manager.input.get(); - document.getElementById("undo-switch").disabled = false; - if (this.dishBuffer) { - this.manager.input.setFile(new File([this.dishBuffer], "output.dat")); - this.manager.input.handleLoaderMessage({ - data: { - progress: 100, - fileBuffer: this.dishBuffer - } - }); - } else { - await this.getDishStr(); - this.app.setInput(this.dishStr); - } - } - - - /** - * Handler for undo switch click events. - * Removes the output from the input and replaces the input that was removed. - */ - undoSwitchClick() { - this.app.setInput(this.switchOrigData); - const undoSwitch = document.getElementById("undo-switch"); - undoSwitch.disabled = true; - $(undoSwitch).tooltip("hide"); - } - - - /** - * Handler for maximise output click events. - * Resizes the output frame to be as large as possible, or restores it to its original size. - */ - maximiseOutputClick(e) { - const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode; - - if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) { - this.app.initialiseSplitter(true); - this.app.columnSplitter.collapse(0); - this.app.columnSplitter.collapse(1); - this.app.ioSplitter.collapse(0); - - $(el).attr("data-original-title", "Restore output pane"); - el.querySelector("i").innerHTML = "fullscreen_exit"; - } else { - $(el).attr("data-original-title", "Maximise output pane"); - el.querySelector("i").innerHTML = "fullscreen"; - this.app.initialiseSplitter(false); - this.app.resetLayout(); - } - } - - /** * Save bombe object then remove it from the DOM so that it does not cause performance issues. */ @@ -370,7 +325,6 @@ class OutputWaiter { this.bombeEl.parentNode.removeChild(this.bombeEl); } - /** * Shows or hides the output loading screen. * The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU @@ -402,7 +356,6 @@ class OutputWaiter { outputElement.disabled = true; outputLoader.style.visibility = "visible"; outputLoader.style.opacity = 1; - this.manager.controls.toggleBakeButtonFunction(true); }.bind(this), 200); } else { // Remove the Bombe from the DOM to save resources @@ -414,262 +367,287 @@ class OutputWaiter { outputElement.disabled = false; outputLoader.style.opacity = 0; outputLoader.style.visibility = "hidden"; - this.manager.controls.toggleBakeButtonFunction(false); - this.setStatusMsg(""); + // this.setStatusMsg(""); } } - /** - * Sets the baking status message value. + * Adds a new output tab. * - * @param {string} msg + * @param {number} inputNum + * @param {boolean} [changeTab=true] */ - setStatusMsg(msg) { - const el = document.querySelector("#output-loader .loading-msg"); + addTab(inputNum, changeTab = true) { + const tabsWrapper = document.getElementById("output-tabs"); + const numTabs = tabsWrapper.children.length; - el.textContent = msg; - } + if (numTabs < this.maxTabs) { + // Create a new tab element + const newTab = this.createTabElement(inputNum); + tabsWrapper.appendChild(newTab); - /** - * Returns true if the output contains carriage returns - * - * @returns {boolean} - */ - async containsCR() { - await this.getDishStr(); - return this.dishStr.indexOf("\r") >= 0; - } + if (numTabs > 0) { + tabsWrapper.parentElement.style.display = "block"; + // output tab buttons? - - /** - * Retrieves the current dish as a string, returning the cached version if possible. - * - * @returns {string} - */ - async getDishStr() { - if (this.dishStr) return this.dishStr; - - this.dishStr = await new Promise(resolve => { - this.manager.worker.getDishAs(this.app.dish, "string", r => { - resolve(r.value); - }); - }); - return this.dishStr; - } - - - /** - * Retrieves the current dish as an ArrayBuffer, returning the cached version if possible. - * - * @returns {ArrayBuffer} - */ - async getDishBuffer() { - if (this.dishBuffer) return this.dishBuffer; - - this.dishBuffer = await new Promise(resolve => { - this.manager.worker.getDishAs(this.app.dish, "ArrayBuffer", r => { - resolve(r.value); - }); - }); - return this.dishBuffer; - } - - - /** - * Triggers the BackgroundWorker to attempt Magic on the current output. - */ - backgroundMagic() { - this.hideMagicButton(); - if (!this.app.options.autoMagic) return; - - const sample = this.dishStr ? this.dishStr.slice(0, 1000) : - this.dishBuffer ? this.dishBuffer.slice(0, 1000) : ""; - - if (sample.length) { - this.manager.background.magic(sample); - } - } - - - /** - * Handles the results of a background Magic call. - * - * @param {Object[]} options - */ - backgroundMagicResult(options) { - if (!options.length || - !options[0].recipe.length) - return; - - const currentRecipeConfig = this.app.getRecipeConfig(); - const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe); - const opSequence = options[0].recipe.map(o => o.op).join(", "); - - this.showMagicButton(opSequence, options[0].data, newRecipeConfig); - } - - - /** - * Handler for Magic click events. - * - * Loads the Magic recipe. - * - * @fires Manager#statechange - */ - magicClick() { - const magicButton = document.getElementById("magic"); - this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe"))); - window.dispatchEvent(this.manager.statechange); - this.hideMagicButton(); - } - - - /** - * Displays the Magic button with a title and adds a link to a complete recipe. - * - * @param {string} opSequence - * @param {string} result - * @param {Object[]} recipeConfig - */ - showMagicButton(opSequence, result, recipeConfig) { - const magicButton = document.getElementById("magic"); - magicButton.setAttribute("data-original-title", `${opSequence} will produce "${Utils.escapeHtml(Utils.truncate(result), 30)}"`); - magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, ""); - magicButton.classList.remove("hidden"); - } - - - /** - * Hides the Magic button and resets its values. - */ - hideMagicButton() { - const magicButton = document.getElementById("magic"); - magicButton.classList.add("hidden"); - magicButton.setAttribute("data-recipe", ""); - magicButton.setAttribute("data-original-title", "Magic!"); - } - - - /** - * Handler for extract file events. - * - * @param {Event} e - */ - async extractFileClick(e) { - e.preventDefault(); - e.stopPropagation(); - - const el = e.target.nodeName === "I" ? e.target.parentNode : e.target; - const blobURL = el.getAttribute("blob-url"); - const fileName = el.getAttribute("file-name"); - - const blob = await fetch(blobURL).then(r => r.blob()); - this.manager.input.loadFile(new File([blob], fileName, {type: blob.type})); - } - - /** - * Function to create a new tab - * - * @param inputNum - */ - addTab(inputNum) { - const tabWrapper = document.getElementById("output-tabs"); - const tabsList = tabWrapper.firstElementChild; - - if (tabsList.children.length > 0) { - tabWrapper.style.display = "block"; - } - - document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; - document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; - document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; - - const newTab = document.createElement("li"); - newTab.id = `output-tab-${inputNum}`; - if (inputNum === this.manager.input.getActiveTab()) { - newTab.classList.add("active-output-tab"); - } - - const newTabContent = document.createElement("div"); - newTabContent.classList.add("output-tab-content"); - newTabContent.innerText = `Tab ${inputNum}`; - - newTab.appendChild(newTabContent); - tabsList.appendChild(newTab); - } - - /** - * Function to change tabs - * - * @param {Element} tabElement - */ - changeTab(tabElement, changeInput=false) { - const liItem = tabElement.parentElement; - const newTabNum = liItem.id.replace("output-tab-", ""); - const currentTabNum = this.manager.input.getActiveTab(); - - const activeTabs = document.getElementsByClassName("active-output-tab"); - for (let i = 0; i < activeTabs.length; i++) { - activeTabs.item(i).classList.remove("active-output-tab"); - } - - document.getElementById(`output-tab-${currentTabNum}`).classList.remove("active-output-tab"); - liItem.classList.add("active-output-tab"); - - for (let i = 0; i < this.outputs.length; i++) { - if (this.outputs[i].inputNum === newTabNum) { - this.set(this.outputs[i].data.result, this.outputs[i].data.type, this.outputs[0].data.duration); + document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; + document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; + document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; + document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))"; } } - if (changeInput) { - this.manager.input.changeTab(document.getElementById(`input-tab-${newTabNum}`).firstElementChild, false); + + if (changeTab) { + this.changeTab(inputNum); } } + /** + * Changes the active tab + * + * @param {number} inputNum + */ + changeTab(inputNum) { + const currentNum = this.getActiveTab(); + if (this.getOutputIndex(inputNum) === -1) return; + + const tabsWrapper = document.getElementById("output-tabs"); + const tabs = tabsWrapper.children; + + let found = false; + for (let i = 0; i < tabs.length; i++) { + if (tabs.item(i).getAttribute("inputNum") === inputNum.toString()) { + tabs.item(i).classList.add("active-output-tab"); + found = true; + } else { + tabs.item(i).classList.remove("active-output-tab"); + } + } + if (!found) { + let direction = "right"; + if (currentNum > inputNum) { + direction = "left"; + } + + const newOutputs = this.getNearbyNums(inputNum, direction); + for (let i = 0; i < newOutputs.length; i++) { + tabs.item(i).setAttribute("inputNum", newOutputs[i].toString()); + this.displayTabInfo(newOutputs[i]); + if (newOutputs[i] === inputNum) { + tabs.item(i).classList.add("active-input-tab"); + } + } + } + + this.set(inputNum); + } + /** * Handler for changing tabs event * * @param {event} mouseEvent */ changeTabClick(mouseEvent) { - if (!mouseEvent.srcElement) { - return; + if (!mouseEvent.srcElement) return; + const tabNum = mouseEvent.srcElement.parentElement.getAttribute("inputNum"); + if (tabNum) { + this.changeTab(parseInt(tabNum, 10)); } - this.changeTab(mouseEvent.srcElement, true); } /** - * Removes a tab from the output window, along with the value for it in outputs + * Generates a list of the nearby inputNums * - * @param {string} inputNum - * @param {string} newActiveNum + * @param {number} inputNum + * @param {string} direction */ - removeTab(inputNum, newActiveNum) { - const tabLiItem = document.getElementById(`output-tab-${inputNum}`); - - if (tabLiItem.parentElement.children.length === 2) { - document.getElementById("output-tabs").style.display = "none"; - - document.getElementById("output-wrapper").style.height = "calc(100% - var(--title-height))"; - document.getElementById("output-highlighter").style.height = "calc(100% - var(--title-height))"; - document.getElementById("output-file").style.height = "calc(100% - var(--title-height))"; - - } - - tabLiItem.parentElement.removeChild(tabLiItem); - - for (let i = 0; i < this.outputs.length; i++) { - if (this.outputs[i].inputNum === inputNum) { - this.outputs.splice(i, 1); - break; + getNearbyNums(inputNum, direction) { + const nums = []; + if (direction === "left") { + let reachedEnd = false; + for (let i = 0; i < this.maxTabs; i++) { + let newNum; + if (i === 0) { + newNum = inputNum; + } else { + newNum = this.getNextInputNum(nums[i-1]); + } + if (newNum === nums[i-1]) { + reachedEnd = true; + nums.sort(function(a, b) { + return b - a; + }); + } + if (reachedEnd) { + newNum = this.getPreviousInputNum(nums[i-1]); + } + if (newNum >= 0) { + nums.push(newNum); + } + } + } else { + let reachedEnd = false; + for (let i = 0; i < this.maxTabs; i++) { + let newNum; + if (i === 0) { + newNum = inputNum; + } else { + if (!reachedEnd) { + newNum = this.getPreviousInputNum(nums[i-1]); + } + if (newNum === nums[i-1]) { + reachedEnd = true; + nums.sort(function(a, b) { + return b - a; + }); + } + if (reachedEnd) { + newNum = this.getNextInputNum(nums[i-1]); + } + } + if (newNum >= 0) { + nums.push(newNum); + } } } - - this.changeTab(document.getElementById(`output-tab-${newActiveNum}`), false); + nums.sort(function(a, b) { + return a - b; + }); + return nums; } + /** + * Gets the largest inputNum + * + * @returns {number} + */ + getLargestInputNum() { + let largest = 0; + for (let i = 0; i < this.outputs.length; i++) { + if (this.outputs[i].inputNum > largest) { + largest = this.outputs[i].inputNum; + } + } + return largest; + } + + /** + * Gets the previous inputNum + * + * @param {number} inputNum - The current input number + * @returns {number} + */ + getPreviousInputNum(inputNum) { + let num = -1; + for (let i = 0; i < this.outputs.length; i++) { + if (this.outputs[i].inputNum < inputNum) { + if (this.outputs[i].inputNum > num) { + num = this.outputs[i].inputNum; + } + } + } + return num; + } + + /** + * Gets the next inputNum + * + * @param {number} inputNum - The current input number + * @returns {number} + */ + getNextInputNum(inputNum) { + let num = this.getLargestInputNum(); + for (let i = 0; i < this.outputs.length; i++) { + if (this.outputs[i].inputNum > inputNum) { + if (this.outputs[i].inputNum < num) { + num = this.outputs[i].inputNum; + } + } + } + return num; + } + + /** + * Removes a tab and it's corresponding output + * + * @param {number} inputNum + */ + removeTab(inputNum) { + if (this.getOutputIndex(inputNum) === -1) return; + + const tabElement = this.getTabItem(inputNum); + + this.removeOutput(inputNum); + + if (tabElement !== null) { + // find new tab number? + } + } + + /** + * Creates a new tab element to be added to the tab bar + * + * @param {number} inputNum + */ + createTabElement(inputNum) { + const newTab = document.createElement("li"); + newTab.setAttribute("inputNum", inputNum.toString()); + + const newTabContent = document.createElement("div"); + newTabContent.classList.add("output-tab-content"); + newTabContent.innerText = `Tab ${inputNum.toString()}`; + + // Do we want remove tab button on output? + newTab.appendChild(newTabContent); + + return newTab; + } + + /** + * Gets the number of the current active tab + * + * @returns {number} + */ + getActiveTab() { + const activeTabs = document.getElementsByClassName("active-output-tab"); + if (activeTabs.length > 0) { + const activeTab = activeTabs.item(0); + const tabNum = activeTab.getAttribute("inputNum"); + return parseInt(tabNum, 10); + } + return -1; + } + + /** + * Gets the li element for a tab + * + * @param {number} inputNum + */ + getTabItem(inputNum) { + const tabs = document.getElementById("output-tabs").children; + for (let i = 0; i < tabs.length; i++) { + if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) { + return tabs.item(i); + } + } + } + + /** + * Display output information in the tab header + * + * @param {number} inputNum + */ + displayTabInfo(inputNum) { + const tabItem = this.getTabItem(inputNum); + + if (!tabItem) return; + + const tabContent = tabItem.firstElementChild; + + tabContent.innerText = `Tab ${inputNum}`; + + } } export default OutputWaiter; diff --git a/src/web/WorkerWaiter.mjs b/src/web/WorkerWaiter.mjs index a219abe4..67209eae 100644 --- a/src/web/WorkerWaiter.mjs +++ b/src/web/WorkerWaiter.mjs @@ -8,85 +8,170 @@ import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker"; /** - * Waiter to handle conversations with the ChefWorker. + * Waiter to handle conversations with the ChefWorker */ class WorkerWaiter { /** - * WorkerWaiter constructor. + * WorkerWaiter constructor * * @param {App} app - The main view object for CyberChef - * @param {Manager} manager - The CyberChef event manager. + * @param {Manager} manager - The CyberChef event manager */ constructor(app, manager) { this.app = app; this.manager = manager; - this.callbacks = {}; - this.callbackID = 0; - this.pendingInputs = []; - this.runningWorkers = 0; this.chefWorkers = []; - this.outputs = []; + this.maxWorkers = navigator.hardwareConcurrency || 4; + this.inputs = []; + this.totalOutputs = 0; } /** - * Sets up a pool of ChefWorkers to be used for baking + * Terminates any existing ChefWorkers and sets up a new worker */ - setupChefWorkers() { - const threads = navigator.hardwareConcurrency || 4; // Default to 4 + setupChefWorker() { + for (let i = 0; i < this.chefWorkers.length; i++) { + const worker = this.chefWorkers.pop(); + worker.terminate(); + } - for (let i = 0; i < threads; i++) { - const newWorker = new ChefWorker(); - newWorker.addEventListener("message", this.handleChefMessage.bind(this)); - let docURL = document.location.href.split(/[#?]/)[0]; - const index = docURL.lastIndexOf("/"); - if (index > 0) { - docURL = docURL.substring(0, index); + this.addChefWorker(); + } + + /** + * Adds a new ChefWorker + * + * @returns {number} The index of the created worker + */ + addChefWorker() { + // First find if there are any inactive workers, as this will be + // more efficient than creating a new one + for (let i = 0; i < this.chefWorkers.length; i++) { + if (!this.chefWorkers[i].active) { + return i; } - newWorker.postMessage({"action": "docURL", "data": docURL}); - newWorker.postMessage({"action": "inputNum", "data": 0}); + } - this.chefWorkers.push({ - worker: newWorker, - inputNum: 0 - }); + if (this.chefWorkers.length === this.maxWorkers) { + // Can't create any more workers + return -1; + } + + log.debug("Adding new ChefWorker"); + + // Create a new ChefWorker and send it the docURL + const newWorker = new ChefWorker(); + newWorker.addEventListener("message", this.handleChefMessage.bind(this)); + let docURL = document.location.href.split(/[#?]/)[0]; + const index = docURL.lastIndexOf("/"); + if (index > 0) { + docURL = docURL.substring(0, index); + } + newWorker.postMessage({"action": "docURL", "data": docURL}); + + // Store the worker, whether or not it's active, and the inputNum as an object + const newWorkerObj = { + worker: newWorker, + active: false, + inputNum: this.manager.input.getActiveTab() + }; + + this.chefWorkers.push(newWorkerObj); + return this.chefWorkers.indexOf(newWorkerObj); + } + + /** + * Removes a ChefWorker + * + * @param {Object} workerObj + */ + removeChefWorker(workerObj) { + const index = this.chefWorkers.indexOf(workerObj); + if (index === -1) { + return; + } + + this.chefWorkers[index].worker.terminate(); + this.chefWorkers.splice(index, 1); + + // There should always be a ChefWorker loaded + if (this.chefWorkers.length === 0) { + this.addChefWorker(); } } + /** + * Finds and returns the object for the ChefWorker of a given inputNum + * + * @param {number} inputNum + */ + getChefWorker(inputNum) { + for (let i = 0; i < this.chefWorkers.length; i++) { + if (this.chefWorkers[i].inputNum === inputNum) { + return this.chefWorkers[i]; + } + } + } /** - * Handler for messages sent back by the ChefWorker. + * Handler for messages sent back by the ChefWorkers * * @param {MessageEvent} e */ handleChefMessage(e) { const r = e.data; - log.debug("Receiving '" + r.action + "' from ChefWorker"); + let inputNum = 0; + log.debug(`Receiving ${r.action} from ChefWorker.`); + + if (r.data.hasOwnProperty("inputNum")) { + inputNum = r.data.inputNum; + } + + const currentWorker = this.getChefWorker(inputNum); switch (r.action) { case "bakeComplete": - this.runningWorkers -= 1; - this.outputs.push({ - data: r.data, - inputNum: r.data.inputNum - }); - if (this.pendingInputs.length > 0) { - log.debug(`Bake ${r.data.inputNum} complete. Baking next input`); - this.bakeNextInput(r.data.inputNum); - } else if (this.runningWorkers <= 0) { - this.runningWorkers = 0; - this.recipeConfig = undefined; - this.options = undefined; - this.progress = undefined; - this.step = undefined; - this.bakingComplete(); + log.debug(`Bake ${inputNum} complete.`); + this.updateOutput(r.data, r.data.inputNum); + + if (this.inputs.length > 0) { + const nextInput = this.inputs.pop(); + log.debug(`Baking input ${nextInput.inputNum}.`); + this.manager.output.updateOutputStatus("baking", nextInput.inputNum); + this.manager.output.updateOutputMessage("Baking...", nextInput.inputNum); + currentWorker.inputNum = nextInput.inputNum; + currentWorker.active = true; + currentWorker.worker.postMessage({ + action: "bake", + data: { + input: nextInput.input, + recipeConfig: nextInput.recipeConfig, + options: nextInput.options, + progress: nextInput.progress, + step: nextInput.step, + inputNum: nextInput.inputNum + } + }); + this.displayProgress(); + } else { + // The ChefWorker is no longer needed + log.debug("No more inputs to bake. Closing ChefWorker."); + this.removeChefWorker(currentWorker); + + this.displayProgress(); + + const progress = this.getBakeProgress(); + if (progress.total === progress.baked) { + this.bakingComplete(); + } } + break; - case "bakeError": - this.runningWorkers -= 1; - this.app.handleError(r.data); - this.setBakingStatus(false); + case "BakeError": + this.manager.output.updateOutputError(r.data, inputNum); + // do more here break; case "dishReturned": this.callbacks[r.data.id](r.data); @@ -95,17 +180,20 @@ class WorkerWaiter { break; case "workerLoaded": this.app.workerLoaded = true; - log.debug("ChefWorker loaded"); + log.debug("ChefWorker loaded."); this.app.loaded(); break; case "statusMessage": - this.manager.output.setStatusMsg(r.data); + // Status message should be done per output + // log.error(r); + this.manager.output.updateOutputMessage(r.data.message, r.data.inputNum); break; case "optionUpdate": log.debug(`Setting ${r.data.option} to ${r.data.value}`); this.app.options[r.data.option] = r.data.value; break; case "setRegisters": + // Should this update with the tabs? this.manager.recipe.setRegisters(r.data.opIndex, r.data.numPrevRegisters, r.data.registers); break; case "highlightsCalculated": @@ -117,78 +205,96 @@ class WorkerWaiter { } } + /** + * Update the value of an output + * + * @param {Object} data + * @param {number} inputNum + */ + updateOutput(data, inputNum) { + + this.manager.output.updateOutputValue(data, inputNum); + this.manager.output.updateOutputStatus("baked", inputNum); + + this.manager.recipe.updateBreakpointIndicator(this.app.progress); + } /** * Updates the UI to show if baking is in process or not. * - * @param {bakingStatus} + * @param {boolean} bakingStatus */ setBakingStatus(bakingStatus) { this.app.baking = bakingStatus; - - this.manager.output.toggleLoader(bakingStatus); + this.manager.controls.toggleBakeButtonFunction(bakingStatus); } + /** + * Get the progress of the ChefWorkers + */ + getBakeProgress() { + const pendingInputs = this.inputs.length; + let bakingInputs = 0; + + for (let i = 0; i < this.chefWorkers.length; i++) { + if (this.chefWorkers[i].active) { + bakingInputs++; + } + } + + const total = this.totalOutputs; + const bakedInputs = total - pendingInputs - bakingInputs; + + return { + total: total, + pending: pendingInputs, + baking: bakingInputs, + baked: bakedInputs + }; + } /** - * Calcels the current bake by terminating and removing all ChefWorkers, - * and creating a new one + * Cancels the current bake by terminating and removing all ChefWorkers */ cancelBake() { for (let i = this.chefWorkers.length - 1; i >= 0; i--) { - this.chefWorkers[i].worker.terminate(); - this.chefWorkers.pop(); + this.removeChefWorker(this.chefWorkers[i]); } - this.setupChefWorkers(); this.setBakingStatus(false); + this.inputs = []; + this.totalOutputs = 0; this.manager.controls.showStaleIndicator(); + this.displayProgress(); } - /** * Handler for completed bakes */ bakingComplete() { this.setBakingStatus(false); - if (this.pendingInputs.length !== 0) return; + // look into changing this to something better + // for (let i = 0; i < this.outputs.length; i++) { + // if (this.outputs[i].data.error) { + // this.app.handleError(this.outputs[i].error); + // } + // } - for (let i = 0; i < this.outputs.length; i++) { - if (this.outputs[i].error) { - this.app.handleError(this.outputs[i].error); - } - } - - this.app.progress = this.outputs[0].data.progress; - this.app.dish = this.outputs[0].data.dish; + // What are these for? + // Should be a value for each input, not just one + // this.app.progress = this.outputs[0].data.progress; + // this.app.dish = this.outputs[0].data.dish; this.manager.recipe.updateBreakpointIndicator(this.app.progress); - this.manager.output.multiSet(this.outputs); + // Don't need to update the output here as updateOutput() will take care of that + document.getElementById("bake").style.background = ""; + this.totalOutputs = 0; // Reset for next time log.debug("--- Bake complete ---"); } /** - * Handler for completed bakes - * - * @param {Object} response - */ - bakingCompleteOld(response) { - this.setBakingStatus(false); - - if (!response) return; - - if (response.error) { - this.app.handleError(response.error); - } - - this.app.progress = response.progress; - this.app.dish = response.dish; - this.manager.recipe.updateBreakpointIndicator(response.progress); - this.manager.output.set(response.result, response.type, response.duration); - log.debug("--- Bake complete ---"); - } - - /** - * Asks the ChefWorker to bake the current input using the current recipe. + * Bakes the current input using the current recipe. + * Either sends the input and recipe to a ChefWorker, + * or, if there's already the max running, adds it to inputs * * @param {string | Array} input * @param {Object[]} recipeConfig @@ -199,67 +305,55 @@ class WorkerWaiter { bake(input, recipeConfig, options, progress, step) { this.setBakingStatus(true); - this.recipeConfig = recipeConfig; - this.options = options; - this.progress = progress; - this.step = step; - this.outputs = []; - if (typeof input === "string") { input = [{ input: input, - inputNum: 0 + inputNum: this.manager.input.getActiveTab() }]; } - const initialInputs = input.slice(0, this.chefWorkers.length); - this.pendingInputs = input.slice(this.chefWorkers.length, input.length); - this.runningWorkers = 0; - - for (let i = 0; i < initialInputs.length; i++) { - this.runningWorkers += 1; - this.chefWorkers[i].inputNum = initialInputs[i].inputNum; - this.chefWorkers[i].worker.postMessage({ - action: "bake", - data: { - input: initialInputs[i].input, + for (let i = 0; i < input.length; i++) { + this.totalOutputs++; + this.manager.output.updateOutputStatus("pending", input[i].inputNum); + this.manager.output.updateOutputMessage(`Input ${input[i].inputNum} has not been baked yet.`, input[i].inputNum); + // If an input exists for the current inputNum, remove it + for (let x = 0; x < this.inputs.length; x++) { + if (this.inputs[x].inputNum === input[i].inputNum) { + this.inputs.splice(x, 1); + } + } + const workerId = this.addChefWorker(); + if (workerId !== -1) { + // Send the input to the ChefWorker + this.manager.output.updateOutputStatus("baking", input[i].inputNum); + this.manager.output.updateOutputMessage("Baking...", input[i].inputNum); + this.chefWorkers[workerId].active = true; + this.chefWorkers[workerId].inputNum = input[i].inputNum; + this.chefWorkers[workerId].worker.postMessage({ + action: "bake", + data: { + input: input[i].input, + recipeConfig: recipeConfig, + options: options, + progress: progress, + step: step, + inputNum: input[i].inputNum + } + }); + } else { + // Add the input to inputs so it can be processed when ready + this.inputs.push({ + input: input[i].input, recipeConfig: recipeConfig, options: options, progress: progress, step: step, - inputNum: initialInputs[i].inputNum - } - }); - } - } - - - /** - * - * @param inputNum - */ - bakeNextInput(inputNum) { - this.runningWorkers += 1; - const nextInput = this.pendingInputs.pop(); - for (let i = 0; i < this.chefWorkers.length; i++) { - if (this.chefWorkers[i].inputNum === inputNum) { - this.chefWorkers[i].inputNum = nextInput.inputNum; - this.chefWorkers[i].worker.postMessage({ - action: "bake", - data: { - input: nextInput.input, - recipeConfig: this.recipeConfig, - options: this.options, - progress: this.progress, - step: this.step, - inputNum: nextInput.inputNum - } + inputNum: input[i].inputNum }); } } } - /** * Asks the ChefWorker to run a silent bake, forcing the browser to load and cache all the relevant * JavaScript code needed to do a real bake. @@ -267,7 +361,11 @@ class WorkerWaiter { * @param {Object[]} [recipeConfig] */ silentBake(recipeConfig) { - this.chefWorkers[0].worker.postMessage({ + // If there aren't any active ChefWorkers, addChefWorker will + // return an inactive worker instead of creating a new one + const workerId = this.addChefWorker(); + if (workerId === -1) return; + this.chefWorkers[workerId].worker.postMessage({ action: "silentBake", data: { recipeConfig: recipeConfig @@ -275,28 +373,6 @@ class WorkerWaiter { }); } - - /** - * Asks the ChefWorker to calculate highlight offsets if possible. - * - * @param {Object[]} recipeConfig - * @param {string} direction - * @param {Object} pos - The position object for the highlight. - * @param {number} pos.start - The start offset. - * @param {number} pos.end - The end offset. - */ - highlight(recipeConfig, direction, pos) { - this.chefWorkers[0].postMessage({ - action: "highlight", - data: { - recipeConfig: recipeConfig, - direction: direction, - pos: pos - } - }); - } - - /** * Asks the ChefWorker to return the dish as the specified type * @@ -306,8 +382,11 @@ class WorkerWaiter { */ getDishAs(dish, type, callback) { const id = this.callbackID++; + const workerId = this.addChefWorker(); + if (workerId === -1) return; + this.callbacks[id] = callback; - this.chefWorkers[0].worker.postMessage({ + this.chefWorkers[workerId].worker.postMessage({ action: "getDishAs", data: { dish: dish, @@ -317,15 +396,12 @@ class WorkerWaiter { }); } - /** - * Sets the console log level in the worker. + * Sets the console log level in the workers. * * @param {string} level */ setLogLevel(level) { - if (!this.chefWorkers || !this.chefWorkers.length > 0) return; - for (let i = 0; i < this.chefWorkers.length; i++) { this.chefWorkers[i].worker.postMessage({ action: "setLogLevel", @@ -333,7 +409,46 @@ class WorkerWaiter { }); } } + + /** + * Display the bake progress in the output bar and bake button + */ + displayProgress() { + const progress = this.getBakeProgress(); + const percentComplete = ((progress.pending + progress.baking) / progress.total) * 100; + const bakeButton = document.getElementById("bake"); + if (this.app.baking) { + if (percentComplete < 100) { + document.getElementById("bake").style.background = `linear-gradient(to left, #fea79a ${percentComplete}%, #f44336 ${percentComplete}%)`; + } else { + bakeButton.style.background = ""; + } + } else { + // not baking + bakeButton.style.background = ""; + } + + const bakeInfo = document.getElementById("bake-info"); + let width = progress.total.toString().length; + width = width < 2 ? 2 : width; + + const totalStr = progress.total.toString().padStart(width, " ").replace(/ /g, " "); + const bakedStr = progress.baked.toString().padStart(width, " ").replace(/ /g, " "); + const pendingStr = progress.pending.toString().padStart(width, " ").replace(/ /g, " "); + const bakingStr = progress.baking.toString().padStart(width, " ").replace(/ /g, " "); + + let msg = "Total: " + totalStr; + msg += "
Baked: " + bakedStr; + + if (progress.pending > 0) { + msg += "
Pending: " + pendingStr; + } else if (progress.baking > 0) { + msg += "
Baking: " + bakingStr; + } + + bakeInfo.innerHTML = msg; + + } } - export default WorkerWaiter; diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 7ff04dea..31935f76 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -45,8 +45,8 @@ -moz-padding-start: 1px; /* Fixes bug in Firefox */ } -#input-tabs ul, -#output-tabs ul { +#input-tabs-wrapper #input-tabs, +#output-tabs-wrapper #output-tabs { list-style: none; background-color: var(--title-background-colour); padding: 0; @@ -59,8 +59,8 @@ height: var(--tab-height); } -#input-tabs ul li, -#output-tabs ul li { +#input-tabs li, +#output-tabs li { display: flex; flex-direction: row; width: 100%; @@ -73,8 +73,8 @@ vertical-align: middle; } -#input-tabs ul li:hover, -#output-tabs ul li:hover { +#input-tabs li:hover, +#output-tabs li:hover { cursor: pointer; background-color: var(--primary-background-colour); } @@ -85,6 +85,11 @@ background-color: var(--primary-background-colour); } +.tab-buttons { + transition: 1s all ease; + display: none; +} + .input-tab-content, .output-tab-content { width: 100%; @@ -142,10 +147,10 @@ #output-loader { position: absolute; - top: 0; + bottom: 0; left: 0; width: 100%; - height: 100%; + height: calc(100% - var(--title-height)); margin: 0; background-color: var(--primary-background-colour); visibility: hidden;