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;